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/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
- # ../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,155 +3124,466 @@ 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
+ # ../../../omlish/subprocesses/run.py
3224
3128
 
3225
3129
 
3226
3130
  ##
3227
3131
 
3228
3132
 
3229
- def build_docker_file_hash(docker_file: str) -> str:
3230
- with open(docker_file) as f:
3231
- contents = f.read()
3133
+ @dc.dataclass(frozen=True)
3134
+ class SubprocessRunOutput(ta.Generic[T]):
3135
+ proc: T
3232
3136
 
3233
- return sha256_str(contents)
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
- 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()
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
- 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
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
- 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()
3176
+ _DEFAULT_SUBPROCESSES: ta.ClassVar[ta.Optional[ta.Any]] = None # AbstractSubprocesses
3254
3177
 
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
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
- # ../github/client.py
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 GithubCacheClient(abc.ABC):
3269
- class Entry(abc.ABC): # noqa
3270
- pass
3271
-
3200
+ class SubprocessRunnable(abc.ABC, ta.Generic[T]):
3272
3201
  @abc.abstractmethod
3273
- def get_entry(self, key: str) -> ta.Awaitable[ta.Optional[Entry]]:
3202
+ def make_run(self) -> SubprocessRun:
3274
3203
  raise NotImplementedError
3275
3204
 
3276
3205
  @abc.abstractmethod
3277
- def download_file(self, entry: Entry, out_file: str) -> ta.Awaitable[None]:
3206
+ def handle_run_output(self, output: SubprocessRunOutput) -> T:
3278
3207
  raise NotImplementedError
3279
3208
 
3280
- @abc.abstractmethod
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
- class GithubCacheServiceV1BaseClient(GithubCacheClient, abc.ABC):
3289
- BASE_URL_ENV_VAR = register_github_env_var('ACTIONS_CACHE_URL')
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
- key_prefix: ta.Optional[str] = None,
3303
- key_suffix: ta.Optional[str] = None,
3225
+ ##
3304
3226
 
3305
- cache_version: int = CI_CACHE_VERSION,
3306
3227
 
3307
- loop: ta.Optional[asyncio.AbstractEventLoop] = None,
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
- if base_url is None:
3314
- base_url = check.non_empty_str(self.BASE_URL_ENV_VAR())
3315
- self._service_url = GithubCacheServiceV1.get_service_url(base_url)
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
- self._key_prefix = key_prefix
3244
+ @property
3245
+ def version(self) -> CacheVersion:
3246
+ return self._version
3324
3247
 
3325
- if key_suffix is None:
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
- self._cache_version = check.isinstance(cache_version, int)
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
- self._given_loop = loop
3265
+ #
3336
3266
 
3337
- #
3338
3267
 
3339
- def _get_loop(self) -> asyncio.AbstractEventLoop:
3340
- if (loop := self._given_loop) is not None:
3341
- return loop
3342
- return asyncio.get_event_loop()
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 build_request_headers(
3276
+ def __init__(
3347
3277
  self,
3348
- headers: ta.Optional[ta.Mapping[str, str]] = None,
3278
+ config: Config,
3349
3279
  *,
3350
- content_type: ta.Optional[str] = None,
3351
- json_content: bool = False,
3352
- ) -> ta.Dict[str, str]:
3353
- dct = {
3354
- 'Accept': ';'.join([
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
- if (auth_token := self._auth_token):
3361
- dct['Authorization'] = f'Bearer {auth_token}'
3286
+ self._config = config
3362
3287
 
3363
- if content_type is None and json_content:
3364
- content_type = 'application/json'
3365
- if content_type is not None:
3366
- dct['Content-Type'] = content_type
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
- def subprocess_close(
3900
- proc: subprocess.Popen,
3901
- timeout: ta.Optional[float] = None,
3902
- ) -> None:
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
- proc.wait(timeout)
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
- @dc.dataclass(frozen=True)
4102
- class SubprocessRun:
4103
- cmd: ta.Sequence[str]
4104
- input: ta.Any = None
4105
- timeout: ta.Optional[float] = None
4106
- check: bool = False
4107
- capture_output: ta.Optional[bool] = None
4108
- kwargs: ta.Optional[ta.Mapping[str, ta.Any]] = None
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
- returncode: int # noqa
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
- class AbstractSubprocesses(BaseSubprocesses, abc.ABC):
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.try_fn(self.check_call, *cmd, **kwargs), Exception):
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.try_fn(self.check_output, *cmd, **kwargs), Exception):
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 AbstractAsyncSubprocesses(BaseSubprocesses):
4550
+ class AbstractSubprocesses(BaseSubprocesses, abc.ABC):
4253
4551
  @abc.abstractmethod
4254
- async def run_(self, run: SubprocessRun) -> SubprocessRunOutput:
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
- ) -> ta.Awaitable[SubprocessRunOutput]:
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
- async def check_call(
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
- async def check_output(
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
- async def check_output_str(
4594
+ def check_output_str(
4297
4595
  self,
4298
4596
  *cmd: str,
4299
4597
  **kwargs: ta.Any,
4300
4598
  ) -> str:
4301
- return (await self.check_output(*cmd, **kwargs)).decode().strip()
4599
+ return self.check_output(*cmd, **kwargs).decode().strip()
4302
4600
 
4303
4601
  #
4304
4602
 
4305
- async def try_call(
4603
+ def try_call(
4306
4604
  self,
4307
4605
  *cmd: str,
4308
4606
  **kwargs: ta.Any,
4309
4607
  ) -> bool:
4310
- if isinstance(await self.async_try_fn(self.check_call, *cmd, **kwargs), Exception):
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
- async def try_output(
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 := await self.async_try_fn(self.check_output, *cmd, **kwargs), Exception):
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
- async def try_output_str(
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 := await self.try_output(*cmd, **kwargs)) is None:
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 GithubFileCache(FileCache):
4344
- @dc.dataclass(frozen=True)
4345
- class Config:
4346
- dir: str
4347
-
4348
- def __init__(
4349
- self,
4350
- config: Config,
4351
- *,
4352
- client: ta.Optional[GithubCacheClient] = None,
4353
- version: ta.Optional[CacheVersion] = None,
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
- self._config = config
4649
+ return SubprocessRunOutput(
4650
+ proc=proc,
4360
4651
 
4361
- if client is None:
4362
- client = GithubCacheServiceV1Client(
4363
- cache_version=self._version,
4364
- )
4365
- self._client: GithubCacheClient = client
4652
+ returncode=proc.returncode,
4366
4653
 
4367
- self._local = DirectoryFileCache(
4368
- DirectoryFileCache.Config(
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
- async def get_file(self, key: str) -> ta.Optional[str]:
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
- key: str,
4393
- file_path: str,
4394
- *,
4395
- steal: bool = False,
4396
- ) -> str:
4397
- cache_file_path = await self._local.put_file(
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
- @argparse_cmd(
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
- @argparse_cmd(
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