omdev 0.0.0.dev288__py3-none-any.whl → 0.0.0.dev290__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
@@ -103,6 +103,10 @@ TimeoutLike = ta.Union['Timeout', ta.Type['Timeout.Default'], ta.Iterable['Timeo
103
103
  # ../../omlish/sockets/addresses.py
104
104
  SocketAddress = ta.Any
105
105
 
106
+ # github/api/v2/api.py
107
+ GithubCacheServiceV2RequestT = ta.TypeVar('GithubCacheServiceV2RequestT')
108
+ GithubCacheServiceV2ResponseT = ta.TypeVar('GithubCacheServiceV2ResponseT')
109
+
106
110
  # ../../omlish/argparse/cli.py
107
111
  ArgparseCmdFn = ta.Callable[[], ta.Optional[int]] # ta.TypeAlias
108
112
 
@@ -418,6 +422,31 @@ class DockerPortRelay:
418
422
  ]
419
423
 
420
424
 
425
+ ########################################
426
+ # ../../../omlish/http/urllib.py
427
+
428
+
429
+ ##
430
+
431
+
432
+ class NonRaisingUrllibErrorProcessor(urllib.request.HTTPErrorProcessor):
433
+ """
434
+ https://stackoverflow.com/a/74844056
435
+
436
+ Usage:
437
+
438
+ opener = urllib.request.build_opener(NonRaisingUrllibErrorProcessor)
439
+ with opener.open(req) as resp:
440
+ ...
441
+ """
442
+
443
+ def http_response(self, request, response):
444
+ return response
445
+
446
+ def https_response(self, request, response):
447
+ return response
448
+
449
+
421
450
  ########################################
422
451
  # ../../../omlish/http/versions.py
423
452
 
@@ -1985,7 +2014,7 @@ def read_docker_tar_image_id(tar_file: str) -> str:
1985
2014
 
1986
2015
 
1987
2016
  ########################################
1988
- # ../github/api.py
2017
+ # ../github/api/v1/api.py
1989
2018
  """
1990
2019
  export FILE_SIZE=$(stat --format="%s" $FILE)
1991
2020
 
@@ -2028,6 +2057,11 @@ curl -s \
2028
2057
 
2029
2058
 
2030
2059
  class GithubCacheServiceV1:
2060
+ def __new__(cls, *args, **kwargs): # noqa
2061
+ raise TypeError
2062
+
2063
+ #
2064
+
2031
2065
  API_VERSION = '6.0-preview.1'
2032
2066
 
2033
2067
  @classmethod
@@ -2098,14 +2132,54 @@ class GithubCacheServiceV1:
2098
2132
  cache_size: ta.Optional[int]
2099
2133
 
2100
2134
 
2135
+ ########################################
2136
+ # ../github/api/v2/api.py
2137
+ """
2138
+ https://github.com/tonistiigi/go-actions-cache/blob/3e9a6642607fd6e4d5d4fdab7c91fe8bf4c36a25/cache_v2.go
2139
+
2140
+ ==
2141
+
2142
+ curl -s \
2143
+ -X POST \
2144
+ "${ACTIONS_RESULTS_URL}twirp/github.actions.results.api.v1.CacheService/CreateCacheEntry" \
2145
+ -H 'Content-Type: application/json' \
2146
+ -H "Authorization: Bearer $ACTIONS_RUNTIME_TOKEN" \
2147
+ -d '{"key": "foo", "version": "0000000000000000000000000000000000000000000000000000000000000001" }' \
2148
+ | jq .
2149
+
2150
+ curl -s \
2151
+ -X POST \
2152
+ "${ACTIONS_RESULTS_URL}twirp/github.actions.results.api.v1.CacheService/GetCacheEntryDownloadURL" \
2153
+ -H 'Content-Type: application/json' \
2154
+ -H "Authorization: Bearer $ACTIONS_RUNTIME_TOKEN" \
2155
+ -d '{"key": "foo", "restoreKeys": [], "version": "0000000000000000000000000000000000000000000000000000000000000001" }' \
2156
+ | jq .
2157
+
2158
+ """ # noqa
2159
+
2160
+
2161
+ ##
2162
+
2163
+
2101
2164
  class GithubCacheServiceV2:
2165
+ def __new__(cls, *args, **kwargs): # noqa
2166
+ raise TypeError
2167
+
2168
+ #
2169
+
2102
2170
  SERVICE_NAME = 'github.actions.results.api.v1.CacheService'
2103
2171
 
2172
+ @classmethod
2173
+ def get_service_url(cls, base_url: str) -> str:
2174
+ return f'{base_url.rstrip("/")}/twirp/{cls.SERVICE_NAME}'
2175
+
2176
+ #
2177
+
2104
2178
  @dc.dataclass(frozen=True)
2105
- class Method:
2179
+ class Method(ta.Generic[GithubCacheServiceV2RequestT, GithubCacheServiceV2ResponseT]):
2106
2180
  name: str
2107
- request: type
2108
- response: type
2181
+ request: ta.Type[GithubCacheServiceV2RequestT]
2182
+ response: ta.Type[GithubCacheServiceV2ResponseT]
2109
2183
 
2110
2184
  #
2111
2185
 
@@ -2124,6 +2198,8 @@ class GithubCacheServiceV2:
2124
2198
  repository_id: int
2125
2199
  scope: ta.Sequence['GithubCacheServiceV2.CacheScope']
2126
2200
 
2201
+ VERSION_LENGTH: int = 64
2202
+
2127
2203
  #
2128
2204
 
2129
2205
  @dc.dataclass(frozen=True)
@@ -2132,12 +2208,18 @@ class GithubCacheServiceV2:
2132
2208
  version: str
2133
2209
  metadata: ta.Optional['GithubCacheServiceV2.CacheMetadata'] = None
2134
2210
 
2211
+ def __post_init__(self) -> None:
2212
+ check.equal(len(self.version), GithubCacheServiceV2.VERSION_LENGTH)
2213
+
2135
2214
  @dc.dataclass(frozen=True)
2136
2215
  class CreateCacheEntryResponse:
2137
2216
  ok: bool
2138
2217
  signed_upload_url: str
2139
2218
 
2140
- CREATE_CACHE_ENTRY_METHOD = Method(
2219
+ CREATE_CACHE_ENTRY_METHOD: Method[
2220
+ CreateCacheEntryRequest,
2221
+ CreateCacheEntryResponse,
2222
+ ] = Method(
2141
2223
  'CreateCacheEntry',
2142
2224
  CreateCacheEntryRequest,
2143
2225
  CreateCacheEntryResponse,
@@ -2157,7 +2239,10 @@ class GithubCacheServiceV2:
2157
2239
  ok: bool
2158
2240
  entry_id: str
2159
2241
 
2160
- FINALIZE_CACHE_ENTRY_METHOD = Method(
2242
+ FINALIZE_CACHE_ENTRY_METHOD: Method[
2243
+ FinalizeCacheEntryUploadRequest,
2244
+ FinalizeCacheEntryUploadResponse,
2245
+ ] = Method(
2161
2246
  'FinalizeCacheEntryUpload',
2162
2247
  FinalizeCacheEntryUploadRequest,
2163
2248
  FinalizeCacheEntryUploadResponse,
@@ -2178,7 +2263,10 @@ class GithubCacheServiceV2:
2178
2263
  signed_download_url: str
2179
2264
  matched_key: str
2180
2265
 
2181
- GET_CACHE_ENTRY_DOWNLOAD_URL_METHOD = Method(
2266
+ GET_CACHE_ENTRY_DOWNLOAD_URL_METHOD: Method[
2267
+ GetCacheEntryDownloadUrlRequest,
2268
+ GetCacheEntryDownloadUrlResponse,
2269
+ ] = Method(
2182
2270
  'GetCacheEntryDownloadURL',
2183
2271
  GetCacheEntryDownloadUrlRequest,
2184
2272
  GetCacheEntryDownloadUrlResponse,
@@ -5978,13 +6066,14 @@ class FileCacheDataCache(DataCache):
5978
6066
 
5979
6067
 
5980
6068
  ########################################
5981
- # ../github/client.py
6069
+ # ../github/api/clients.py
5982
6070
 
5983
6071
 
5984
6072
  ##
5985
6073
 
5986
6074
 
5987
6075
  class GithubCacheClient(abc.ABC):
6076
+ @dc.dataclass(frozen=True)
5988
6077
  class Entry(abc.ABC): # noqa
5989
6078
  pass
5990
6079
 
@@ -6007,18 +6096,21 @@ class GithubCacheClient(abc.ABC):
6007
6096
  ##
6008
6097
 
6009
6098
 
6010
- class GithubCacheServiceV1BaseClient(GithubCacheClient, abc.ABC):
6011
- BASE_URL_ENV_VAR = register_github_env_var('ACTIONS_CACHE_URL')
6099
+ class BaseGithubCacheClient(GithubCacheClient, abc.ABC):
6012
6100
  AUTH_TOKEN_ENV_VAR = register_github_env_var('ACTIONS_RUNTIME_TOKEN') # noqa
6013
6101
 
6014
6102
  KEY_SUFFIX_ENV_VAR = register_github_env_var('GITHUB_RUN_ID')
6015
6103
 
6104
+ DEFAULT_CONCURRENCY = 4
6105
+ DEFAULT_CHUNK_SIZE = 64 * 1024 * 1024
6106
+
6016
6107
  #
6017
6108
 
6018
6109
  def __init__(
6019
6110
  self,
6020
6111
  *,
6021
- base_url: ta.Optional[str] = None,
6112
+ service_url: str,
6113
+
6022
6114
  auth_token: ta.Optional[str] = None,
6023
6115
 
6024
6116
  key_prefix: ta.Optional[str] = None,
@@ -6027,14 +6119,15 @@ class GithubCacheServiceV1BaseClient(GithubCacheClient, abc.ABC):
6027
6119
  cache_version: int = CI_CACHE_VERSION,
6028
6120
 
6029
6121
  loop: ta.Optional[asyncio.AbstractEventLoop] = None,
6122
+
6123
+ concurrency: int = DEFAULT_CONCURRENCY,
6124
+ chunk_size: int = DEFAULT_CHUNK_SIZE,
6030
6125
  ) -> None:
6031
6126
  super().__init__()
6032
6127
 
6033
6128
  #
6034
6129
 
6035
- if base_url is None:
6036
- base_url = check.non_empty_str(self.BASE_URL_ENV_VAR())
6037
- self._service_url = GithubCacheServiceV1.get_service_url(base_url)
6130
+ self._service_url = check.non_empty_str(service_url)
6038
6131
 
6039
6132
  if auth_token is None:
6040
6133
  auth_token = self.AUTH_TOKEN_ENV_VAR()
@@ -6056,7 +6149,16 @@ class GithubCacheServiceV1BaseClient(GithubCacheClient, abc.ABC):
6056
6149
 
6057
6150
  self._given_loop = loop
6058
6151
 
6059
- #
6152
+ #
6153
+
6154
+ check.arg(concurrency > 0)
6155
+ self._concurrency = concurrency
6156
+
6157
+ check.arg(chunk_size > 0)
6158
+ self._chunk_size = chunk_size
6159
+
6160
+ ##
6161
+ # misc
6060
6162
 
6061
6163
  def _get_loop(self) -> asyncio.AbstractEventLoop:
6062
6164
  if (loop := self._given_loop) is not None:
@@ -6065,21 +6167,25 @@ class GithubCacheServiceV1BaseClient(GithubCacheClient, abc.ABC):
6065
6167
 
6066
6168
  #
6067
6169
 
6068
- def build_request_headers(
6170
+ def _load_json_bytes(self, b: ta.Optional[bytes]) -> ta.Optional[ta.Any]:
6171
+ if not b:
6172
+ return None
6173
+ return json.loads(b.decode('utf-8-sig'))
6174
+
6175
+ ##
6176
+ # requests
6177
+
6178
+ def _build_request_headers(
6069
6179
  self,
6070
6180
  headers: ta.Optional[ta.Mapping[str, str]] = None,
6071
6181
  *,
6182
+ no_auth: bool = False,
6072
6183
  content_type: ta.Optional[str] = None,
6073
6184
  json_content: bool = False,
6074
6185
  ) -> ta.Dict[str, str]:
6075
- dct = {
6076
- 'Accept': ';'.join([
6077
- 'application/json',
6078
- f'api-version={GithubCacheServiceV1.API_VERSION}',
6079
- ]),
6080
- }
6186
+ dct = {}
6081
6187
 
6082
- if (auth_token := self._auth_token):
6188
+ if not no_auth and (auth_token := self._auth_token):
6083
6189
  dct['Authorization'] = f'Bearer {auth_token}'
6084
6190
 
6085
6191
  if content_type is None and json_content:
@@ -6094,19 +6200,13 @@ class GithubCacheServiceV1BaseClient(GithubCacheClient, abc.ABC):
6094
6200
 
6095
6201
  #
6096
6202
 
6097
- def load_json_bytes(self, b: ta.Optional[bytes]) -> ta.Optional[ta.Any]:
6098
- if not b:
6099
- return None
6100
- return json.loads(b.decode('utf-8-sig'))
6101
-
6102
- #
6103
-
6104
- async def send_url_request(
6203
+ async def _send_urllib_request(
6105
6204
  self,
6106
6205
  req: urllib.request.Request,
6107
6206
  ) -> ta.Tuple[http.client.HTTPResponse, ta.Optional[bytes]]:
6108
6207
  def run_sync():
6109
- with urllib.request.urlopen(req) as resp: # noqa
6208
+ opener = urllib.request.build_opener(NonRaisingUrllibErrorProcessor)
6209
+ with opener.open(req) as resp: # noqa
6110
6210
  body = resp.read()
6111
6211
  return (resp, body)
6112
6212
 
@@ -6122,18 +6222,32 @@ class GithubCacheServiceV1BaseClient(GithubCacheClient, abc.ABC):
6122
6222
  def __str__(self) -> str:
6123
6223
  return repr(self)
6124
6224
 
6125
- async def send_service_request(
6225
+ async def _send_request(
6126
6226
  self,
6127
- path: str,
6128
6227
  *,
6228
+ url: ta.Optional[str] = None,
6229
+ path: ta.Optional[str] = None,
6230
+
6129
6231
  method: ta.Optional[str] = None,
6232
+
6130
6233
  headers: ta.Optional[ta.Mapping[str, str]] = None,
6234
+ no_auth: bool = False,
6131
6235
  content_type: ta.Optional[str] = None,
6236
+
6132
6237
  content: ta.Optional[bytes] = None,
6133
6238
  json_content: ta.Optional[ta.Any] = None,
6239
+
6134
6240
  success_status_codes: ta.Optional[ta.Container[int]] = None,
6241
+
6242
+ retry_status_codes: ta.Optional[ta.Container[int]] = None,
6243
+ num_retries: int = 0,
6244
+ retry_sleep: ta.Optional[float] = None,
6135
6245
  ) -> ta.Optional[ta.Any]:
6136
- url = f'{self._service_url}/{path}'
6246
+ if url is not None and path is not None:
6247
+ raise RuntimeError('Must not pass both url and path')
6248
+ elif path is not None:
6249
+ url = f'{self._service_url}/{path}'
6250
+ url = check.non_empty_str(url)
6137
6251
 
6138
6252
  if content is not None and json_content is not None:
6139
6253
  raise RuntimeError('Must not pass both content and json_content')
@@ -6146,33 +6260,52 @@ class GithubCacheServiceV1BaseClient(GithubCacheClient, abc.ABC):
6146
6260
  if method is None:
6147
6261
  method = 'POST' if content is not None else 'GET'
6148
6262
 
6263
+ headers = self._build_request_headers(
6264
+ headers,
6265
+ no_auth=no_auth,
6266
+ content_type=content_type,
6267
+ json_content=header_json_content,
6268
+ )
6269
+
6149
6270
  #
6150
6271
 
6151
- req = urllib.request.Request( # noqa
6152
- url,
6153
- method=method,
6154
- headers=self.build_request_headers(
6155
- headers,
6156
- content_type=content_type,
6157
- json_content=header_json_content,
6158
- ),
6159
- data=content,
6160
- )
6272
+ for n in itertools.count():
6273
+ req = urllib.request.Request( # noqa
6274
+ url,
6275
+ method=method,
6276
+ headers=headers,
6277
+ data=content,
6278
+ )
6161
6279
 
6162
- resp, body = await self.send_url_request(req)
6280
+ resp, body = await self._send_urllib_request(req)
6163
6281
 
6164
- #
6282
+ #
6165
6283
 
6166
- if success_status_codes is not None:
6167
- is_success = resp.status in success_status_codes
6168
- else:
6169
- is_success = (200 <= resp.status <= 300)
6170
- if not is_success:
6171
- raise self.ServiceRequestError(resp.status, body)
6284
+ if success_status_codes is not None:
6285
+ is_success = resp.status in success_status_codes
6286
+ else:
6287
+ is_success = (200 <= resp.status < 300)
6288
+ if is_success:
6289
+ return self._load_json_bytes(body)
6290
+
6291
+ #
6172
6292
 
6173
- return self.load_json_bytes(body)
6293
+ log.debug(f'Request to url {url} got unsuccessful status code {resp.status}') # noqa
6174
6294
 
6175
- #
6295
+ if not (
6296
+ retry_status_codes is not None and
6297
+ resp.status in retry_status_codes and
6298
+ n < num_retries
6299
+ ):
6300
+ raise self.ServiceRequestError(resp.status, body)
6301
+
6302
+ if retry_sleep is not None:
6303
+ await asyncio.sleep(retry_sleep)
6304
+
6305
+ raise RuntimeError('Unreachable')
6306
+
6307
+ ##
6308
+ # keys
6176
6309
 
6177
6310
  KEY_PART_SEPARATOR = '---'
6178
6311
 
@@ -6183,73 +6316,8 @@ class GithubCacheServiceV1BaseClient(GithubCacheClient, abc.ABC):
6183
6316
  ('' if partial_suffix else self._key_suffix),
6184
6317
  ])
6185
6318
 
6186
- #
6187
-
6188
- @dc.dataclass(frozen=True)
6189
- class Entry(GithubCacheClient.Entry):
6190
- artifact: GithubCacheServiceV1.ArtifactCacheEntry
6191
-
6192
- def get_entry_url(self, entry: GithubCacheClient.Entry) -> ta.Optional[str]:
6193
- entry1 = check.isinstance(entry, self.Entry)
6194
- return entry1.artifact.archive_location
6195
-
6196
- #
6197
-
6198
- def build_get_entry_url_path(self, *keys: str) -> str:
6199
- qp = dict(
6200
- keys=','.join(urllib.parse.quote_plus(k) for k in keys),
6201
- version=str(self._cache_version),
6202
- )
6203
-
6204
- return '?'.join([
6205
- 'cache',
6206
- '&'.join([
6207
- f'{k}={v}'
6208
- for k, v in qp.items()
6209
- ]),
6210
- ])
6211
-
6212
- GET_ENTRY_SUCCESS_STATUS_CODES = (200, 204)
6213
-
6214
-
6215
- ##
6216
-
6217
-
6218
- class GithubCacheServiceV1Client(GithubCacheServiceV1BaseClient):
6219
- DEFAULT_CONCURRENCY = 4
6220
-
6221
- DEFAULT_CHUNK_SIZE = 32 * 1024 * 1024
6222
-
6223
- def __init__(
6224
- self,
6225
- *,
6226
- concurrency: int = DEFAULT_CONCURRENCY,
6227
- chunk_size: int = DEFAULT_CHUNK_SIZE,
6228
- **kwargs: ta.Any,
6229
- ) -> None:
6230
- super().__init__(**kwargs)
6231
-
6232
- check.arg(concurrency > 0)
6233
- self._concurrency = concurrency
6234
-
6235
- check.arg(chunk_size > 0)
6236
- self._chunk_size = chunk_size
6237
-
6238
- #
6239
-
6240
- async def get_entry(self, key: str) -> ta.Optional[GithubCacheServiceV1BaseClient.Entry]:
6241
- obj = await self.send_service_request(
6242
- self.build_get_entry_url_path(self.fix_key(key, partial_suffix=True)),
6243
- )
6244
- if obj is None:
6245
- return None
6246
-
6247
- return self.Entry(GithubCacheServiceV1.dataclass_from_json(
6248
- GithubCacheServiceV1.ArtifactCacheEntry,
6249
- obj,
6250
- ))
6251
-
6252
- #
6319
+ ##
6320
+ # downloading
6253
6321
 
6254
6322
  @dc.dataclass(frozen=True)
6255
6323
  class _DownloadChunk:
@@ -6267,7 +6335,7 @@ class GithubCacheServiceV1Client(GithubCacheServiceV1BaseClient):
6267
6335
  },
6268
6336
  )
6269
6337
 
6270
- _, buf_ = await self.send_url_request(req)
6338
+ _, buf_ = await self._send_urllib_request(req)
6271
6339
 
6272
6340
  buf = check.not_none(buf_)
6273
6341
  check.equal(len(buf), chunk.size)
@@ -6310,7 +6378,7 @@ class GithubCacheServiceV1Client(GithubCacheServiceV1BaseClient):
6310
6378
  #
6311
6379
  # status_code = check.isinstance(curl_res['response_code'], int)
6312
6380
  #
6313
- # if not (200 <= status_code <= 300):
6381
+ # if not (200 <= status_code < 300):
6314
6382
  # raise RuntimeError(f'Curl chunk download {chunk} failed: {curl_res}')
6315
6383
 
6316
6384
  async def _download_file_chunk(self, chunk: _DownloadChunk) -> None:
@@ -6322,11 +6390,17 @@ class GithubCacheServiceV1Client(GithubCacheServiceV1BaseClient):
6322
6390
  ):
6323
6391
  await self._download_file_chunk_urllib(chunk)
6324
6392
 
6325
- async def _download_file(self, entry: GithubCacheServiceV1BaseClient.Entry, out_file: str) -> None:
6326
- key = check.non_empty_str(entry.artifact.cache_key)
6327
- url = check.non_empty_str(entry.artifact.archive_location)
6393
+ async def _download_file_chunks(
6394
+ self,
6395
+ *,
6396
+ key: str,
6397
+ url: str,
6398
+ out_file: str,
6399
+ ) -> None:
6400
+ check.non_empty_str(key)
6401
+ check.non_empty_str(url)
6328
6402
 
6329
- head_resp, _ = await self.send_url_request(urllib.request.Request( # noqa
6403
+ head_resp, _ = await self._send_urllib_request(urllib.request.Request( # noqa
6330
6404
  url,
6331
6405
  method='HEAD',
6332
6406
  ))
@@ -6355,74 +6429,68 @@ class GithubCacheServiceV1Client(GithubCacheServiceV1BaseClient):
6355
6429
 
6356
6430
  await asyncio_wait_concurrent(download_tasks, self._concurrency)
6357
6431
 
6358
- async def download_file(self, entry: GithubCacheClient.Entry, out_file: str) -> None:
6359
- entry1 = check.isinstance(entry, self.Entry)
6360
- with log_timing_context(
6361
- 'Downloading github cache '
6362
- f'key {entry1.artifact.cache_key} '
6363
- f'version {entry1.artifact.cache_version} '
6364
- f'to {out_file}',
6365
- ):
6366
- await self._download_file(entry1, out_file)
6432
+ ##
6433
+ # uploading
6367
6434
 
6368
- #
6435
+ @dc.dataclass(frozen=True)
6436
+ class _UploadChunk:
6437
+ url: str
6438
+ key: str
6439
+ in_file: str
6440
+ offset: int
6441
+ size: int
6369
6442
 
6370
- async def _upload_file_chunk(
6371
- self,
6372
- key: str,
6373
- cache_id: int,
6374
- in_file: str,
6375
- offset: int,
6376
- size: int,
6377
- ) -> None:
6378
- with log_timing_context(
6379
- f'Uploading github cache {key} '
6380
- f'file {in_file} '
6381
- f'chunk {offset} - {offset + size}',
6382
- ):
6383
- with open(in_file, 'rb') as f: # noqa
6384
- f.seek(offset)
6385
- buf = f.read(size)
6443
+ UPLOAD_CHUNK_NUM_RETRIES = 10
6444
+ UPLOAD_CHUNK_RETRY_SLEEP = .5
6386
6445
 
6387
- check.equal(len(buf), size)
6446
+ async def _upload_file_chunk_(self, chunk: _UploadChunk) -> None:
6447
+ with open(chunk.in_file, 'rb') as f: # noqa
6448
+ f.seek(chunk.offset)
6449
+ buf = f.read(chunk.size)
6388
6450
 
6389
- await self.send_service_request(
6390
- f'caches/{cache_id}',
6391
- method='PATCH',
6392
- content_type='application/octet-stream',
6393
- headers={
6394
- 'Content-Range': f'bytes {offset}-{offset + size - 1}/*',
6395
- },
6396
- content=buf,
6397
- success_status_codes=[204],
6398
- )
6451
+ check.equal(len(buf), chunk.size)
6399
6452
 
6400
- async def _upload_file(self, key: str, in_file: str) -> None:
6401
- fixed_key = self.fix_key(key)
6453
+ await self._send_request(
6454
+ url=chunk.url,
6402
6455
 
6403
- check.state(os.path.isfile(in_file))
6456
+ method='PATCH',
6404
6457
 
6405
- file_size = os.stat(in_file).st_size
6458
+ headers={
6459
+ 'Content-Range': f'bytes {chunk.offset}-{chunk.offset + chunk.size - 1}/*',
6460
+ },
6461
+ no_auth=True,
6462
+ content_type='application/octet-stream',
6406
6463
 
6407
- #
6464
+ content=buf,
6408
6465
 
6409
- reserve_req = GithubCacheServiceV1.ReserveCacheRequest(
6410
- key=fixed_key,
6411
- cache_size=file_size,
6412
- version=str(self._cache_version),
6413
- )
6414
- reserve_resp_obj = await self.send_service_request(
6415
- 'caches',
6416
- json_content=GithubCacheServiceV1.dataclass_to_json(reserve_req),
6417
- success_status_codes=[201],
6418
- )
6419
- reserve_resp = GithubCacheServiceV1.dataclass_from_json( # noqa
6420
- GithubCacheServiceV1.ReserveCacheResponse,
6421
- reserve_resp_obj,
6466
+ success_status_codes=[204],
6467
+
6468
+ # retry_status_codes=[405],
6469
+ num_retries=self.UPLOAD_CHUNK_NUM_RETRIES,
6470
+ retry_sleep=self.UPLOAD_CHUNK_RETRY_SLEEP,
6422
6471
  )
6423
- cache_id = check.isinstance(reserve_resp.cache_id, int)
6424
6472
 
6425
- log.debug(f'Github cache file {os.path.basename(in_file)} got id {cache_id}') # noqa
6473
+ async def _upload_file_chunk(self, chunk: _UploadChunk) -> None:
6474
+ with log_timing_context(
6475
+ f'Uploading github cache {chunk.key} '
6476
+ f'file {chunk.in_file} '
6477
+ f'chunk {chunk.offset} - {chunk.offset + chunk.size}',
6478
+ ):
6479
+ await self._upload_file_chunk_(chunk)
6480
+
6481
+ async def _upload_file_chunks(
6482
+ self,
6483
+ *,
6484
+ in_file: str,
6485
+ url: str,
6486
+ key: str,
6487
+
6488
+ file_size: ta.Optional[int] = None,
6489
+ ) -> None:
6490
+ check.state(os.path.isfile(in_file))
6491
+
6492
+ if file_size is None:
6493
+ file_size = os.stat(in_file).st_size
6426
6494
 
6427
6495
  #
6428
6496
 
@@ -6431,34 +6499,16 @@ class GithubCacheServiceV1Client(GithubCacheServiceV1BaseClient):
6431
6499
  for i in range((file_size // chunk_size) + (1 if file_size % chunk_size else 0)):
6432
6500
  offset = i * chunk_size
6433
6501
  size = min(chunk_size, file_size - offset)
6434
- upload_tasks.append(self._upload_file_chunk(
6435
- fixed_key,
6436
- cache_id,
6437
- in_file,
6438
- offset,
6439
- size,
6440
- ))
6502
+ upload_tasks.append(self._upload_file_chunk(self._UploadChunk(
6503
+ url=url,
6504
+ key=key,
6505
+ in_file=in_file,
6506
+ offset=offset,
6507
+ size=size,
6508
+ )))
6441
6509
 
6442
6510
  await asyncio_wait_concurrent(upload_tasks, self._concurrency)
6443
6511
 
6444
- #
6445
-
6446
- commit_req = GithubCacheServiceV1.CommitCacheRequest(
6447
- size=file_size,
6448
- )
6449
- await self.send_service_request(
6450
- f'caches/{cache_id}',
6451
- json_content=GithubCacheServiceV1.dataclass_to_json(commit_req),
6452
- success_status_codes=[204],
6453
- )
6454
-
6455
- async def upload_file(self, key: str, in_file: str) -> None:
6456
- with log_timing_context(
6457
- f'Uploading github cache file {os.path.basename(in_file)} '
6458
- f'key {key}',
6459
- ):
6460
- await self._upload_file(key, in_file)
6461
-
6462
6512
 
6463
6513
  ########################################
6464
6514
  # ../../dataserver/targets.py
@@ -7708,119 +7758,318 @@ def subprocess_maybe_shell_wrap_exec(*cmd: str) -> ta.Tuple[str, ...]:
7708
7758
 
7709
7759
 
7710
7760
  ########################################
7711
- # ../github/cache.py
7761
+ # ../github/api/v1/client.py
7712
7762
 
7713
7763
 
7714
7764
  ##
7715
7765
 
7716
7766
 
7717
- class GithubCache(FileCache, DataCache):
7718
- @dc.dataclass(frozen=True)
7719
- class Config:
7720
- pass
7767
+ class GithubCacheServiceV1Client(BaseGithubCacheClient):
7768
+ BASE_URL_ENV_VAR = register_github_env_var('ACTIONS_CACHE_URL')
7721
7769
 
7722
7770
  def __init__(
7723
7771
  self,
7724
- config: Config = Config(),
7725
7772
  *,
7726
- client: ta.Optional[GithubCacheClient] = None,
7727
- version: ta.Optional[CacheVersion] = None,
7773
+ base_url: ta.Optional[str] = None,
7728
7774
 
7729
- local: DirectoryFileCache,
7775
+ **kwargs: ta.Any,
7730
7776
  ) -> None:
7777
+ if base_url is None:
7778
+ base_url = check.non_empty_str(self.BASE_URL_ENV_VAR())
7779
+ service_url = GithubCacheServiceV1.get_service_url(base_url)
7780
+
7731
7781
  super().__init__(
7732
- version=version,
7782
+ service_url=service_url,
7783
+ **kwargs,
7733
7784
  )
7734
7785
 
7735
- self._config = config
7786
+ #
7736
7787
 
7737
- if client is None:
7738
- client = GithubCacheServiceV1Client(
7739
- cache_version=self._version,
7740
- )
7741
- self._client: GithubCacheClient = client
7788
+ def _build_request_headers(
7789
+ self,
7790
+ headers: ta.Optional[ta.Mapping[str, str]] = None,
7791
+ **kwargs: ta.Any,
7792
+ ) -> ta.Dict[str, str]:
7793
+ return super()._build_request_headers(
7794
+ {
7795
+ 'Accept': ';'.join([
7796
+ 'application/json',
7797
+ f'api-version={GithubCacheServiceV1.API_VERSION}',
7798
+ ]),
7799
+ **(headers or {}),
7800
+ },
7801
+ **kwargs,
7802
+ )
7742
7803
 
7743
- self._local = local
7804
+ #
7805
+
7806
+ @dc.dataclass(frozen=True)
7807
+ class Entry(GithubCacheClient.Entry):
7808
+ artifact: GithubCacheServiceV1.ArtifactCacheEntry
7809
+
7810
+ def get_entry_url(self, entry: GithubCacheClient.Entry) -> ta.Optional[str]:
7811
+ entry1 = check.isinstance(entry, self.Entry)
7812
+ return entry1.artifact.archive_location
7744
7813
 
7745
7814
  #
7746
7815
 
7747
- async def get_file(self, key: str) -> ta.Optional[str]:
7748
- local_file = self._local.get_cache_file_path(key)
7749
- if os.path.exists(local_file):
7750
- return local_file
7816
+ def _build_get_entry_url_path(self, *keys: str) -> str:
7817
+ qp = dict(
7818
+ keys=','.join(urllib.parse.quote_plus(k) for k in keys),
7819
+ version=str(self._cache_version),
7820
+ )
7751
7821
 
7752
- if (entry := await self._client.get_entry(key)) is None:
7822
+ return '?'.join([
7823
+ 'cache',
7824
+ '&'.join([
7825
+ f'{k}={v}'
7826
+ for k, v in qp.items()
7827
+ ]),
7828
+ ])
7829
+
7830
+ GET_ENTRY_SUCCESS_STATUS_CODES = (200, 204)
7831
+
7832
+ #
7833
+
7834
+ async def get_entry(self, key: str) -> ta.Optional[GithubCacheClient.Entry]:
7835
+ obj = await self._send_request(
7836
+ path=self._build_get_entry_url_path(self.fix_key(key, partial_suffix=True)),
7837
+ )
7838
+ if obj is None:
7753
7839
  return None
7754
7840
 
7755
- tmp_file = self._local.format_incomplete_file(local_file)
7756
- with unlinking_if_exists(tmp_file):
7757
- await self._client.download_file(entry, tmp_file)
7841
+ return self.Entry(GithubCacheServiceV1.dataclass_from_json(
7842
+ GithubCacheServiceV1.ArtifactCacheEntry,
7843
+ obj,
7844
+ ))
7758
7845
 
7759
- os.replace(tmp_file, local_file)
7846
+ #
7760
7847
 
7761
- return local_file
7848
+ async def download_file(self, entry: GithubCacheClient.Entry, out_file: str) -> None:
7849
+ entry1 = check.isinstance(entry, self.Entry)
7850
+ with log_timing_context(
7851
+ 'Downloading github cache '
7852
+ f'key {entry1.artifact.cache_key} '
7853
+ f'version {entry1.artifact.cache_version} '
7854
+ f'to {out_file}',
7855
+ ):
7856
+ await self._download_file_chunks(
7857
+ key=check.non_empty_str(entry1.artifact.cache_key),
7858
+ url=check.non_empty_str(entry1.artifact.archive_location),
7859
+ out_file=out_file,
7860
+ )
7762
7861
 
7763
- async def put_file(
7862
+ #
7863
+
7864
+ async def _upload_file(self, key: str, in_file: str) -> None:
7865
+ fixed_key = self.fix_key(key)
7866
+
7867
+ check.state(os.path.isfile(in_file))
7868
+ file_size = os.stat(in_file).st_size
7869
+
7870
+ #
7871
+
7872
+ reserve_req = GithubCacheServiceV1.ReserveCacheRequest(
7873
+ key=fixed_key,
7874
+ cache_size=file_size,
7875
+ version=str(self._cache_version),
7876
+ )
7877
+ reserve_resp_obj = await self._send_request(
7878
+ path='caches',
7879
+ json_content=GithubCacheServiceV1.dataclass_to_json(reserve_req),
7880
+ success_status_codes=[201],
7881
+ )
7882
+ reserve_resp = GithubCacheServiceV1.dataclass_from_json( # noqa
7883
+ GithubCacheServiceV1.ReserveCacheResponse,
7884
+ reserve_resp_obj,
7885
+ )
7886
+ cache_id = check.isinstance(reserve_resp.cache_id, int)
7887
+
7888
+ log.debug(f'Github cache file {os.path.basename(in_file)} got id {cache_id}') # noqa
7889
+
7890
+ #
7891
+
7892
+ url = f'{self._service_url}/caches/{cache_id}'
7893
+
7894
+ await self._upload_file_chunks(
7895
+ in_file=in_file,
7896
+ url=url,
7897
+ key=fixed_key,
7898
+ file_size=file_size,
7899
+ )
7900
+
7901
+ #
7902
+
7903
+ commit_req = GithubCacheServiceV1.CommitCacheRequest(
7904
+ size=file_size,
7905
+ )
7906
+ await self._send_request(
7907
+ path=f'caches/{cache_id}',
7908
+ json_content=GithubCacheServiceV1.dataclass_to_json(commit_req),
7909
+ success_status_codes=[204],
7910
+ )
7911
+
7912
+ async def upload_file(self, key: str, in_file: str) -> None:
7913
+ with log_timing_context(
7914
+ f'Uploading github cache file {os.path.basename(in_file)} '
7915
+ f'key {key}',
7916
+ ):
7917
+ await self._upload_file(key, in_file)
7918
+
7919
+
7920
+ ########################################
7921
+ # ../github/api/v2/client.py
7922
+
7923
+
7924
+ ##
7925
+
7926
+
7927
+ class GithubCacheServiceV2Client(BaseGithubCacheClient):
7928
+ BASE_URL_ENV_VAR = register_github_env_var('ACTIONS_RESULTS_URL')
7929
+
7930
+ def __init__(
7764
7931
  self,
7765
- key: str,
7766
- file_path: str,
7767
7932
  *,
7768
- steal: bool = False,
7769
- ) -> str:
7770
- cache_file_path = await self._local.put_file(
7771
- key,
7772
- file_path,
7773
- steal=steal,
7933
+ base_url: ta.Optional[str] = None,
7934
+
7935
+ **kwargs: ta.Any,
7936
+ ) -> None:
7937
+ if base_url is None:
7938
+ base_url = check.non_empty_str(self.BASE_URL_ENV_VAR())
7939
+ service_url = GithubCacheServiceV2.get_service_url(base_url)
7940
+
7941
+ super().__init__(
7942
+ service_url=service_url,
7943
+ **kwargs,
7774
7944
  )
7775
7945
 
7776
- await self._client.upload_file(key, cache_file_path)
7946
+ #
7777
7947
 
7778
- return cache_file_path
7948
+ async def _send_method_request(
7949
+ self,
7950
+ method: GithubCacheServiceV2.Method[
7951
+ GithubCacheServiceV2RequestT,
7952
+ GithubCacheServiceV2ResponseT,
7953
+ ],
7954
+ request: GithubCacheServiceV2RequestT,
7955
+ **kwargs: ta.Any,
7956
+ ) -> ta.Optional[GithubCacheServiceV2ResponseT]:
7957
+ obj = await self._send_request(
7958
+ path=method.name,
7959
+ json_content=dc.asdict(request), # type: ignore[call-overload]
7960
+ **kwargs,
7961
+ )
7962
+
7963
+ if obj is None:
7964
+ return None
7965
+ return method.response(**obj)
7779
7966
 
7780
7967
  #
7781
7968
 
7782
- async def get_data(self, key: str) -> ta.Optional[DataCache.Data]:
7783
- local_file = self._local.get_cache_file_path(key)
7784
- if os.path.exists(local_file):
7785
- return DataCache.FileData(local_file)
7969
+ @dc.dataclass(frozen=True)
7970
+ class Entry(GithubCacheClient.Entry):
7971
+ request: GithubCacheServiceV2.GetCacheEntryDownloadUrlRequest
7972
+ response: GithubCacheServiceV2.GetCacheEntryDownloadUrlResponse
7786
7973
 
7787
- if (entry := await self._client.get_entry(key)) is None:
7974
+ def __post_init__(self) -> None:
7975
+ check.state(self.response.ok)
7976
+ check.non_empty_str(self.response.signed_download_url)
7977
+
7978
+ def get_entry_url(self, entry: GithubCacheClient.Entry) -> ta.Optional[str]:
7979
+ entry2 = check.isinstance(entry, self.Entry)
7980
+ return check.non_empty_str(entry2.response.signed_download_url)
7981
+
7982
+ #
7983
+
7984
+ async def get_entry(self, key: str) -> ta.Optional[GithubCacheClient.Entry]:
7985
+ version = str(self._cache_version).zfill(GithubCacheServiceV2.VERSION_LENGTH)
7986
+
7987
+ req = GithubCacheServiceV2.GetCacheEntryDownloadUrlRequest(
7988
+ key=self.fix_key(key),
7989
+ restore_keys=[self.fix_key(key, partial_suffix=True)],
7990
+ version=version,
7991
+ )
7992
+
7993
+ resp = await self._send_method_request(
7994
+ GithubCacheServiceV2.GET_CACHE_ENTRY_DOWNLOAD_URL_METHOD,
7995
+ req,
7996
+ )
7997
+ if resp is None or not resp.ok:
7788
7998
  return None
7789
7999
 
7790
- return DataCache.UrlData(check.non_empty_str(self._client.get_entry_url(entry)))
8000
+ return self.Entry(
8001
+ request=req,
8002
+ response=resp,
8003
+ )
7791
8004
 
7792
- async def put_data(self, key: str, data: DataCache.Data) -> None:
7793
- await FileCacheDataCache(self).put_data(key, data)
8005
+ #
7794
8006
 
8007
+ async def download_file(self, entry: GithubCacheClient.Entry, out_file: str) -> None:
8008
+ entry2 = check.isinstance(entry, self.Entry)
8009
+ with log_timing_context(
8010
+ 'Downloading github cache '
8011
+ f'key {entry2.response.matched_key} '
8012
+ f'version {entry2.request.version} '
8013
+ f'to {out_file}',
8014
+ ):
8015
+ await self._download_file_chunks(
8016
+ key=check.non_empty_str(entry2.response.matched_key),
8017
+ url=check.non_empty_str(entry2.response.signed_download_url),
8018
+ out_file=out_file,
8019
+ )
7795
8020
 
7796
- ########################################
7797
- # ../github/cli.py
7798
- """
7799
- See:
7800
- - https://docs.github.com/en/rest/actions/cache?apiVersion=2022-11-28
7801
- """
8021
+ #
7802
8022
 
8023
+ async def _upload_file(self, key: str, in_file: str) -> None:
8024
+ fixed_key = self.fix_key(key)
7803
8025
 
7804
- class GithubCli(ArgparseCli):
7805
- @argparse_cmd()
7806
- def list_referenced_env_vars(self) -> None:
7807
- print('\n'.join(sorted(ev.k for ev in GITHUB_ENV_VARS)))
8026
+ check.state(os.path.isfile(in_file))
8027
+ file_size = os.stat(in_file).st_size
7808
8028
 
7809
- @argparse_cmd(
7810
- argparse_arg('key'),
7811
- )
7812
- async def get_cache_entry(self) -> None:
7813
- client = GithubCacheServiceV1Client()
7814
- entry = await client.get_entry(self.args.key)
7815
- if entry is None:
7816
- return
7817
- print(json_dumps_pretty(dc.asdict(entry))) # noqa
8029
+ #
7818
8030
 
7819
- @argparse_cmd(
7820
- argparse_arg('repository-id'),
7821
- )
7822
- def list_cache_entries(self) -> None:
7823
- raise NotImplementedError
8031
+ version = str(self._cache_version).zfill(GithubCacheServiceV2.VERSION_LENGTH)
8032
+
8033
+ reserve_resp = check.not_none(await self._send_method_request(
8034
+ GithubCacheServiceV2.CREATE_CACHE_ENTRY_METHOD, # type: ignore[arg-type]
8035
+ GithubCacheServiceV2.CreateCacheEntryRequest(
8036
+ key=fixed_key,
8037
+ version=version,
8038
+ ),
8039
+ ))
8040
+ check.state(reserve_resp.ok)
8041
+
8042
+ log.debug(f'Github cache file {os.path.basename(in_file)} upload reserved for file size {file_size}') # noqa
8043
+
8044
+ #
8045
+
8046
+ await self._upload_file_chunks(
8047
+ in_file=in_file,
8048
+ url=reserve_resp.signed_upload_url,
8049
+ key=fixed_key,
8050
+ file_size=file_size,
8051
+ )
8052
+
8053
+ #
8054
+
8055
+ commit_resp = check.not_none(await self._send_method_request(
8056
+ GithubCacheServiceV2.FINALIZE_CACHE_ENTRY_METHOD, # type: ignore[arg-type]
8057
+ GithubCacheServiceV2.FinalizeCacheEntryUploadRequest(
8058
+ key=key,
8059
+ size_bytes=file_size,
8060
+ version=version,
8061
+ ),
8062
+ ))
8063
+ check.state(commit_resp.ok)
8064
+
8065
+ log.debug(f'Github cache file {os.path.basename(in_file)} upload complete, entry id {commit_resp.entry_id}') # noqa
8066
+
8067
+ async def upload_file(self, key: str, in_file: str) -> None:
8068
+ with log_timing_context(
8069
+ f'Uploading github cache file {os.path.basename(in_file)} '
8070
+ f'key {key}',
8071
+ ):
8072
+ await self._upload_file(key, in_file)
7824
8073
 
7825
8074
 
7826
8075
  ########################################
@@ -9619,20 +9868,129 @@ async def build_cache_served_docker_image_data_server_routes(
9619
9868
 
9620
9869
 
9621
9870
  ########################################
9622
- # ../github/inject.py
9871
+ # ../github/cache.py
9623
9872
 
9624
9873
 
9625
9874
  ##
9626
9875
 
9627
9876
 
9628
- def bind_github() -> InjectorBindings:
9629
- lst: ta.List[InjectorBindingOrBindings] = [
9630
- inj.bind(GithubCache, singleton=True),
9631
- inj.bind(DataCache, to_key=GithubCache),
9632
- inj.bind(FileCache, to_key=GithubCache),
9633
- ]
9877
+ class GithubCache(FileCache, DataCache):
9878
+ @dc.dataclass(frozen=True)
9879
+ class Config:
9880
+ pass
9634
9881
 
9635
- return inj.as_bindings(*lst)
9882
+ DEFAULT_CLIENT_VERSION: ta.ClassVar[int] = 2
9883
+
9884
+ DEFAULT_CLIENTS_BY_VERSION: ta.ClassVar[ta.Mapping[int, ta.Callable[..., GithubCacheClient]]] = {
9885
+ 1: GithubCacheServiceV1Client,
9886
+ 2: GithubCacheServiceV2Client,
9887
+ }
9888
+
9889
+ def __init__(
9890
+ self,
9891
+ config: Config = Config(),
9892
+ *,
9893
+ client: ta.Optional[GithubCacheClient] = None,
9894
+ default_client_version: ta.Optional[int] = None,
9895
+
9896
+ version: ta.Optional[CacheVersion] = None,
9897
+
9898
+ local: DirectoryFileCache,
9899
+ ) -> None:
9900
+ super().__init__(
9901
+ version=version,
9902
+ )
9903
+
9904
+ self._config = config
9905
+
9906
+ if client is None:
9907
+ client_cls = self.DEFAULT_CLIENTS_BY_VERSION[default_client_version or self.DEFAULT_CLIENT_VERSION]
9908
+ client = client_cls(
9909
+ cache_version=self._version,
9910
+ )
9911
+ self._client: GithubCacheClient = client
9912
+
9913
+ self._local = local
9914
+
9915
+ #
9916
+
9917
+ async def get_file(self, key: str) -> ta.Optional[str]:
9918
+ local_file = self._local.get_cache_file_path(key)
9919
+ if os.path.exists(local_file):
9920
+ return local_file
9921
+
9922
+ if (entry := await self._client.get_entry(key)) is None:
9923
+ return None
9924
+
9925
+ tmp_file = self._local.format_incomplete_file(local_file)
9926
+ with unlinking_if_exists(tmp_file):
9927
+ await self._client.download_file(entry, tmp_file)
9928
+
9929
+ os.replace(tmp_file, local_file)
9930
+
9931
+ return local_file
9932
+
9933
+ async def put_file(
9934
+ self,
9935
+ key: str,
9936
+ file_path: str,
9937
+ *,
9938
+ steal: bool = False,
9939
+ ) -> str:
9940
+ cache_file_path = await self._local.put_file(
9941
+ key,
9942
+ file_path,
9943
+ steal=steal,
9944
+ )
9945
+
9946
+ await self._client.upload_file(key, cache_file_path)
9947
+
9948
+ return cache_file_path
9949
+
9950
+ #
9951
+
9952
+ async def get_data(self, key: str) -> ta.Optional[DataCache.Data]:
9953
+ local_file = self._local.get_cache_file_path(key)
9954
+ if os.path.exists(local_file):
9955
+ return DataCache.FileData(local_file)
9956
+
9957
+ if (entry := await self._client.get_entry(key)) is None:
9958
+ return None
9959
+
9960
+ return DataCache.UrlData(check.non_empty_str(self._client.get_entry_url(entry)))
9961
+
9962
+ async def put_data(self, key: str, data: DataCache.Data) -> None:
9963
+ await FileCacheDataCache(self).put_data(key, data)
9964
+
9965
+
9966
+ ########################################
9967
+ # ../github/cli.py
9968
+ """
9969
+ See:
9970
+ - https://docs.github.com/en/rest/actions/cache?apiVersion=2022-11-28
9971
+ """
9972
+
9973
+
9974
+ class GithubCli(ArgparseCli):
9975
+ @argparse_cmd()
9976
+ def list_referenced_env_vars(self) -> None:
9977
+ print('\n'.join(sorted(ev.k for ev in GITHUB_ENV_VARS)))
9978
+
9979
+ @argparse_cmd(
9980
+ argparse_arg('key'),
9981
+ )
9982
+ async def get_cache_entry(self) -> None:
9983
+ client = GithubCacheServiceV1Client()
9984
+ entry = await client.get_entry(self.args.key)
9985
+ if entry is None:
9986
+ return
9987
+ print(json_dumps_pretty(dc.asdict(entry))) # noqa
9988
+
9989
+ @argparse_cmd(
9990
+ argparse_arg('repository-id'),
9991
+ )
9992
+ def list_cache_entries(self) -> None:
9993
+ raise NotImplementedError
9636
9994
 
9637
9995
 
9638
9996
  ########################################
@@ -10325,6 +10683,23 @@ subprocesses = Subprocesses()
10325
10683
  SubprocessRun._DEFAULT_SUBPROCESSES = subprocesses # noqa
10326
10684
 
10327
10685
 
10686
+ ########################################
10687
+ # ../github/inject.py
10688
+
10689
+
10690
+ ##
10691
+
10692
+
10693
+ def bind_github() -> InjectorBindings:
10694
+ lst: ta.List[InjectorBindingOrBindings] = [
10695
+ inj.bind(GithubCache, singleton=True),
10696
+ inj.bind(DataCache, to_key=GithubCache),
10697
+ inj.bind(FileCache, to_key=GithubCache),
10698
+ ]
10699
+
10700
+ return inj.as_bindings(*lst)
10701
+
10702
+
10328
10703
  ########################################
10329
10704
  # ../requirements.py
10330
10705
  """