omdev 0.0.0.dev223__py3-none-any.whl → 0.0.0.dev225__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
@@ -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