omdev 0.0.0.dev223__py3-none-any.whl → 0.0.0.dev225__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- omdev/amalg/typing.py +7 -1
- omdev/ci/cache.py +108 -0
- omdev/ci/ci.py +1 -1
- omdev/ci/docker/cacheserved.py +262 -0
- omdev/ci/docker/dataserver.py +204 -0
- omdev/ci/docker/imagepulling.py +2 -1
- omdev/ci/docker/packing.py +72 -0
- omdev/ci/docker/repositories.py +40 -0
- omdev/ci/github/cache.py +20 -1
- omdev/ci/github/client.py +9 -2
- omdev/ci/github/inject.py +4 -4
- omdev/ci/requirements.py +1 -1
- omdev/ci/utils.py +0 -49
- omdev/dataserver/targets.py +32 -0
- omdev/git/revisions.py +2 -2
- omdev/git/shallow.py +1 -1
- omdev/git/status.py +1 -1
- omdev/oci/data.py +19 -0
- omdev/oci/dataserver.py +4 -1
- omdev/oci/pack/__init__.py +0 -0
- omdev/oci/pack/packing.py +185 -0
- omdev/oci/pack/repositories.py +162 -0
- omdev/oci/{packing.py → pack/unpacking.py} +0 -177
- omdev/oci/repositories.py +6 -0
- omdev/precheck/lite.py +1 -1
- omdev/pyproject/pkg.py +1 -1
- omdev/scripts/ci.py +773 -552
- omdev/scripts/interp.py +230 -299
- omdev/scripts/pyproject.py +328 -256
- {omdev-0.0.0.dev223.dist-info → omdev-0.0.0.dev225.dist-info}/METADATA +2 -2
- {omdev-0.0.0.dev223.dist-info → omdev-0.0.0.dev225.dist-info}/RECORD +35 -28
- {omdev-0.0.0.dev223.dist-info → omdev-0.0.0.dev225.dist-info}/LICENSE +0 -0
- {omdev-0.0.0.dev223.dist-info → omdev-0.0.0.dev225.dist-info}/WHEEL +0 -0
- {omdev-0.0.0.dev223.dist-info → omdev-0.0.0.dev225.dist-info}/entry_points.txt +0 -0
- {omdev-0.0.0.dev223.dist-info → omdev-0.0.0.dev225.dist-info}/top_level.txt +0 -0
omdev/scripts/ci.py
CHANGED
@@ -89,7 +89,7 @@ InjectorProviderFn = ta.Callable[['Injector'], ta.Any]
|
|
89
89
|
InjectorProviderFnMap = ta.Mapping['InjectorKey', 'InjectorProviderFn']
|
90
90
|
InjectorBindingOrBindings = ta.Union['InjectorBinding', 'InjectorBindings']
|
91
91
|
|
92
|
-
# ../../omlish/subprocesses.py
|
92
|
+
# ../../omlish/subprocesses/base.py
|
93
93
|
SubprocessChannelOption = ta.Literal['pipe', 'stdout', 'devnull'] # ta.TypeAlias
|
94
94
|
|
95
95
|
|
@@ -159,6 +159,27 @@ class ShellCmd:
|
|
159
159
|
)
|
160
160
|
|
161
161
|
|
162
|
+
########################################
|
163
|
+
# ../utils.py
|
164
|
+
|
165
|
+
|
166
|
+
##
|
167
|
+
|
168
|
+
|
169
|
+
def read_yaml_file(yaml_file: str) -> ta.Any:
|
170
|
+
yaml = __import__('yaml')
|
171
|
+
|
172
|
+
with open(yaml_file) as f:
|
173
|
+
return yaml.safe_load(f)
|
174
|
+
|
175
|
+
|
176
|
+
##
|
177
|
+
|
178
|
+
|
179
|
+
def sha256_str(s: str) -> str:
|
180
|
+
return hashlib.sha256(s.encode('utf-8')).hexdigest()
|
181
|
+
|
182
|
+
|
162
183
|
########################################
|
163
184
|
# ../../../omlish/asyncs/asyncio/asyncio.py
|
164
185
|
|
@@ -1146,6 +1167,63 @@ class ProxyLogHandler(ProxyLogFilterer, logging.Handler):
|
|
1146
1167
|
self._underlying.handleError(record)
|
1147
1168
|
|
1148
1169
|
|
1170
|
+
########################################
|
1171
|
+
# ../../../omlish/logs/timing.py
|
1172
|
+
|
1173
|
+
|
1174
|
+
##
|
1175
|
+
|
1176
|
+
|
1177
|
+
class LogTimingContext:
|
1178
|
+
DEFAULT_LOG: ta.ClassVar[ta.Optional[logging.Logger]] = None
|
1179
|
+
|
1180
|
+
class _NOT_SPECIFIED: # noqa
|
1181
|
+
def __new__(cls, *args, **kwargs): # noqa
|
1182
|
+
raise TypeError
|
1183
|
+
|
1184
|
+
def __init__(
|
1185
|
+
self,
|
1186
|
+
description: str,
|
1187
|
+
*,
|
1188
|
+
log: ta.Union[logging.Logger, ta.Type[_NOT_SPECIFIED], None] = _NOT_SPECIFIED, # noqa
|
1189
|
+
level: int = logging.DEBUG,
|
1190
|
+
) -> None:
|
1191
|
+
super().__init__()
|
1192
|
+
|
1193
|
+
self._description = description
|
1194
|
+
if log is self._NOT_SPECIFIED:
|
1195
|
+
log = self.DEFAULT_LOG # noqa
|
1196
|
+
self._log: ta.Optional[logging.Logger] = log # type: ignore
|
1197
|
+
self._level = level
|
1198
|
+
|
1199
|
+
def set_description(self, description: str) -> 'LogTimingContext':
|
1200
|
+
self._description = description
|
1201
|
+
return self
|
1202
|
+
|
1203
|
+
_begin_time: float
|
1204
|
+
_end_time: float
|
1205
|
+
|
1206
|
+
def __enter__(self) -> 'LogTimingContext':
|
1207
|
+
self._begin_time = time.time()
|
1208
|
+
|
1209
|
+
if self._log is not None:
|
1210
|
+
self._log.log(self._level, f'Begin : {self._description}') # noqa
|
1211
|
+
|
1212
|
+
return self
|
1213
|
+
|
1214
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
1215
|
+
self._end_time = time.time()
|
1216
|
+
|
1217
|
+
if self._log is not None:
|
1218
|
+
self._log.log(
|
1219
|
+
self._level,
|
1220
|
+
f'End : {self._description} - {self._end_time - self._begin_time:0.2f} s elapsed',
|
1221
|
+
)
|
1222
|
+
|
1223
|
+
|
1224
|
+
log_timing_context = LogTimingContext
|
1225
|
+
|
1226
|
+
|
1149
1227
|
########################################
|
1150
1228
|
# ../../../omlish/os/files.py
|
1151
1229
|
|
@@ -1183,159 +1261,47 @@ def unlinking_if_exists(path: str) -> ta.Iterator[None]:
|
|
1183
1261
|
|
1184
1262
|
|
1185
1263
|
########################################
|
1186
|
-
# ../
|
1187
|
-
|
1188
|
-
|
1189
|
-
|
1264
|
+
# ../docker/utils.py
|
1265
|
+
"""
|
1266
|
+
TODO:
|
1267
|
+
- some less stupid Dockerfile hash
|
1268
|
+
- doesn't change too much though
|
1269
|
+
"""
|
1190
1270
|
|
1191
1271
|
|
1192
1272
|
##
|
1193
1273
|
|
1194
1274
|
|
1195
|
-
|
1196
|
-
|
1197
|
-
|
1198
|
-
def __init__(
|
1199
|
-
self,
|
1200
|
-
*,
|
1201
|
-
version: ta.Optional[CacheVersion] = None,
|
1202
|
-
) -> None:
|
1203
|
-
super().__init__()
|
1204
|
-
|
1205
|
-
if version is None:
|
1206
|
-
version = self.DEFAULT_CACHE_VERSION
|
1207
|
-
check.isinstance(version, int)
|
1208
|
-
check.arg(version >= 0)
|
1209
|
-
self._version: CacheVersion = version
|
1210
|
-
|
1211
|
-
@property
|
1212
|
-
def version(self) -> CacheVersion:
|
1213
|
-
return self._version
|
1214
|
-
|
1215
|
-
#
|
1216
|
-
|
1217
|
-
@abc.abstractmethod
|
1218
|
-
def get_file(self, key: str) -> ta.Awaitable[ta.Optional[str]]:
|
1219
|
-
raise NotImplementedError
|
1220
|
-
|
1221
|
-
@abc.abstractmethod
|
1222
|
-
def put_file(
|
1223
|
-
self,
|
1224
|
-
key: str,
|
1225
|
-
file_path: str,
|
1226
|
-
*,
|
1227
|
-
steal: bool = False,
|
1228
|
-
) -> ta.Awaitable[str]:
|
1229
|
-
raise NotImplementedError
|
1230
|
-
|
1231
|
-
|
1232
|
-
#
|
1233
|
-
|
1234
|
-
|
1235
|
-
class DirectoryFileCache(FileCache):
|
1236
|
-
@dc.dataclass(frozen=True)
|
1237
|
-
class Config:
|
1238
|
-
dir: str
|
1239
|
-
|
1240
|
-
no_create: bool = False
|
1241
|
-
no_purge: bool = False
|
1242
|
-
|
1243
|
-
def __init__(
|
1244
|
-
self,
|
1245
|
-
config: Config,
|
1246
|
-
*,
|
1247
|
-
version: ta.Optional[CacheVersion] = None,
|
1248
|
-
) -> None: # noqa
|
1249
|
-
super().__init__(
|
1250
|
-
version=version,
|
1251
|
-
)
|
1252
|
-
|
1253
|
-
self._config = config
|
1254
|
-
|
1255
|
-
@property
|
1256
|
-
def dir(self) -> str:
|
1257
|
-
return self._config.dir
|
1258
|
-
|
1259
|
-
#
|
1260
|
-
|
1261
|
-
VERSION_FILE_NAME = '.ci-cache-version'
|
1262
|
-
|
1263
|
-
@cached_nullary
|
1264
|
-
def setup_dir(self) -> None:
|
1265
|
-
version_file = os.path.join(self.dir, self.VERSION_FILE_NAME)
|
1266
|
-
|
1267
|
-
if self._config.no_create:
|
1268
|
-
check.state(os.path.isdir(self.dir))
|
1269
|
-
|
1270
|
-
elif not os.path.isdir(self.dir):
|
1271
|
-
os.makedirs(self.dir)
|
1272
|
-
with open(version_file, 'w') as f:
|
1273
|
-
f.write(str(self._version))
|
1274
|
-
return
|
1275
|
-
|
1276
|
-
# NOTE: intentionally raises FileNotFoundError to refuse to use an existing non-cache dir as a cache dir.
|
1277
|
-
with open(version_file) as f:
|
1278
|
-
dir_version = int(f.read().strip())
|
1279
|
-
|
1280
|
-
if dir_version == self._version:
|
1281
|
-
return
|
1282
|
-
|
1283
|
-
if self._config.no_purge:
|
1284
|
-
raise RuntimeError(f'{dir_version=} != {self._version=}')
|
1285
|
-
|
1286
|
-
dirs = [n for n in sorted(os.listdir(self.dir)) if os.path.isdir(os.path.join(self.dir, n))]
|
1287
|
-
if dirs:
|
1288
|
-
raise RuntimeError(
|
1289
|
-
f'Refusing to remove stale cache dir {self.dir!r} '
|
1290
|
-
f'due to present directories: {", ".join(dirs)}',
|
1291
|
-
)
|
1275
|
+
def build_docker_file_hash(docker_file: str) -> str:
|
1276
|
+
with open(docker_file) as f:
|
1277
|
+
contents = f.read()
|
1292
1278
|
|
1293
|
-
|
1294
|
-
if n.startswith('.'):
|
1295
|
-
continue
|
1296
|
-
fp = os.path.join(self.dir, n)
|
1297
|
-
check.state(os.path.isfile(fp))
|
1298
|
-
log.debug('Purging stale cache file: %s', fp)
|
1299
|
-
os.unlink(fp)
|
1279
|
+
return sha256_str(contents)
|
1300
1280
|
|
1301
|
-
os.unlink(version_file)
|
1302
1281
|
|
1303
|
-
|
1304
|
-
f.write(str(self._version))
|
1282
|
+
##
|
1305
1283
|
|
1306
|
-
#
|
1307
1284
|
|
1308
|
-
|
1309
|
-
|
1310
|
-
|
1311
|
-
|
1312
|
-
self.setup_dir()
|
1313
|
-
return os.path.join(self.dir, key)
|
1285
|
+
def read_docker_tar_image_tag(tar_file: str) -> str:
|
1286
|
+
with tarfile.open(tar_file) as tf:
|
1287
|
+
with contextlib.closing(check.not_none(tf.extractfile('manifest.json'))) as mf:
|
1288
|
+
m = mf.read()
|
1314
1289
|
|
1315
|
-
|
1316
|
-
|
1290
|
+
manifests = json.loads(m.decode('utf-8'))
|
1291
|
+
manifest = check.single(manifests)
|
1292
|
+
tag = check.non_empty_str(check.single(manifest['RepoTags']))
|
1293
|
+
return tag
|
1317
1294
|
|
1318
|
-
#
|
1319
1295
|
|
1320
|
-
|
1321
|
-
|
1322
|
-
|
1323
|
-
|
1324
|
-
return cache_file_path
|
1296
|
+
def read_docker_tar_image_id(tar_file: str) -> str:
|
1297
|
+
with tarfile.open(tar_file) as tf:
|
1298
|
+
with contextlib.closing(check.not_none(tf.extractfile('index.json'))) as mf:
|
1299
|
+
i = mf.read()
|
1325
1300
|
|
1326
|
-
|
1327
|
-
|
1328
|
-
|
1329
|
-
|
1330
|
-
*,
|
1331
|
-
steal: bool = False,
|
1332
|
-
) -> str:
|
1333
|
-
cache_file_path = self.get_cache_file_path(key)
|
1334
|
-
if steal:
|
1335
|
-
shutil.move(file_path, cache_file_path)
|
1336
|
-
else:
|
1337
|
-
shutil.copyfile(file_path, cache_file_path)
|
1338
|
-
return cache_file_path
|
1301
|
+
index = json.loads(i.decode('utf-8'))
|
1302
|
+
manifest = check.single(index['manifests'])
|
1303
|
+
image_id = check.non_empty_str(manifest['digest'])
|
1304
|
+
return image_id
|
1339
1305
|
|
1340
1306
|
|
1341
1307
|
########################################
|
@@ -1560,99 +1526,33 @@ def is_in_github_actions() -> bool:
|
|
1560
1526
|
|
1561
1527
|
|
1562
1528
|
########################################
|
1563
|
-
#
|
1529
|
+
# ../../../omlish/argparse/cli.py
|
1530
|
+
"""
|
1531
|
+
TODO:
|
1532
|
+
- default command
|
1533
|
+
- auto match all underscores to hyphens
|
1534
|
+
- pre-run, post-run hooks
|
1535
|
+
- exitstack?
|
1536
|
+
"""
|
1564
1537
|
|
1565
1538
|
|
1566
1539
|
##
|
1567
1540
|
|
1568
1541
|
|
1569
|
-
|
1570
|
-
|
1542
|
+
@dc.dataclass(eq=False)
|
1543
|
+
class ArgparseArg:
|
1544
|
+
args: ta.Sequence[ta.Any]
|
1545
|
+
kwargs: ta.Mapping[str, ta.Any]
|
1546
|
+
dest: ta.Optional[str] = None
|
1571
1547
|
|
1572
|
-
|
1573
|
-
|
1548
|
+
def __get__(self, instance, owner=None):
|
1549
|
+
if instance is None:
|
1550
|
+
return self
|
1551
|
+
return getattr(instance.args, self.dest) # type: ignore
|
1574
1552
|
|
1575
1553
|
|
1576
|
-
|
1577
|
-
|
1578
|
-
|
1579
|
-
def sha256_str(s: str) -> str:
|
1580
|
-
return hashlib.sha256(s.encode('utf-8')).hexdigest()
|
1581
|
-
|
1582
|
-
|
1583
|
-
##
|
1584
|
-
|
1585
|
-
|
1586
|
-
class LogTimingContext:
|
1587
|
-
DEFAULT_LOG: ta.ClassVar[logging.Logger] = log
|
1588
|
-
|
1589
|
-
def __init__(
|
1590
|
-
self,
|
1591
|
-
description: str,
|
1592
|
-
*,
|
1593
|
-
log: ta.Optional[logging.Logger] = None, # noqa
|
1594
|
-
level: int = logging.DEBUG,
|
1595
|
-
) -> None:
|
1596
|
-
super().__init__()
|
1597
|
-
|
1598
|
-
self._description = description
|
1599
|
-
self._log = log if log is not None else self.DEFAULT_LOG
|
1600
|
-
self._level = level
|
1601
|
-
|
1602
|
-
def set_description(self, description: str) -> 'LogTimingContext':
|
1603
|
-
self._description = description
|
1604
|
-
return self
|
1605
|
-
|
1606
|
-
_begin_time: float
|
1607
|
-
_end_time: float
|
1608
|
-
|
1609
|
-
def __enter__(self) -> 'LogTimingContext':
|
1610
|
-
self._begin_time = time.time()
|
1611
|
-
|
1612
|
-
self._log.log(self._level, f'Begin : {self._description}') # noqa
|
1613
|
-
|
1614
|
-
return self
|
1615
|
-
|
1616
|
-
def __exit__(self, exc_type, exc_val, exc_tb):
|
1617
|
-
self._end_time = time.time()
|
1618
|
-
|
1619
|
-
self._log.log(
|
1620
|
-
self._level,
|
1621
|
-
f'End : {self._description} - {self._end_time - self._begin_time:0.2f} s elapsed',
|
1622
|
-
)
|
1623
|
-
|
1624
|
-
|
1625
|
-
log_timing_context = LogTimingContext
|
1626
|
-
|
1627
|
-
|
1628
|
-
########################################
|
1629
|
-
# ../../../omlish/argparse/cli.py
|
1630
|
-
"""
|
1631
|
-
TODO:
|
1632
|
-
- default command
|
1633
|
-
- auto match all underscores to hyphens
|
1634
|
-
- pre-run, post-run hooks
|
1635
|
-
- exitstack?
|
1636
|
-
"""
|
1637
|
-
|
1638
|
-
|
1639
|
-
##
|
1640
|
-
|
1641
|
-
|
1642
|
-
@dc.dataclass(eq=False)
|
1643
|
-
class ArgparseArg:
|
1644
|
-
args: ta.Sequence[ta.Any]
|
1645
|
-
kwargs: ta.Mapping[str, ta.Any]
|
1646
|
-
dest: ta.Optional[str] = None
|
1647
|
-
|
1648
|
-
def __get__(self, instance, owner=None):
|
1649
|
-
if instance is None:
|
1650
|
-
return self
|
1651
|
-
return getattr(instance.args, self.dest) # type: ignore
|
1652
|
-
|
1653
|
-
|
1654
|
-
def argparse_arg(*args, **kwargs) -> ArgparseArg:
|
1655
|
-
return ArgparseArg(args, kwargs)
|
1554
|
+
def argparse_arg(*args, **kwargs) -> ArgparseArg:
|
1555
|
+
return ArgparseArg(args, kwargs)
|
1656
1556
|
|
1657
1557
|
|
1658
1558
|
#
|
@@ -3115,6 +3015,15 @@ def check_lite_runtime_version() -> None:
|
|
3115
3015
|
raise OSError(f'Requires python {LITE_REQUIRED_PYTHON_VERSION}, got {sys.version_info} from {sys.executable}') # noqa
|
3116
3016
|
|
3117
3017
|
|
3018
|
+
########################################
|
3019
|
+
# ../../../omlish/lite/timing.py
|
3020
|
+
|
3021
|
+
|
3022
|
+
LogTimingContext.DEFAULT_LOG = log
|
3023
|
+
|
3024
|
+
log_timing_context = log_timing_context # noqa
|
3025
|
+
|
3026
|
+
|
3118
3027
|
########################################
|
3119
3028
|
# ../../../omlish/logs/json.py
|
3120
3029
|
"""
|
@@ -3215,155 +3124,466 @@ def temp_named_file_context(
|
|
3215
3124
|
|
3216
3125
|
|
3217
3126
|
########################################
|
3218
|
-
#
|
3219
|
-
"""
|
3220
|
-
TODO:
|
3221
|
-
- some less stupid Dockerfile hash
|
3222
|
-
- doesn't change too much though
|
3223
|
-
"""
|
3127
|
+
# ../../../omlish/subprocesses/run.py
|
3224
3128
|
|
3225
3129
|
|
3226
3130
|
##
|
3227
3131
|
|
3228
3132
|
|
3229
|
-
|
3230
|
-
|
3231
|
-
|
3133
|
+
@dc.dataclass(frozen=True)
|
3134
|
+
class SubprocessRunOutput(ta.Generic[T]):
|
3135
|
+
proc: T
|
3232
3136
|
|
3233
|
-
|
3137
|
+
returncode: int # noqa
|
3138
|
+
|
3139
|
+
stdout: ta.Optional[bytes] = None
|
3140
|
+
stderr: ta.Optional[bytes] = None
|
3234
3141
|
|
3235
3142
|
|
3236
3143
|
##
|
3237
3144
|
|
3238
3145
|
|
3239
|
-
|
3240
|
-
|
3241
|
-
|
3242
|
-
|
3146
|
+
@dc.dataclass(frozen=True)
|
3147
|
+
class SubprocessRun:
|
3148
|
+
cmd: ta.Sequence[str]
|
3149
|
+
input: ta.Any = None
|
3150
|
+
timeout: ta.Optional[float] = None
|
3151
|
+
check: bool = False
|
3152
|
+
capture_output: ta.Optional[bool] = None
|
3153
|
+
kwargs: ta.Optional[ta.Mapping[str, ta.Any]] = None
|
3243
3154
|
|
3244
|
-
|
3245
|
-
|
3246
|
-
|
3247
|
-
|
3155
|
+
@classmethod
|
3156
|
+
def of(
|
3157
|
+
cls,
|
3158
|
+
*cmd: str,
|
3159
|
+
input: ta.Any = None, # noqa
|
3160
|
+
timeout: ta.Optional[float] = None,
|
3161
|
+
check: bool = False, # noqa
|
3162
|
+
capture_output: ta.Optional[bool] = None,
|
3163
|
+
**kwargs: ta.Any,
|
3164
|
+
) -> 'SubprocessRun':
|
3165
|
+
return cls(
|
3166
|
+
cmd=cmd,
|
3167
|
+
input=input,
|
3168
|
+
timeout=timeout,
|
3169
|
+
check=check,
|
3170
|
+
capture_output=capture_output,
|
3171
|
+
kwargs=kwargs,
|
3172
|
+
)
|
3248
3173
|
|
3174
|
+
#
|
3249
3175
|
|
3250
|
-
|
3251
|
-
with tarfile.open(tar_file) as tf:
|
3252
|
-
with contextlib.closing(check.not_none(tf.extractfile('index.json'))) as mf:
|
3253
|
-
i = mf.read()
|
3176
|
+
_DEFAULT_SUBPROCESSES: ta.ClassVar[ta.Optional[ta.Any]] = None # AbstractSubprocesses
|
3254
3177
|
|
3255
|
-
|
3256
|
-
|
3257
|
-
|
3258
|
-
|
3178
|
+
def run(
|
3179
|
+
self,
|
3180
|
+
subprocesses: ta.Optional[ta.Any] = None, # AbstractSubprocesses
|
3181
|
+
) -> SubprocessRunOutput:
|
3182
|
+
if subprocesses is None:
|
3183
|
+
subprocesses = self._DEFAULT_SUBPROCESSES
|
3184
|
+
return check.not_none(subprocesses).run_(self) # type: ignore[attr-defined]
|
3259
3185
|
|
3186
|
+
_DEFAULT_ASYNC_SUBPROCESSES: ta.ClassVar[ta.Optional[ta.Any]] = None # AbstractAsyncSubprocesses
|
3260
3187
|
|
3261
|
-
|
3262
|
-
|
3188
|
+
async def async_run(
|
3189
|
+
self,
|
3190
|
+
async_subprocesses: ta.Optional[ta.Any] = None, # AbstractAsyncSubprocesses
|
3191
|
+
) -> SubprocessRunOutput:
|
3192
|
+
if async_subprocesses is None:
|
3193
|
+
async_subprocesses = self._DEFAULT_ASYNC_SUBPROCESSES
|
3194
|
+
return await check.not_none(async_subprocesses).run_(self) # type: ignore[attr-defined]
|
3263
3195
|
|
3264
3196
|
|
3265
3197
|
##
|
3266
3198
|
|
3267
3199
|
|
3268
|
-
class
|
3269
|
-
class Entry(abc.ABC): # noqa
|
3270
|
-
pass
|
3271
|
-
|
3200
|
+
class SubprocessRunnable(abc.ABC, ta.Generic[T]):
|
3272
3201
|
@abc.abstractmethod
|
3273
|
-
def
|
3202
|
+
def make_run(self) -> SubprocessRun:
|
3274
3203
|
raise NotImplementedError
|
3275
3204
|
|
3276
3205
|
@abc.abstractmethod
|
3277
|
-
def
|
3206
|
+
def handle_run_output(self, output: SubprocessRunOutput) -> T:
|
3278
3207
|
raise NotImplementedError
|
3279
3208
|
|
3280
|
-
|
3281
|
-
def upload_file(self, key: str, in_file: str) -> ta.Awaitable[None]:
|
3282
|
-
raise NotImplementedError
|
3209
|
+
#
|
3283
3210
|
|
3211
|
+
def run(self, subprocesses: ta.Optional[ta.Any] = None) -> T: # AbstractSubprocesses
|
3212
|
+
return self.handle_run_output(self.make_run().run(subprocesses))
|
3284
3213
|
|
3285
|
-
|
3214
|
+
async def async_run(self, async_subprocesses: ta.Optional[ta.Any] = None) -> T: # AbstractAsyncSubprocesses
|
3215
|
+
return self.handle_run_output(await self.make_run().async_run(async_subprocesses))
|
3286
3216
|
|
3287
3217
|
|
3288
|
-
|
3289
|
-
|
3290
|
-
AUTH_TOKEN_ENV_VAR = register_github_env_var('ACTIONS_RUNTIME_TOKEN') # noqa
|
3218
|
+
########################################
|
3219
|
+
# ../cache.py
|
3291
3220
|
|
3292
|
-
KEY_SUFFIX_ENV_VAR = register_github_env_var('GITHUB_RUN_ID')
|
3293
3221
|
|
3294
|
-
|
3222
|
+
CacheVersion = ta.NewType('CacheVersion', int)
|
3295
3223
|
|
3296
|
-
def __init__(
|
3297
|
-
self,
|
3298
|
-
*,
|
3299
|
-
base_url: ta.Optional[str] = None,
|
3300
|
-
auth_token: ta.Optional[str] = None,
|
3301
3224
|
|
3302
|
-
|
3303
|
-
key_suffix: ta.Optional[str] = None,
|
3225
|
+
##
|
3304
3226
|
|
3305
|
-
cache_version: int = CI_CACHE_VERSION,
|
3306
3227
|
|
3307
|
-
|
3228
|
+
class FileCache(abc.ABC):
|
3229
|
+
DEFAULT_CACHE_VERSION: ta.ClassVar[CacheVersion] = CacheVersion(CI_CACHE_VERSION)
|
3230
|
+
|
3231
|
+
def __init__(
|
3232
|
+
self,
|
3233
|
+
*,
|
3234
|
+
version: ta.Optional[CacheVersion] = None,
|
3308
3235
|
) -> None:
|
3309
3236
|
super().__init__()
|
3310
3237
|
|
3311
|
-
|
3312
|
-
|
3313
|
-
|
3314
|
-
|
3315
|
-
self.
|
3316
|
-
|
3317
|
-
if auth_token is None:
|
3318
|
-
auth_token = self.AUTH_TOKEN_ENV_VAR()
|
3319
|
-
self._auth_token = auth_token
|
3320
|
-
|
3321
|
-
#
|
3238
|
+
if version is None:
|
3239
|
+
version = self.DEFAULT_CACHE_VERSION
|
3240
|
+
check.isinstance(version, int)
|
3241
|
+
check.arg(version >= 0)
|
3242
|
+
self._version: CacheVersion = version
|
3322
3243
|
|
3323
|
-
|
3244
|
+
@property
|
3245
|
+
def version(self) -> CacheVersion:
|
3246
|
+
return self._version
|
3324
3247
|
|
3325
|
-
|
3326
|
-
key_suffix = self.KEY_SUFFIX_ENV_VAR()
|
3327
|
-
self._key_suffix = check.non_empty_str(key_suffix)
|
3248
|
+
#
|
3328
3249
|
|
3329
|
-
|
3250
|
+
@abc.abstractmethod
|
3251
|
+
def get_file(self, key: str) -> ta.Awaitable[ta.Optional[str]]:
|
3252
|
+
raise NotImplementedError
|
3330
3253
|
|
3331
|
-
|
3254
|
+
@abc.abstractmethod
|
3255
|
+
def put_file(
|
3256
|
+
self,
|
3257
|
+
key: str,
|
3258
|
+
file_path: str,
|
3259
|
+
*,
|
3260
|
+
steal: bool = False,
|
3261
|
+
) -> ta.Awaitable[str]:
|
3262
|
+
raise NotImplementedError
|
3332
3263
|
|
3333
|
-
#
|
3334
3264
|
|
3335
|
-
|
3265
|
+
#
|
3336
3266
|
|
3337
|
-
#
|
3338
3267
|
|
3339
|
-
|
3340
|
-
|
3341
|
-
|
3342
|
-
|
3268
|
+
class DirectoryFileCache(FileCache):
|
3269
|
+
@dc.dataclass(frozen=True)
|
3270
|
+
class Config:
|
3271
|
+
dir: str
|
3343
3272
|
|
3344
|
-
|
3273
|
+
no_create: bool = False
|
3274
|
+
no_purge: bool = False
|
3345
3275
|
|
3346
|
-
def
|
3276
|
+
def __init__(
|
3347
3277
|
self,
|
3348
|
-
|
3278
|
+
config: Config,
|
3349
3279
|
*,
|
3350
|
-
|
3351
|
-
|
3352
|
-
|
3353
|
-
|
3354
|
-
|
3355
|
-
'application/json',
|
3356
|
-
f'api-version={GithubCacheServiceV1.API_VERSION}',
|
3357
|
-
]),
|
3358
|
-
}
|
3280
|
+
version: ta.Optional[CacheVersion] = None,
|
3281
|
+
) -> None: # noqa
|
3282
|
+
super().__init__(
|
3283
|
+
version=version,
|
3284
|
+
)
|
3359
3285
|
|
3360
|
-
|
3361
|
-
dct['Authorization'] = f'Bearer {auth_token}'
|
3286
|
+
self._config = config
|
3362
3287
|
|
3363
|
-
|
3364
|
-
|
3365
|
-
|
3366
|
-
|
3288
|
+
@property
|
3289
|
+
def dir(self) -> str:
|
3290
|
+
return self._config.dir
|
3291
|
+
|
3292
|
+
#
|
3293
|
+
|
3294
|
+
VERSION_FILE_NAME = '.ci-cache-version'
|
3295
|
+
|
3296
|
+
@cached_nullary
|
3297
|
+
def setup_dir(self) -> None:
|
3298
|
+
version_file = os.path.join(self.dir, self.VERSION_FILE_NAME)
|
3299
|
+
|
3300
|
+
if self._config.no_create:
|
3301
|
+
check.state(os.path.isdir(self.dir))
|
3302
|
+
|
3303
|
+
elif not os.path.isdir(self.dir):
|
3304
|
+
os.makedirs(self.dir)
|
3305
|
+
with open(version_file, 'w') as f:
|
3306
|
+
f.write(str(self._version))
|
3307
|
+
return
|
3308
|
+
|
3309
|
+
# NOTE: intentionally raises FileNotFoundError to refuse to use an existing non-cache dir as a cache dir.
|
3310
|
+
with open(version_file) as f:
|
3311
|
+
dir_version = int(f.read().strip())
|
3312
|
+
|
3313
|
+
if dir_version == self._version:
|
3314
|
+
return
|
3315
|
+
|
3316
|
+
if self._config.no_purge:
|
3317
|
+
raise RuntimeError(f'{dir_version=} != {self._version=}')
|
3318
|
+
|
3319
|
+
dirs = [n for n in sorted(os.listdir(self.dir)) if os.path.isdir(os.path.join(self.dir, n))]
|
3320
|
+
if dirs:
|
3321
|
+
raise RuntimeError(
|
3322
|
+
f'Refusing to remove stale cache dir {self.dir!r} '
|
3323
|
+
f'due to present directories: {", ".join(dirs)}',
|
3324
|
+
)
|
3325
|
+
|
3326
|
+
for n in sorted(os.listdir(self.dir)):
|
3327
|
+
if n.startswith('.'):
|
3328
|
+
continue
|
3329
|
+
fp = os.path.join(self.dir, n)
|
3330
|
+
check.state(os.path.isfile(fp))
|
3331
|
+
log.debug('Purging stale cache file: %s', fp)
|
3332
|
+
os.unlink(fp)
|
3333
|
+
|
3334
|
+
os.unlink(version_file)
|
3335
|
+
|
3336
|
+
with open(version_file, 'w') as f:
|
3337
|
+
f.write(str(self._version))
|
3338
|
+
|
3339
|
+
#
|
3340
|
+
|
3341
|
+
def get_cache_file_path(
|
3342
|
+
self,
|
3343
|
+
key: str,
|
3344
|
+
) -> str:
|
3345
|
+
self.setup_dir()
|
3346
|
+
return os.path.join(self.dir, key)
|
3347
|
+
|
3348
|
+
def format_incomplete_file(self, f: str) -> str:
|
3349
|
+
return os.path.join(os.path.dirname(f), f'_{os.path.basename(f)}.incomplete')
|
3350
|
+
|
3351
|
+
#
|
3352
|
+
|
3353
|
+
async def get_file(self, key: str) -> ta.Optional[str]:
|
3354
|
+
cache_file_path = self.get_cache_file_path(key)
|
3355
|
+
if not os.path.exists(cache_file_path):
|
3356
|
+
return None
|
3357
|
+
return cache_file_path
|
3358
|
+
|
3359
|
+
async def put_file(
|
3360
|
+
self,
|
3361
|
+
key: str,
|
3362
|
+
file_path: str,
|
3363
|
+
*,
|
3364
|
+
steal: bool = False,
|
3365
|
+
) -> str:
|
3366
|
+
cache_file_path = self.get_cache_file_path(key)
|
3367
|
+
if steal:
|
3368
|
+
shutil.move(file_path, cache_file_path)
|
3369
|
+
else:
|
3370
|
+
shutil.copyfile(file_path, cache_file_path)
|
3371
|
+
return cache_file_path
|
3372
|
+
|
3373
|
+
|
3374
|
+
##
|
3375
|
+
|
3376
|
+
|
3377
|
+
class DataCache:
|
3378
|
+
@dc.dataclass(frozen=True)
|
3379
|
+
class Data(abc.ABC): # noqa
|
3380
|
+
pass
|
3381
|
+
|
3382
|
+
@dc.dataclass(frozen=True)
|
3383
|
+
class BytesData(Data):
|
3384
|
+
data: bytes
|
3385
|
+
|
3386
|
+
@dc.dataclass(frozen=True)
|
3387
|
+
class FileData(Data):
|
3388
|
+
file_path: str
|
3389
|
+
|
3390
|
+
@dc.dataclass(frozen=True)
|
3391
|
+
class UrlData(Data):
|
3392
|
+
url: str
|
3393
|
+
|
3394
|
+
#
|
3395
|
+
|
3396
|
+
@abc.abstractmethod
|
3397
|
+
def get_data(self, key: str) -> ta.Awaitable[ta.Optional[Data]]:
|
3398
|
+
raise NotImplementedError
|
3399
|
+
|
3400
|
+
@abc.abstractmethod
|
3401
|
+
def put_data(self, key: str, data: Data) -> ta.Awaitable[None]:
|
3402
|
+
raise NotImplementedError
|
3403
|
+
|
3404
|
+
|
3405
|
+
#
|
3406
|
+
|
3407
|
+
|
3408
|
+
@functools.singledispatch
|
3409
|
+
async def read_data_cache_data(data: DataCache.Data) -> bytes:
|
3410
|
+
raise TypeError(data)
|
3411
|
+
|
3412
|
+
|
3413
|
+
@read_data_cache_data.register
|
3414
|
+
async def _(data: DataCache.BytesData) -> bytes:
|
3415
|
+
return data.data
|
3416
|
+
|
3417
|
+
|
3418
|
+
@read_data_cache_data.register
|
3419
|
+
async def _(data: DataCache.FileData) -> bytes:
|
3420
|
+
with open(data.file_path, 'rb') as f: # noqa
|
3421
|
+
return f.read()
|
3422
|
+
|
3423
|
+
|
3424
|
+
@read_data_cache_data.register
|
3425
|
+
async def _(data: DataCache.UrlData) -> bytes:
|
3426
|
+
def inner() -> bytes:
|
3427
|
+
with urllib.request.urlopen(urllib.request.Request( # noqa
|
3428
|
+
data.url,
|
3429
|
+
)) as resp:
|
3430
|
+
return resp.read()
|
3431
|
+
|
3432
|
+
return await asyncio.get_running_loop().run_in_executor(None, inner)
|
3433
|
+
|
3434
|
+
|
3435
|
+
#
|
3436
|
+
|
3437
|
+
|
3438
|
+
class FileCacheDataCache(DataCache):
|
3439
|
+
def __init__(
|
3440
|
+
self,
|
3441
|
+
file_cache: FileCache,
|
3442
|
+
) -> None:
|
3443
|
+
super().__init__()
|
3444
|
+
|
3445
|
+
self._file_cache = file_cache
|
3446
|
+
|
3447
|
+
async def get_data(self, key: str) -> ta.Optional[DataCache.Data]:
|
3448
|
+
if (file_path := await self._file_cache.get_file(key)) is None:
|
3449
|
+
return None
|
3450
|
+
|
3451
|
+
return DataCache.FileData(file_path)
|
3452
|
+
|
3453
|
+
async def put_data(self, key: str, data: DataCache.Data) -> None:
|
3454
|
+
steal = False
|
3455
|
+
|
3456
|
+
if isinstance(data, DataCache.BytesData):
|
3457
|
+
file_path = make_temp_file()
|
3458
|
+
with open(file_path, 'wb') as f: # noqa
|
3459
|
+
f.write(data.data)
|
3460
|
+
steal = True
|
3461
|
+
|
3462
|
+
elif isinstance(data, DataCache.FileData):
|
3463
|
+
file_path = data.file_path
|
3464
|
+
|
3465
|
+
elif isinstance(data, DataCache.UrlData):
|
3466
|
+
raise NotImplementedError
|
3467
|
+
|
3468
|
+
else:
|
3469
|
+
raise TypeError(data)
|
3470
|
+
|
3471
|
+
await self._file_cache.put_file(
|
3472
|
+
key,
|
3473
|
+
file_path,
|
3474
|
+
steal=steal,
|
3475
|
+
)
|
3476
|
+
|
3477
|
+
|
3478
|
+
########################################
|
3479
|
+
# ../github/client.py
|
3480
|
+
|
3481
|
+
|
3482
|
+
##
|
3483
|
+
|
3484
|
+
|
3485
|
+
class GithubCacheClient(abc.ABC):
|
3486
|
+
class Entry(abc.ABC): # noqa
|
3487
|
+
pass
|
3488
|
+
|
3489
|
+
@abc.abstractmethod
|
3490
|
+
def get_entry(self, key: str) -> ta.Awaitable[ta.Optional[Entry]]:
|
3491
|
+
raise NotImplementedError
|
3492
|
+
|
3493
|
+
def get_entry_url(self, entry: Entry) -> ta.Optional[str]:
|
3494
|
+
return None
|
3495
|
+
|
3496
|
+
@abc.abstractmethod
|
3497
|
+
def download_file(self, entry: Entry, out_file: str) -> ta.Awaitable[None]:
|
3498
|
+
raise NotImplementedError
|
3499
|
+
|
3500
|
+
@abc.abstractmethod
|
3501
|
+
def upload_file(self, key: str, in_file: str) -> ta.Awaitable[None]:
|
3502
|
+
raise NotImplementedError
|
3503
|
+
|
3504
|
+
|
3505
|
+
##
|
3506
|
+
|
3507
|
+
|
3508
|
+
class GithubCacheServiceV1BaseClient(GithubCacheClient, abc.ABC):
|
3509
|
+
BASE_URL_ENV_VAR = register_github_env_var('ACTIONS_CACHE_URL')
|
3510
|
+
AUTH_TOKEN_ENV_VAR = register_github_env_var('ACTIONS_RUNTIME_TOKEN') # noqa
|
3511
|
+
|
3512
|
+
KEY_SUFFIX_ENV_VAR = register_github_env_var('GITHUB_RUN_ID')
|
3513
|
+
|
3514
|
+
#
|
3515
|
+
|
3516
|
+
def __init__(
|
3517
|
+
self,
|
3518
|
+
*,
|
3519
|
+
base_url: ta.Optional[str] = None,
|
3520
|
+
auth_token: ta.Optional[str] = None,
|
3521
|
+
|
3522
|
+
key_prefix: ta.Optional[str] = None,
|
3523
|
+
key_suffix: ta.Optional[str] = None,
|
3524
|
+
|
3525
|
+
cache_version: int = CI_CACHE_VERSION,
|
3526
|
+
|
3527
|
+
loop: ta.Optional[asyncio.AbstractEventLoop] = None,
|
3528
|
+
) -> None:
|
3529
|
+
super().__init__()
|
3530
|
+
|
3531
|
+
#
|
3532
|
+
|
3533
|
+
if base_url is None:
|
3534
|
+
base_url = check.non_empty_str(self.BASE_URL_ENV_VAR())
|
3535
|
+
self._service_url = GithubCacheServiceV1.get_service_url(base_url)
|
3536
|
+
|
3537
|
+
if auth_token is None:
|
3538
|
+
auth_token = self.AUTH_TOKEN_ENV_VAR()
|
3539
|
+
self._auth_token = auth_token
|
3540
|
+
|
3541
|
+
#
|
3542
|
+
|
3543
|
+
self._key_prefix = key_prefix
|
3544
|
+
|
3545
|
+
if key_suffix is None:
|
3546
|
+
key_suffix = self.KEY_SUFFIX_ENV_VAR()
|
3547
|
+
self._key_suffix = check.non_empty_str(key_suffix)
|
3548
|
+
|
3549
|
+
#
|
3550
|
+
|
3551
|
+
self._cache_version = check.isinstance(cache_version, int)
|
3552
|
+
|
3553
|
+
#
|
3554
|
+
|
3555
|
+
self._given_loop = loop
|
3556
|
+
|
3557
|
+
#
|
3558
|
+
|
3559
|
+
def _get_loop(self) -> asyncio.AbstractEventLoop:
|
3560
|
+
if (loop := self._given_loop) is not None:
|
3561
|
+
return loop
|
3562
|
+
return asyncio.get_running_loop()
|
3563
|
+
|
3564
|
+
#
|
3565
|
+
|
3566
|
+
def build_request_headers(
|
3567
|
+
self,
|
3568
|
+
headers: ta.Optional[ta.Mapping[str, str]] = None,
|
3569
|
+
*,
|
3570
|
+
content_type: ta.Optional[str] = None,
|
3571
|
+
json_content: bool = False,
|
3572
|
+
) -> ta.Dict[str, str]:
|
3573
|
+
dct = {
|
3574
|
+
'Accept': ';'.join([
|
3575
|
+
'application/json',
|
3576
|
+
f'api-version={GithubCacheServiceV1.API_VERSION}',
|
3577
|
+
]),
|
3578
|
+
}
|
3579
|
+
|
3580
|
+
if (auth_token := self._auth_token):
|
3581
|
+
dct['Authorization'] = f'Bearer {auth_token}'
|
3582
|
+
|
3583
|
+
if content_type is None and json_content:
|
3584
|
+
content_type = 'application/json'
|
3585
|
+
if content_type is not None:
|
3586
|
+
dct['Content-Type'] = content_type
|
3367
3587
|
|
3368
3588
|
if headers:
|
3369
3589
|
dct.update(headers)
|
@@ -3467,6 +3687,10 @@ class GithubCacheServiceV1BaseClient(GithubCacheClient, abc.ABC):
|
|
3467
3687
|
class Entry(GithubCacheClient.Entry):
|
3468
3688
|
artifact: GithubCacheServiceV1.ArtifactCacheEntry
|
3469
3689
|
|
3690
|
+
def get_entry_url(self, entry: GithubCacheClient.Entry) -> ta.Optional[str]:
|
3691
|
+
entry1 = check.isinstance(entry, self.Entry)
|
3692
|
+
return entry1.artifact.cache_key
|
3693
|
+
|
3470
3694
|
#
|
3471
3695
|
|
3472
3696
|
def build_get_entry_url_path(self, *keys: str) -> str:
|
@@ -3857,23 +4081,7 @@ def configure_standard_logging(
|
|
3857
4081
|
|
3858
4082
|
|
3859
4083
|
########################################
|
3860
|
-
# ../../../omlish/subprocesses.py
|
3861
|
-
|
3862
|
-
|
3863
|
-
##
|
3864
|
-
|
3865
|
-
|
3866
|
-
# Valid channel type kwarg values:
|
3867
|
-
# - A special flag negative int
|
3868
|
-
# - A positive fd int
|
3869
|
-
# - A file-like object
|
3870
|
-
# - None
|
3871
|
-
|
3872
|
-
SUBPROCESS_CHANNEL_OPTION_VALUES: ta.Mapping[SubprocessChannelOption, int] = {
|
3873
|
-
'pipe': subprocess.PIPE,
|
3874
|
-
'stdout': subprocess.STDOUT,
|
3875
|
-
'devnull': subprocess.DEVNULL,
|
3876
|
-
}
|
4084
|
+
# ../../../omlish/subprocesses/wrap.py
|
3877
4085
|
|
3878
4086
|
|
3879
4087
|
##
|
@@ -3893,22 +4101,143 @@ def subprocess_maybe_shell_wrap_exec(*cmd: str) -> ta.Tuple[str, ...]:
|
|
3893
4101
|
return cmd
|
3894
4102
|
|
3895
4103
|
|
4104
|
+
########################################
|
4105
|
+
# ../github/cache.py
|
4106
|
+
|
4107
|
+
|
3896
4108
|
##
|
3897
4109
|
|
3898
4110
|
|
3899
|
-
|
3900
|
-
|
3901
|
-
|
3902
|
-
|
3903
|
-
# TODO: terminate, sleep, kill
|
3904
|
-
if proc.stdout:
|
3905
|
-
proc.stdout.close()
|
3906
|
-
if proc.stderr:
|
3907
|
-
proc.stderr.close()
|
3908
|
-
if proc.stdin:
|
3909
|
-
proc.stdin.close()
|
4111
|
+
class GithubCache(FileCache, DataCache):
|
4112
|
+
@dc.dataclass(frozen=True)
|
4113
|
+
class Config:
|
4114
|
+
dir: str
|
3910
4115
|
|
3911
|
-
|
4116
|
+
def __init__(
|
4117
|
+
self,
|
4118
|
+
config: Config,
|
4119
|
+
*,
|
4120
|
+
client: ta.Optional[GithubCacheClient] = None,
|
4121
|
+
version: ta.Optional[CacheVersion] = None,
|
4122
|
+
) -> None:
|
4123
|
+
super().__init__(
|
4124
|
+
version=version,
|
4125
|
+
)
|
4126
|
+
|
4127
|
+
self._config = config
|
4128
|
+
|
4129
|
+
if client is None:
|
4130
|
+
client = GithubCacheServiceV1Client(
|
4131
|
+
cache_version=self._version,
|
4132
|
+
)
|
4133
|
+
self._client: GithubCacheClient = client
|
4134
|
+
|
4135
|
+
self._local = DirectoryFileCache(
|
4136
|
+
DirectoryFileCache.Config(
|
4137
|
+
dir=check.non_empty_str(config.dir),
|
4138
|
+
),
|
4139
|
+
version=self._version,
|
4140
|
+
)
|
4141
|
+
|
4142
|
+
#
|
4143
|
+
|
4144
|
+
async def get_file(self, key: str) -> ta.Optional[str]:
|
4145
|
+
local_file = self._local.get_cache_file_path(key)
|
4146
|
+
if os.path.exists(local_file):
|
4147
|
+
return local_file
|
4148
|
+
|
4149
|
+
if (entry := await self._client.get_entry(key)) is None:
|
4150
|
+
return None
|
4151
|
+
|
4152
|
+
tmp_file = self._local.format_incomplete_file(local_file)
|
4153
|
+
with unlinking_if_exists(tmp_file):
|
4154
|
+
await self._client.download_file(entry, tmp_file)
|
4155
|
+
|
4156
|
+
os.replace(tmp_file, local_file)
|
4157
|
+
|
4158
|
+
return local_file
|
4159
|
+
|
4160
|
+
async def put_file(
|
4161
|
+
self,
|
4162
|
+
key: str,
|
4163
|
+
file_path: str,
|
4164
|
+
*,
|
4165
|
+
steal: bool = False,
|
4166
|
+
) -> str:
|
4167
|
+
cache_file_path = await self._local.put_file(
|
4168
|
+
key,
|
4169
|
+
file_path,
|
4170
|
+
steal=steal,
|
4171
|
+
)
|
4172
|
+
|
4173
|
+
await self._client.upload_file(key, cache_file_path)
|
4174
|
+
|
4175
|
+
return cache_file_path
|
4176
|
+
|
4177
|
+
#
|
4178
|
+
|
4179
|
+
async def get_data(self, key: str) -> ta.Optional[DataCache.Data]:
|
4180
|
+
local_file = self._local.get_cache_file_path(key)
|
4181
|
+
if os.path.exists(local_file):
|
4182
|
+
return DataCache.FileData(local_file)
|
4183
|
+
|
4184
|
+
if (entry := await self._client.get_entry(key)) is None:
|
4185
|
+
return None
|
4186
|
+
|
4187
|
+
return DataCache.UrlData(check.non_empty_str(self._client.get_entry_url(entry)))
|
4188
|
+
|
4189
|
+
async def put_data(self, key: str, data: DataCache.Data) -> None:
|
4190
|
+
await FileCacheDataCache(self).put_data(key, data)
|
4191
|
+
|
4192
|
+
|
4193
|
+
########################################
|
4194
|
+
# ../github/cli.py
|
4195
|
+
"""
|
4196
|
+
See:
|
4197
|
+
- https://docs.github.com/en/rest/actions/cache?apiVersion=2022-11-28
|
4198
|
+
"""
|
4199
|
+
|
4200
|
+
|
4201
|
+
class GithubCli(ArgparseCli):
|
4202
|
+
@argparse_cmd()
|
4203
|
+
def list_referenced_env_vars(self) -> None:
|
4204
|
+
print('\n'.join(sorted(ev.k for ev in GITHUB_ENV_VARS)))
|
4205
|
+
|
4206
|
+
@argparse_cmd(
|
4207
|
+
argparse_arg('key'),
|
4208
|
+
)
|
4209
|
+
async def get_cache_entry(self) -> None:
|
4210
|
+
client = GithubCacheServiceV1Client()
|
4211
|
+
entry = await client.get_entry(self.args.key)
|
4212
|
+
if entry is None:
|
4213
|
+
return
|
4214
|
+
print(json_dumps_pretty(dc.asdict(entry))) # noqa
|
4215
|
+
|
4216
|
+
@argparse_cmd(
|
4217
|
+
argparse_arg('repository-id'),
|
4218
|
+
)
|
4219
|
+
def list_cache_entries(self) -> None:
|
4220
|
+
raise NotImplementedError
|
4221
|
+
|
4222
|
+
|
4223
|
+
########################################
|
4224
|
+
# ../../../omlish/subprocesses/base.py
|
4225
|
+
|
4226
|
+
|
4227
|
+
##
|
4228
|
+
|
4229
|
+
|
4230
|
+
# Valid channel type kwarg values:
|
4231
|
+
# - A special flag negative int
|
4232
|
+
# - A positive fd int
|
4233
|
+
# - A file-like object
|
4234
|
+
# - None
|
4235
|
+
|
4236
|
+
SUBPROCESS_CHANNEL_OPTION_VALUES: ta.Mapping[SubprocessChannelOption, int] = {
|
4237
|
+
'pipe': subprocess.PIPE,
|
4238
|
+
'stdout': subprocess.STDOUT,
|
4239
|
+
'devnull': subprocess.DEVNULL,
|
4240
|
+
}
|
3912
4241
|
|
3913
4242
|
|
3914
4243
|
##
|
@@ -4095,32 +4424,41 @@ class BaseSubprocesses(abc.ABC): # noqa
|
|
4095
4424
|
return e
|
4096
4425
|
|
4097
4426
|
|
4427
|
+
########################################
|
4428
|
+
# ../github/inject.py
|
4429
|
+
|
4430
|
+
|
4098
4431
|
##
|
4099
4432
|
|
4100
4433
|
|
4101
|
-
|
4102
|
-
|
4103
|
-
|
4104
|
-
|
4105
|
-
|
4106
|
-
|
4107
|
-
|
4108
|
-
|
4434
|
+
def bind_github(
|
4435
|
+
*,
|
4436
|
+
cache_dir: ta.Optional[str] = None,
|
4437
|
+
) -> InjectorBindings:
|
4438
|
+
lst: ta.List[InjectorBindingOrBindings] = []
|
4439
|
+
|
4440
|
+
if cache_dir is not None:
|
4441
|
+
lst.extend([
|
4442
|
+
inj.bind(GithubCache.Config(
|
4443
|
+
dir=cache_dir,
|
4444
|
+
)),
|
4445
|
+
inj.bind(GithubCache, singleton=True),
|
4446
|
+
inj.bind(FileCache, to_key=GithubCache),
|
4447
|
+
])
|
4109
4448
|
|
4449
|
+
return inj.as_bindings(*lst)
|
4110
4450
|
|
4111
|
-
@dc.dataclass(frozen=True)
|
4112
|
-
class SubprocessRunOutput(ta.Generic[T]):
|
4113
|
-
proc: T
|
4114
4451
|
|
4115
|
-
|
4452
|
+
########################################
|
4453
|
+
# ../../../omlish/subprocesses/async_.py
|
4116
4454
|
|
4117
|
-
stdout: ta.Optional[bytes] = None
|
4118
|
-
stderr: ta.Optional[bytes] = None
|
4119
4455
|
|
4456
|
+
##
|
4120
4457
|
|
4121
|
-
|
4458
|
+
|
4459
|
+
class AbstractAsyncSubprocesses(BaseSubprocesses):
|
4122
4460
|
@abc.abstractmethod
|
4123
|
-
def run_(self, run: SubprocessRun) -> SubprocessRunOutput:
|
4461
|
+
async def run_(self, run: SubprocessRun) -> SubprocessRunOutput:
|
4124
4462
|
raise NotImplementedError
|
4125
4463
|
|
4126
4464
|
def run(
|
@@ -4131,7 +4469,7 @@ class AbstractSubprocesses(BaseSubprocesses, abc.ABC):
|
|
4131
4469
|
check: bool = False,
|
4132
4470
|
capture_output: ta.Optional[bool] = None,
|
4133
4471
|
**kwargs: ta.Any,
|
4134
|
-
) -> SubprocessRunOutput:
|
4472
|
+
) -> ta.Awaitable[SubprocessRunOutput]:
|
4135
4473
|
return self.run_(SubprocessRun(
|
4136
4474
|
cmd=cmd,
|
4137
4475
|
input=input,
|
@@ -4144,7 +4482,7 @@ class AbstractSubprocesses(BaseSubprocesses, abc.ABC):
|
|
4144
4482
|
#
|
4145
4483
|
|
4146
4484
|
@abc.abstractmethod
|
4147
|
-
def check_call(
|
4485
|
+
async def check_call(
|
4148
4486
|
self,
|
4149
4487
|
*cmd: str,
|
4150
4488
|
stdout: ta.Any = sys.stderr,
|
@@ -4153,7 +4491,7 @@ class AbstractSubprocesses(BaseSubprocesses, abc.ABC):
|
|
4153
4491
|
raise NotImplementedError
|
4154
4492
|
|
4155
4493
|
@abc.abstractmethod
|
4156
|
-
def check_output(
|
4494
|
+
async def check_output(
|
4157
4495
|
self,
|
4158
4496
|
*cmd: str,
|
4159
4497
|
**kwargs: ta.Any,
|
@@ -4162,96 +4500,56 @@ class AbstractSubprocesses(BaseSubprocesses, abc.ABC):
|
|
4162
4500
|
|
4163
4501
|
#
|
4164
4502
|
|
4165
|
-
def check_output_str(
|
4503
|
+
async def check_output_str(
|
4166
4504
|
self,
|
4167
4505
|
*cmd: str,
|
4168
4506
|
**kwargs: ta.Any,
|
4169
4507
|
) -> str:
|
4170
|
-
return self.check_output(*cmd, **kwargs).decode().strip()
|
4508
|
+
return (await self.check_output(*cmd, **kwargs)).decode().strip()
|
4171
4509
|
|
4172
4510
|
#
|
4173
4511
|
|
4174
|
-
def try_call(
|
4512
|
+
async def try_call(
|
4175
4513
|
self,
|
4176
4514
|
*cmd: str,
|
4177
4515
|
**kwargs: ta.Any,
|
4178
4516
|
) -> bool:
|
4179
|
-
if isinstance(self.
|
4517
|
+
if isinstance(await self.async_try_fn(self.check_call, *cmd, **kwargs), Exception):
|
4180
4518
|
return False
|
4181
4519
|
else:
|
4182
4520
|
return True
|
4183
4521
|
|
4184
|
-
def try_output(
|
4522
|
+
async def try_output(
|
4185
4523
|
self,
|
4186
4524
|
*cmd: str,
|
4187
4525
|
**kwargs: ta.Any,
|
4188
4526
|
) -> ta.Optional[bytes]:
|
4189
|
-
if isinstance(ret := self.
|
4527
|
+
if isinstance(ret := await self.async_try_fn(self.check_output, *cmd, **kwargs), Exception):
|
4190
4528
|
return None
|
4191
4529
|
else:
|
4192
4530
|
return ret
|
4193
4531
|
|
4194
|
-
def try_output_str(
|
4532
|
+
async def try_output_str(
|
4195
4533
|
self,
|
4196
4534
|
*cmd: str,
|
4197
4535
|
**kwargs: ta.Any,
|
4198
4536
|
) -> ta.Optional[str]:
|
4199
|
-
if (ret := self.try_output(*cmd, **kwargs)) is None:
|
4537
|
+
if (ret := await self.try_output(*cmd, **kwargs)) is None:
|
4200
4538
|
return None
|
4201
4539
|
else:
|
4202
4540
|
return ret.decode().strip()
|
4203
4541
|
|
4204
4542
|
|
4205
|
-
|
4206
|
-
|
4207
|
-
|
4208
|
-
class Subprocesses(AbstractSubprocesses):
|
4209
|
-
def run_(self, run: SubprocessRun) -> SubprocessRunOutput[subprocess.CompletedProcess]:
|
4210
|
-
proc = subprocess.run(
|
4211
|
-
run.cmd,
|
4212
|
-
input=run.input,
|
4213
|
-
timeout=run.timeout,
|
4214
|
-
check=run.check,
|
4215
|
-
capture_output=run.capture_output or False,
|
4216
|
-
**(run.kwargs or {}),
|
4217
|
-
)
|
4218
|
-
|
4219
|
-
return SubprocessRunOutput(
|
4220
|
-
proc=proc,
|
4221
|
-
|
4222
|
-
returncode=proc.returncode,
|
4223
|
-
|
4224
|
-
stdout=proc.stdout, # noqa
|
4225
|
-
stderr=proc.stderr, # noqa
|
4226
|
-
)
|
4227
|
-
|
4228
|
-
def check_call(
|
4229
|
-
self,
|
4230
|
-
*cmd: str,
|
4231
|
-
stdout: ta.Any = sys.stderr,
|
4232
|
-
**kwargs: ta.Any,
|
4233
|
-
) -> None:
|
4234
|
-
with self.prepare_and_wrap(*cmd, stdout=stdout, **kwargs) as (cmd, kwargs): # noqa
|
4235
|
-
subprocess.check_call(cmd, **kwargs)
|
4236
|
-
|
4237
|
-
def check_output(
|
4238
|
-
self,
|
4239
|
-
*cmd: str,
|
4240
|
-
**kwargs: ta.Any,
|
4241
|
-
) -> bytes:
|
4242
|
-
with self.prepare_and_wrap(*cmd, **kwargs) as (cmd, kwargs): # noqa
|
4243
|
-
return subprocess.check_output(cmd, **kwargs)
|
4244
|
-
|
4245
|
-
|
4246
|
-
subprocesses = Subprocesses()
|
4543
|
+
########################################
|
4544
|
+
# ../../../omlish/subprocesses/sync.py
|
4247
4545
|
|
4248
4546
|
|
4249
4547
|
##
|
4250
4548
|
|
4251
4549
|
|
4252
|
-
class
|
4550
|
+
class AbstractSubprocesses(BaseSubprocesses, abc.ABC):
|
4253
4551
|
@abc.abstractmethod
|
4254
|
-
|
4552
|
+
def run_(self, run: SubprocessRun) -> SubprocessRunOutput:
|
4255
4553
|
raise NotImplementedError
|
4256
4554
|
|
4257
4555
|
def run(
|
@@ -4262,7 +4560,7 @@ class AbstractAsyncSubprocesses(BaseSubprocesses):
|
|
4262
4560
|
check: bool = False,
|
4263
4561
|
capture_output: ta.Optional[bool] = None,
|
4264
4562
|
**kwargs: ta.Any,
|
4265
|
-
) ->
|
4563
|
+
) -> SubprocessRunOutput:
|
4266
4564
|
return self.run_(SubprocessRun(
|
4267
4565
|
cmd=cmd,
|
4268
4566
|
input=input,
|
@@ -4275,7 +4573,7 @@ class AbstractAsyncSubprocesses(BaseSubprocesses):
|
|
4275
4573
|
#
|
4276
4574
|
|
4277
4575
|
@abc.abstractmethod
|
4278
|
-
|
4576
|
+
def check_call(
|
4279
4577
|
self,
|
4280
4578
|
*cmd: str,
|
4281
4579
|
stdout: ta.Any = sys.stderr,
|
@@ -4284,7 +4582,7 @@ class AbstractAsyncSubprocesses(BaseSubprocesses):
|
|
4284
4582
|
raise NotImplementedError
|
4285
4583
|
|
4286
4584
|
@abc.abstractmethod
|
4287
|
-
|
4585
|
+
def check_output(
|
4288
4586
|
self,
|
4289
4587
|
*cmd: str,
|
4290
4588
|
**kwargs: ta.Any,
|
@@ -4293,146 +4591,94 @@ class AbstractAsyncSubprocesses(BaseSubprocesses):
|
|
4293
4591
|
|
4294
4592
|
#
|
4295
4593
|
|
4296
|
-
|
4594
|
+
def check_output_str(
|
4297
4595
|
self,
|
4298
4596
|
*cmd: str,
|
4299
4597
|
**kwargs: ta.Any,
|
4300
4598
|
) -> str:
|
4301
|
-
return
|
4599
|
+
return self.check_output(*cmd, **kwargs).decode().strip()
|
4302
4600
|
|
4303
4601
|
#
|
4304
4602
|
|
4305
|
-
|
4603
|
+
def try_call(
|
4306
4604
|
self,
|
4307
4605
|
*cmd: str,
|
4308
4606
|
**kwargs: ta.Any,
|
4309
4607
|
) -> bool:
|
4310
|
-
if isinstance(
|
4608
|
+
if isinstance(self.try_fn(self.check_call, *cmd, **kwargs), Exception):
|
4311
4609
|
return False
|
4312
4610
|
else:
|
4313
4611
|
return True
|
4314
4612
|
|
4315
|
-
|
4613
|
+
def try_output(
|
4316
4614
|
self,
|
4317
4615
|
*cmd: str,
|
4318
4616
|
**kwargs: ta.Any,
|
4319
4617
|
) -> ta.Optional[bytes]:
|
4320
|
-
if isinstance(ret :=
|
4618
|
+
if isinstance(ret := self.try_fn(self.check_output, *cmd, **kwargs), Exception):
|
4321
4619
|
return None
|
4322
4620
|
else:
|
4323
4621
|
return ret
|
4324
4622
|
|
4325
|
-
|
4623
|
+
def try_output_str(
|
4326
4624
|
self,
|
4327
4625
|
*cmd: str,
|
4328
4626
|
**kwargs: ta.Any,
|
4329
4627
|
) -> ta.Optional[str]:
|
4330
|
-
if (ret :=
|
4628
|
+
if (ret := self.try_output(*cmd, **kwargs)) is None:
|
4331
4629
|
return None
|
4332
4630
|
else:
|
4333
4631
|
return ret.decode().strip()
|
4334
4632
|
|
4335
4633
|
|
4336
|
-
########################################
|
4337
|
-
# ../github/cache.py
|
4338
|
-
|
4339
|
-
|
4340
4634
|
##
|
4341
4635
|
|
4342
4636
|
|
4343
|
-
class
|
4344
|
-
|
4345
|
-
|
4346
|
-
|
4347
|
-
|
4348
|
-
|
4349
|
-
|
4350
|
-
|
4351
|
-
|
4352
|
-
|
4353
|
-
|
4354
|
-
) -> None:
|
4355
|
-
super().__init__(
|
4356
|
-
version=version,
|
4357
|
-
)
|
4637
|
+
class Subprocesses(AbstractSubprocesses):
|
4638
|
+
def run_(self, run: SubprocessRun) -> SubprocessRunOutput[subprocess.CompletedProcess]:
|
4639
|
+
with self.prepare_and_wrap(
|
4640
|
+
*run.cmd,
|
4641
|
+
input=run.input,
|
4642
|
+
timeout=run.timeout,
|
4643
|
+
check=run.check,
|
4644
|
+
capture_output=run.capture_output or False,
|
4645
|
+
**(run.kwargs or {}),
|
4646
|
+
) as (cmd, kwargs):
|
4647
|
+
proc = subprocess.run(cmd, **kwargs) # noqa
|
4358
4648
|
|
4359
|
-
|
4649
|
+
return SubprocessRunOutput(
|
4650
|
+
proc=proc,
|
4360
4651
|
|
4361
|
-
|
4362
|
-
client = GithubCacheServiceV1Client(
|
4363
|
-
cache_version=self._version,
|
4364
|
-
)
|
4365
|
-
self._client: GithubCacheClient = client
|
4652
|
+
returncode=proc.returncode,
|
4366
4653
|
|
4367
|
-
|
4368
|
-
|
4369
|
-
dir=check.non_empty_str(config.dir),
|
4370
|
-
),
|
4371
|
-
version=self._version,
|
4654
|
+
stdout=proc.stdout, # noqa
|
4655
|
+
stderr=proc.stderr, # noqa
|
4372
4656
|
)
|
4373
4657
|
|
4374
|
-
|
4375
|
-
local_file = self._local.get_cache_file_path(key)
|
4376
|
-
if os.path.exists(local_file):
|
4377
|
-
return local_file
|
4378
|
-
|
4379
|
-
if (entry := await self._client.get_entry(key)) is None:
|
4380
|
-
return None
|
4381
|
-
|
4382
|
-
tmp_file = self._local.format_incomplete_file(local_file)
|
4383
|
-
with unlinking_if_exists(tmp_file):
|
4384
|
-
await self._client.download_file(entry, tmp_file)
|
4385
|
-
|
4386
|
-
os.replace(tmp_file, local_file)
|
4387
|
-
|
4388
|
-
return local_file
|
4389
|
-
|
4390
|
-
async def put_file(
|
4658
|
+
def check_call(
|
4391
4659
|
self,
|
4392
|
-
|
4393
|
-
|
4394
|
-
|
4395
|
-
|
4396
|
-
|
4397
|
-
|
4398
|
-
key,
|
4399
|
-
file_path,
|
4400
|
-
steal=steal,
|
4401
|
-
)
|
4402
|
-
|
4403
|
-
await self._client.upload_file(key, cache_file_path)
|
4404
|
-
|
4405
|
-
return cache_file_path
|
4660
|
+
*cmd: str,
|
4661
|
+
stdout: ta.Any = sys.stderr,
|
4662
|
+
**kwargs: ta.Any,
|
4663
|
+
) -> None:
|
4664
|
+
with self.prepare_and_wrap(*cmd, stdout=stdout, **kwargs) as (cmd, kwargs): # noqa
|
4665
|
+
subprocess.check_call(cmd, **kwargs)
|
4406
4666
|
|
4667
|
+
def check_output(
|
4668
|
+
self,
|
4669
|
+
*cmd: str,
|
4670
|
+
**kwargs: ta.Any,
|
4671
|
+
) -> bytes:
|
4672
|
+
with self.prepare_and_wrap(*cmd, **kwargs) as (cmd, kwargs): # noqa
|
4673
|
+
return subprocess.check_output(cmd, **kwargs)
|
4407
4674
|
|
4408
|
-
########################################
|
4409
|
-
# ../github/cli.py
|
4410
|
-
"""
|
4411
|
-
See:
|
4412
|
-
- https://docs.github.com/en/rest/actions/cache?apiVersion=2022-11-28
|
4413
|
-
"""
|
4414
4675
|
|
4676
|
+
##
|
4415
4677
|
|
4416
|
-
class GithubCli(ArgparseCli):
|
4417
|
-
@argparse_cmd()
|
4418
|
-
def list_referenced_env_vars(self) -> None:
|
4419
|
-
print('\n'.join(sorted(ev.k for ev in GITHUB_ENV_VARS)))
|
4420
4678
|
|
4421
|
-
|
4422
|
-
argparse_arg('key'),
|
4423
|
-
)
|
4424
|
-
async def get_cache_entry(self) -> None:
|
4425
|
-
client = GithubCacheServiceV1Client()
|
4426
|
-
entry = await client.get_entry(self.args.key)
|
4427
|
-
if entry is None:
|
4428
|
-
return
|
4429
|
-
print(json_dumps_pretty(dc.asdict(entry))) # noqa
|
4679
|
+
subprocesses = Subprocesses()
|
4430
4680
|
|
4431
|
-
|
4432
|
-
argparse_arg('repository-id'),
|
4433
|
-
)
|
4434
|
-
def list_cache_entries(self) -> None:
|
4435
|
-
raise NotImplementedError
|
4681
|
+
SubprocessRun._DEFAULT_SUBPROCESSES = subprocesses # noqa
|
4436
4682
|
|
4437
4683
|
|
4438
4684
|
########################################
|
@@ -4648,19 +4894,19 @@ class AsyncioSubprocesses(AbstractAsyncSubprocesses):
|
|
4648
4894
|
timeout: ta.Optional[float] = None,
|
4649
4895
|
**kwargs: ta.Any,
|
4650
4896
|
) -> ta.AsyncGenerator[asyncio.subprocess.Process, None]:
|
4651
|
-
fac: ta.Any
|
4652
|
-
if shell:
|
4653
|
-
fac = functools.partial(
|
4654
|
-
asyncio.create_subprocess_shell,
|
4655
|
-
check.single(cmd),
|
4656
|
-
)
|
4657
|
-
else:
|
4658
|
-
fac = functools.partial(
|
4659
|
-
asyncio.create_subprocess_exec,
|
4660
|
-
*cmd,
|
4661
|
-
)
|
4662
|
-
|
4663
4897
|
with self.prepare_and_wrap( *cmd, shell=shell, **kwargs) as (cmd, kwargs): # noqa
|
4898
|
+
fac: ta.Any
|
4899
|
+
if shell:
|
4900
|
+
fac = functools.partial(
|
4901
|
+
asyncio.create_subprocess_shell,
|
4902
|
+
check.single(cmd),
|
4903
|
+
)
|
4904
|
+
else:
|
4905
|
+
fac = functools.partial(
|
4906
|
+
asyncio.create_subprocess_exec,
|
4907
|
+
*cmd,
|
4908
|
+
)
|
4909
|
+
|
4664
4910
|
proc: asyncio.subprocess.Process = await fac(**kwargs)
|
4665
4911
|
try:
|
4666
4912
|
yield proc
|
@@ -5004,31 +5250,6 @@ async def load_docker_tar(
|
|
5004
5250
|
return await load_docker_tar_cmd(ShellCmd(f'cat {shlex.quote(tar_file)}'))
|
5005
5251
|
|
5006
5252
|
|
5007
|
-
########################################
|
5008
|
-
# ../github/inject.py
|
5009
|
-
|
5010
|
-
|
5011
|
-
##
|
5012
|
-
|
5013
|
-
|
5014
|
-
def bind_github(
|
5015
|
-
*,
|
5016
|
-
cache_dir: ta.Optional[str] = None,
|
5017
|
-
) -> InjectorBindings:
|
5018
|
-
lst: ta.List[InjectorBindingOrBindings] = []
|
5019
|
-
|
5020
|
-
if cache_dir is not None:
|
5021
|
-
lst.extend([
|
5022
|
-
inj.bind(GithubFileCache.Config(
|
5023
|
-
dir=cache_dir,
|
5024
|
-
)),
|
5025
|
-
inj.bind(GithubFileCache, singleton=True),
|
5026
|
-
inj.bind(FileCache, to_key=GithubFileCache),
|
5027
|
-
])
|
5028
|
-
|
5029
|
-
return inj.as_bindings(*lst)
|
5030
|
-
|
5031
|
-
|
5032
5253
|
########################################
|
5033
5254
|
# ../docker/cache.py
|
5034
5255
|
|