omdev 0.0.0.dev223__py3-none-any.whl → 0.0.0.dev224__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.
omdev/scripts/ci.py CHANGED
@@ -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
- # ../cache.py
1187
-
1188
-
1189
- CacheVersion = ta.NewType('CacheVersion', int)
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
- class FileCache(abc.ABC):
1196
- DEFAULT_CACHE_VERSION: ta.ClassVar[CacheVersion] = CacheVersion(CI_CACHE_VERSION)
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
- for n in sorted(os.listdir(self.dir)):
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
- with open(version_file, 'w') as f:
1304
- f.write(str(self._version))
1282
+ ##
1305
1283
 
1306
- #
1307
1284
 
1308
- def get_cache_file_path(
1309
- self,
1310
- key: str,
1311
- ) -> str:
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
- def format_incomplete_file(self, f: str) -> str:
1316
- return os.path.join(os.path.dirname(f), f'_{os.path.basename(f)}.incomplete')
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
- async def get_file(self, key: str) -> ta.Optional[str]:
1321
- cache_file_path = self.get_cache_file_path(key)
1322
- if not os.path.exists(cache_file_path):
1323
- return None
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
- async def put_file(
1327
- self,
1328
- key: str,
1329
- file_path: str,
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
- # ../utils.py
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
- def read_yaml_file(yaml_file: str) -> ta.Any:
1570
- yaml = __import__('yaml')
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
- with open(yaml_file) as f:
1573
- return yaml.safe_load(f)
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,47 +3124,263 @@ def temp_named_file_context(
3215
3124
 
3216
3125
 
3217
3126
  ########################################
3218
- # ../docker/utils.py
3219
- """
3220
- TODO:
3221
- - some less stupid Dockerfile hash
3222
- - doesn't change too much though
3223
- """
3127
+ # ../cache.py
3128
+
3129
+
3130
+ CacheVersion = ta.NewType('CacheVersion', int)
3224
3131
 
3225
3132
 
3226
3133
  ##
3227
3134
 
3228
3135
 
3229
- def build_docker_file_hash(docker_file: str) -> str:
3230
- with open(docker_file) as f:
3231
- contents = f.read()
3136
+ class FileCache(abc.ABC):
3137
+ DEFAULT_CACHE_VERSION: ta.ClassVar[CacheVersion] = CacheVersion(CI_CACHE_VERSION)
3232
3138
 
3233
- return sha256_str(contents)
3139
+ def __init__(
3140
+ self,
3141
+ *,
3142
+ version: ta.Optional[CacheVersion] = None,
3143
+ ) -> None:
3144
+ super().__init__()
3145
+
3146
+ if version is None:
3147
+ version = self.DEFAULT_CACHE_VERSION
3148
+ check.isinstance(version, int)
3149
+ check.arg(version >= 0)
3150
+ self._version: CacheVersion = version
3151
+
3152
+ @property
3153
+ def version(self) -> CacheVersion:
3154
+ return self._version
3155
+
3156
+ #
3157
+
3158
+ @abc.abstractmethod
3159
+ def get_file(self, key: str) -> ta.Awaitable[ta.Optional[str]]:
3160
+ raise NotImplementedError
3161
+
3162
+ @abc.abstractmethod
3163
+ def put_file(
3164
+ self,
3165
+ key: str,
3166
+ file_path: str,
3167
+ *,
3168
+ steal: bool = False,
3169
+ ) -> ta.Awaitable[str]:
3170
+ raise NotImplementedError
3171
+
3172
+
3173
+ #
3174
+
3175
+
3176
+ class DirectoryFileCache(FileCache):
3177
+ @dc.dataclass(frozen=True)
3178
+ class Config:
3179
+ dir: str
3180
+
3181
+ no_create: bool = False
3182
+ no_purge: bool = False
3183
+
3184
+ def __init__(
3185
+ self,
3186
+ config: Config,
3187
+ *,
3188
+ version: ta.Optional[CacheVersion] = None,
3189
+ ) -> None: # noqa
3190
+ super().__init__(
3191
+ version=version,
3192
+ )
3193
+
3194
+ self._config = config
3195
+
3196
+ @property
3197
+ def dir(self) -> str:
3198
+ return self._config.dir
3199
+
3200
+ #
3201
+
3202
+ VERSION_FILE_NAME = '.ci-cache-version'
3203
+
3204
+ @cached_nullary
3205
+ def setup_dir(self) -> None:
3206
+ version_file = os.path.join(self.dir, self.VERSION_FILE_NAME)
3207
+
3208
+ if self._config.no_create:
3209
+ check.state(os.path.isdir(self.dir))
3210
+
3211
+ elif not os.path.isdir(self.dir):
3212
+ os.makedirs(self.dir)
3213
+ with open(version_file, 'w') as f:
3214
+ f.write(str(self._version))
3215
+ return
3216
+
3217
+ # NOTE: intentionally raises FileNotFoundError to refuse to use an existing non-cache dir as a cache dir.
3218
+ with open(version_file) as f:
3219
+ dir_version = int(f.read().strip())
3220
+
3221
+ if dir_version == self._version:
3222
+ return
3223
+
3224
+ if self._config.no_purge:
3225
+ raise RuntimeError(f'{dir_version=} != {self._version=}')
3226
+
3227
+ dirs = [n for n in sorted(os.listdir(self.dir)) if os.path.isdir(os.path.join(self.dir, n))]
3228
+ if dirs:
3229
+ raise RuntimeError(
3230
+ f'Refusing to remove stale cache dir {self.dir!r} '
3231
+ f'due to present directories: {", ".join(dirs)}',
3232
+ )
3233
+
3234
+ for n in sorted(os.listdir(self.dir)):
3235
+ if n.startswith('.'):
3236
+ continue
3237
+ fp = os.path.join(self.dir, n)
3238
+ check.state(os.path.isfile(fp))
3239
+ log.debug('Purging stale cache file: %s', fp)
3240
+ os.unlink(fp)
3241
+
3242
+ os.unlink(version_file)
3243
+
3244
+ with open(version_file, 'w') as f:
3245
+ f.write(str(self._version))
3246
+
3247
+ #
3248
+
3249
+ def get_cache_file_path(
3250
+ self,
3251
+ key: str,
3252
+ ) -> str:
3253
+ self.setup_dir()
3254
+ return os.path.join(self.dir, key)
3255
+
3256
+ def format_incomplete_file(self, f: str) -> str:
3257
+ return os.path.join(os.path.dirname(f), f'_{os.path.basename(f)}.incomplete')
3258
+
3259
+ #
3260
+
3261
+ async def get_file(self, key: str) -> ta.Optional[str]:
3262
+ cache_file_path = self.get_cache_file_path(key)
3263
+ if not os.path.exists(cache_file_path):
3264
+ return None
3265
+ return cache_file_path
3266
+
3267
+ async def put_file(
3268
+ self,
3269
+ key: str,
3270
+ file_path: str,
3271
+ *,
3272
+ steal: bool = False,
3273
+ ) -> str:
3274
+ cache_file_path = self.get_cache_file_path(key)
3275
+ if steal:
3276
+ shutil.move(file_path, cache_file_path)
3277
+ else:
3278
+ shutil.copyfile(file_path, cache_file_path)
3279
+ return cache_file_path
3234
3280
 
3235
3281
 
3236
3282
  ##
3237
3283
 
3238
3284
 
3239
- def read_docker_tar_image_tag(tar_file: str) -> str:
3240
- with tarfile.open(tar_file) as tf:
3241
- with contextlib.closing(check.not_none(tf.extractfile('manifest.json'))) as mf:
3242
- m = mf.read()
3285
+ class DataCache:
3286
+ @dc.dataclass(frozen=True)
3287
+ class Data(abc.ABC): # noqa
3288
+ pass
3243
3289
 
3244
- manifests = json.loads(m.decode('utf-8'))
3245
- manifest = check.single(manifests)
3246
- tag = check.non_empty_str(check.single(manifest['RepoTags']))
3247
- return tag
3290
+ @dc.dataclass(frozen=True)
3291
+ class BytesData(Data):
3292
+ data: bytes
3248
3293
 
3294
+ @dc.dataclass(frozen=True)
3295
+ class FileData(Data):
3296
+ file_path: str
3249
3297
 
3250
- def read_docker_tar_image_id(tar_file: str) -> str:
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()
3298
+ @dc.dataclass(frozen=True)
3299
+ class UrlData(Data):
3300
+ url: str
3254
3301
 
3255
- index = json.loads(i.decode('utf-8'))
3256
- manifest = check.single(index['manifests'])
3257
- image_id = check.non_empty_str(manifest['digest'])
3258
- return image_id
3302
+ #
3303
+
3304
+ @abc.abstractmethod
3305
+ def get_data(self, key: str) -> ta.Awaitable[ta.Optional[Data]]:
3306
+ raise NotImplementedError
3307
+
3308
+ @abc.abstractmethod
3309
+ def put_data(self, key: str, data: Data) -> ta.Awaitable[None]:
3310
+ raise NotImplementedError
3311
+
3312
+
3313
+ #
3314
+
3315
+
3316
+ @functools.singledispatch
3317
+ async def read_data_cache_data(data: DataCache.Data) -> bytes:
3318
+ raise TypeError(data)
3319
+
3320
+
3321
+ @read_data_cache_data.register
3322
+ async def _(data: DataCache.BytesData) -> bytes:
3323
+ return data.data
3324
+
3325
+
3326
+ @read_data_cache_data.register
3327
+ async def _(data: DataCache.FileData) -> bytes:
3328
+ with open(data.file_path, 'rb') as f: # noqa
3329
+ return f.read()
3330
+
3331
+
3332
+ @read_data_cache_data.register
3333
+ async def _(data: DataCache.UrlData) -> bytes:
3334
+ def inner() -> bytes:
3335
+ with urllib.request.urlopen(urllib.request.Request( # noqa
3336
+ data.url,
3337
+ )) as resp:
3338
+ return resp.read()
3339
+
3340
+ return await asyncio.get_running_loop().run_in_executor(None, inner)
3341
+
3342
+
3343
+ #
3344
+
3345
+
3346
+ class FileCacheDataCache(DataCache):
3347
+ def __init__(
3348
+ self,
3349
+ file_cache: FileCache,
3350
+ ) -> None:
3351
+ super().__init__()
3352
+
3353
+ self._file_cache = file_cache
3354
+
3355
+ async def get_data(self, key: str) -> ta.Optional[DataCache.Data]:
3356
+ if (file_path := await self._file_cache.get_file(key)) is None:
3357
+ return None
3358
+
3359
+ return DataCache.FileData(file_path)
3360
+
3361
+ async def put_data(self, key: str, data: DataCache.Data) -> None:
3362
+ steal = False
3363
+
3364
+ if isinstance(data, DataCache.BytesData):
3365
+ file_path = make_temp_file()
3366
+ with open(file_path, 'wb') as f: # noqa
3367
+ f.write(data.data)
3368
+ steal = True
3369
+
3370
+ elif isinstance(data, DataCache.FileData):
3371
+ file_path = data.file_path
3372
+
3373
+ elif isinstance(data, DataCache.UrlData):
3374
+ raise NotImplementedError
3375
+
3376
+ else:
3377
+ raise TypeError(data)
3378
+
3379
+ await self._file_cache.put_file(
3380
+ key,
3381
+ file_path,
3382
+ steal=steal,
3383
+ )
3259
3384
 
3260
3385
 
3261
3386
  ########################################
@@ -3273,6 +3398,9 @@ class GithubCacheClient(abc.ABC):
3273
3398
  def get_entry(self, key: str) -> ta.Awaitable[ta.Optional[Entry]]:
3274
3399
  raise NotImplementedError
3275
3400
 
3401
+ def get_entry_url(self, entry: Entry) -> ta.Optional[str]:
3402
+ return None
3403
+
3276
3404
  @abc.abstractmethod
3277
3405
  def download_file(self, entry: Entry, out_file: str) -> ta.Awaitable[None]:
3278
3406
  raise NotImplementedError
@@ -3339,7 +3467,7 @@ class GithubCacheServiceV1BaseClient(GithubCacheClient, abc.ABC):
3339
3467
  def _get_loop(self) -> asyncio.AbstractEventLoop:
3340
3468
  if (loop := self._given_loop) is not None:
3341
3469
  return loop
3342
- return asyncio.get_event_loop()
3470
+ return asyncio.get_running_loop()
3343
3471
 
3344
3472
  #
3345
3473
 
@@ -3467,6 +3595,10 @@ class GithubCacheServiceV1BaseClient(GithubCacheClient, abc.ABC):
3467
3595
  class Entry(GithubCacheClient.Entry):
3468
3596
  artifact: GithubCacheServiceV1.ArtifactCacheEntry
3469
3597
 
3598
+ def get_entry_url(self, entry: GithubCacheClient.Entry) -> ta.Optional[str]:
3599
+ entry1 = check.isinstance(entry, self.Entry)
3600
+ return entry1.artifact.cache_key
3601
+
3470
3602
  #
3471
3603
 
3472
3604
  def build_get_entry_url_path(self, *keys: str) -> str:
@@ -4107,6 +4239,25 @@ class SubprocessRun:
4107
4239
  capture_output: ta.Optional[bool] = None
4108
4240
  kwargs: ta.Optional[ta.Mapping[str, ta.Any]] = None
4109
4241
 
4242
+ @classmethod
4243
+ def of(
4244
+ cls,
4245
+ *cmd: str,
4246
+ input: ta.Any = None, # noqa
4247
+ timeout: ta.Optional[float] = None,
4248
+ check: bool = False,
4249
+ capture_output: ta.Optional[bool] = None,
4250
+ **kwargs: ta.Any,
4251
+ ) -> 'SubprocessRun':
4252
+ return cls(
4253
+ cmd=cmd,
4254
+ input=input,
4255
+ timeout=timeout,
4256
+ check=check,
4257
+ capture_output=capture_output,
4258
+ kwargs=kwargs,
4259
+ )
4260
+
4110
4261
 
4111
4262
  @dc.dataclass(frozen=True)
4112
4263
  class SubprocessRunOutput(ta.Generic[T]):
@@ -4340,7 +4491,7 @@ class AbstractAsyncSubprocesses(BaseSubprocesses):
4340
4491
  ##
4341
4492
 
4342
4493
 
4343
- class GithubFileCache(FileCache):
4494
+ class GithubCache(FileCache, DataCache):
4344
4495
  @dc.dataclass(frozen=True)
4345
4496
  class Config:
4346
4497
  dir: str
@@ -4371,6 +4522,8 @@ class GithubFileCache(FileCache):
4371
4522
  version=self._version,
4372
4523
  )
4373
4524
 
4525
+ #
4526
+
4374
4527
  async def get_file(self, key: str) -> ta.Optional[str]:
4375
4528
  local_file = self._local.get_cache_file_path(key)
4376
4529
  if os.path.exists(local_file):
@@ -4404,6 +4557,21 @@ class GithubFileCache(FileCache):
4404
4557
 
4405
4558
  return cache_file_path
4406
4559
 
4560
+ #
4561
+
4562
+ async def get_data(self, key: str) -> ta.Optional[DataCache.Data]:
4563
+ local_file = self._local.get_cache_file_path(key)
4564
+ if os.path.exists(local_file):
4565
+ return DataCache.FileData(local_file)
4566
+
4567
+ if (entry := await self._client.get_entry(key)) is None:
4568
+ return None
4569
+
4570
+ return DataCache.UrlData(check.non_empty_str(self._client.get_entry_url(entry)))
4571
+
4572
+ async def put_data(self, key: str, data: DataCache.Data) -> None:
4573
+ await FileCacheDataCache(self).put_data(key, data)
4574
+
4407
4575
 
4408
4576
  ########################################
4409
4577
  # ../github/cli.py
@@ -5019,11 +5187,11 @@ def bind_github(
5019
5187
 
5020
5188
  if cache_dir is not None:
5021
5189
  lst.extend([
5022
- inj.bind(GithubFileCache.Config(
5190
+ inj.bind(GithubCache.Config(
5023
5191
  dir=cache_dir,
5024
5192
  )),
5025
- inj.bind(GithubFileCache, singleton=True),
5026
- inj.bind(FileCache, to_key=GithubFileCache),
5193
+ inj.bind(GithubCache, singleton=True),
5194
+ inj.bind(FileCache, to_key=GithubCache),
5027
5195
  ])
5028
5196
 
5029
5197
  return inj.as_bindings(*lst)