omdev 0.0.0.dev289__py3-none-any.whl → 0.0.0.dev291__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/ci/github/api/__init__.py +0 -0
- omdev/ci/github/{client.py → api/clients.py} +190 -207
- omdev/ci/github/api/v1/__init__.py +0 -0
- omdev/ci/github/{api.py → api/v1/api.py} +5 -87
- omdev/ci/github/api/v1/client.py +171 -0
- omdev/ci/github/api/v2/__init__.py +0 -0
- omdev/ci/github/api/v2/api.py +148 -0
- omdev/ci/github/api/v2/azure.py +185 -0
- omdev/ci/github/api/v2/client.py +201 -0
- omdev/ci/github/cache.py +14 -3
- omdev/ci/github/cli.py +1 -1
- omdev/pyproject/pkg.py +5 -2
- omdev/scripts/ci.py +890 -285
- omdev/scripts/pyproject.py +5 -2
- {omdev-0.0.0.dev289.dist-info → omdev-0.0.0.dev291.dist-info}/METADATA +2 -2
- {omdev-0.0.0.dev289.dist-info → omdev-0.0.0.dev291.dist-info}/RECORD +20 -13
- {omdev-0.0.0.dev289.dist-info → omdev-0.0.0.dev291.dist-info}/WHEEL +1 -1
- {omdev-0.0.0.dev289.dist-info → omdev-0.0.0.dev291.dist-info}/entry_points.txt +0 -0
- {omdev-0.0.0.dev289.dist-info → omdev-0.0.0.dev291.dist-info}/licenses/LICENSE +0 -0
- {omdev-0.0.0.dev289.dist-info → omdev-0.0.0.dev291.dist-info}/top_level.txt +0 -0
omdev/scripts/ci.py
CHANGED
@@ -71,6 +71,7 @@ import urllib.parse
|
|
71
71
|
import urllib.request
|
72
72
|
import uuid
|
73
73
|
import weakref
|
74
|
+
import xml.etree.ElementTree as ET
|
74
75
|
|
75
76
|
|
76
77
|
########################################
|
@@ -103,6 +104,10 @@ TimeoutLike = ta.Union['Timeout', ta.Type['Timeout.Default'], ta.Iterable['Timeo
|
|
103
104
|
# ../../omlish/sockets/addresses.py
|
104
105
|
SocketAddress = ta.Any
|
105
106
|
|
107
|
+
# github/api/v2/api.py
|
108
|
+
GithubCacheServiceV2RequestT = ta.TypeVar('GithubCacheServiceV2RequestT')
|
109
|
+
GithubCacheServiceV2ResponseT = ta.TypeVar('GithubCacheServiceV2ResponseT')
|
110
|
+
|
106
111
|
# ../../omlish/argparse/cli.py
|
107
112
|
ArgparseCmdFn = ta.Callable[[], ta.Optional[int]] # ta.TypeAlias
|
108
113
|
|
@@ -418,6 +423,31 @@ class DockerPortRelay:
|
|
418
423
|
]
|
419
424
|
|
420
425
|
|
426
|
+
########################################
|
427
|
+
# ../../../omlish/http/urllib.py
|
428
|
+
|
429
|
+
|
430
|
+
##
|
431
|
+
|
432
|
+
|
433
|
+
class NonRaisingUrllibErrorProcessor(urllib.request.HTTPErrorProcessor):
|
434
|
+
"""
|
435
|
+
https://stackoverflow.com/a/74844056
|
436
|
+
|
437
|
+
Usage:
|
438
|
+
|
439
|
+
opener = urllib.request.build_opener(NonRaisingUrllibErrorProcessor)
|
440
|
+
with opener.open(req) as resp:
|
441
|
+
...
|
442
|
+
"""
|
443
|
+
|
444
|
+
def http_response(self, request, response):
|
445
|
+
return response
|
446
|
+
|
447
|
+
def https_response(self, request, response):
|
448
|
+
return response
|
449
|
+
|
450
|
+
|
421
451
|
########################################
|
422
452
|
# ../../../omlish/http/versions.py
|
423
453
|
|
@@ -1985,7 +2015,7 @@ def read_docker_tar_image_id(tar_file: str) -> str:
|
|
1985
2015
|
|
1986
2016
|
|
1987
2017
|
########################################
|
1988
|
-
# ../github/api.py
|
2018
|
+
# ../github/api/v1/api.py
|
1989
2019
|
"""
|
1990
2020
|
export FILE_SIZE=$(stat --format="%s" $FILE)
|
1991
2021
|
|
@@ -2028,6 +2058,11 @@ curl -s \
|
|
2028
2058
|
|
2029
2059
|
|
2030
2060
|
class GithubCacheServiceV1:
|
2061
|
+
def __new__(cls, *args, **kwargs): # noqa
|
2062
|
+
raise TypeError
|
2063
|
+
|
2064
|
+
#
|
2065
|
+
|
2031
2066
|
API_VERSION = '6.0-preview.1'
|
2032
2067
|
|
2033
2068
|
@classmethod
|
@@ -2098,14 +2133,54 @@ class GithubCacheServiceV1:
|
|
2098
2133
|
cache_size: ta.Optional[int]
|
2099
2134
|
|
2100
2135
|
|
2136
|
+
########################################
|
2137
|
+
# ../github/api/v2/api.py
|
2138
|
+
"""
|
2139
|
+
https://github.com/tonistiigi/go-actions-cache/blob/3e9a6642607fd6e4d5d4fdab7c91fe8bf4c36a25/cache_v2.go
|
2140
|
+
|
2141
|
+
==
|
2142
|
+
|
2143
|
+
curl -s \
|
2144
|
+
-X POST \
|
2145
|
+
"${ACTIONS_RESULTS_URL}twirp/github.actions.results.api.v1.CacheService/CreateCacheEntry" \
|
2146
|
+
-H 'Content-Type: application/json' \
|
2147
|
+
-H "Authorization: Bearer $ACTIONS_RUNTIME_TOKEN" \
|
2148
|
+
-d '{"key": "foo", "version": "0000000000000000000000000000000000000000000000000000000000000001" }' \
|
2149
|
+
| jq .
|
2150
|
+
|
2151
|
+
curl -s \
|
2152
|
+
-X POST \
|
2153
|
+
"${ACTIONS_RESULTS_URL}twirp/github.actions.results.api.v1.CacheService/GetCacheEntryDownloadURL" \
|
2154
|
+
-H 'Content-Type: application/json' \
|
2155
|
+
-H "Authorization: Bearer $ACTIONS_RUNTIME_TOKEN" \
|
2156
|
+
-d '{"key": "foo", "restoreKeys": [], "version": "0000000000000000000000000000000000000000000000000000000000000001" }' \
|
2157
|
+
| jq .
|
2158
|
+
|
2159
|
+
""" # noqa
|
2160
|
+
|
2161
|
+
|
2162
|
+
##
|
2163
|
+
|
2164
|
+
|
2101
2165
|
class GithubCacheServiceV2:
|
2166
|
+
def __new__(cls, *args, **kwargs): # noqa
|
2167
|
+
raise TypeError
|
2168
|
+
|
2169
|
+
#
|
2170
|
+
|
2102
2171
|
SERVICE_NAME = 'github.actions.results.api.v1.CacheService'
|
2103
2172
|
|
2173
|
+
@classmethod
|
2174
|
+
def get_service_url(cls, base_url: str) -> str:
|
2175
|
+
return f'{base_url.rstrip("/")}/twirp/{cls.SERVICE_NAME}'
|
2176
|
+
|
2177
|
+
#
|
2178
|
+
|
2104
2179
|
@dc.dataclass(frozen=True)
|
2105
|
-
class Method:
|
2180
|
+
class Method(ta.Generic[GithubCacheServiceV2RequestT, GithubCacheServiceV2ResponseT]):
|
2106
2181
|
name: str
|
2107
|
-
request:
|
2108
|
-
response:
|
2182
|
+
request: ta.Type[GithubCacheServiceV2RequestT]
|
2183
|
+
response: ta.Type[GithubCacheServiceV2ResponseT]
|
2109
2184
|
|
2110
2185
|
#
|
2111
2186
|
|
@@ -2124,6 +2199,8 @@ class GithubCacheServiceV2:
|
|
2124
2199
|
repository_id: int
|
2125
2200
|
scope: ta.Sequence['GithubCacheServiceV2.CacheScope']
|
2126
2201
|
|
2202
|
+
VERSION_LENGTH: int = 64
|
2203
|
+
|
2127
2204
|
#
|
2128
2205
|
|
2129
2206
|
@dc.dataclass(frozen=True)
|
@@ -2132,12 +2209,18 @@ class GithubCacheServiceV2:
|
|
2132
2209
|
version: str
|
2133
2210
|
metadata: ta.Optional['GithubCacheServiceV2.CacheMetadata'] = None
|
2134
2211
|
|
2212
|
+
def __post_init__(self) -> None:
|
2213
|
+
check.equal(len(self.version), GithubCacheServiceV2.VERSION_LENGTH)
|
2214
|
+
|
2135
2215
|
@dc.dataclass(frozen=True)
|
2136
2216
|
class CreateCacheEntryResponse:
|
2137
2217
|
ok: bool
|
2138
2218
|
signed_upload_url: str
|
2139
2219
|
|
2140
|
-
CREATE_CACHE_ENTRY_METHOD
|
2220
|
+
CREATE_CACHE_ENTRY_METHOD: Method[
|
2221
|
+
CreateCacheEntryRequest,
|
2222
|
+
CreateCacheEntryResponse,
|
2223
|
+
] = Method(
|
2141
2224
|
'CreateCacheEntry',
|
2142
2225
|
CreateCacheEntryRequest,
|
2143
2226
|
CreateCacheEntryResponse,
|
@@ -2157,7 +2240,10 @@ class GithubCacheServiceV2:
|
|
2157
2240
|
ok: bool
|
2158
2241
|
entry_id: str
|
2159
2242
|
|
2160
|
-
FINALIZE_CACHE_ENTRY_METHOD
|
2243
|
+
FINALIZE_CACHE_ENTRY_METHOD: Method[
|
2244
|
+
FinalizeCacheEntryUploadRequest,
|
2245
|
+
FinalizeCacheEntryUploadResponse,
|
2246
|
+
] = Method(
|
2161
2247
|
'FinalizeCacheEntryUpload',
|
2162
2248
|
FinalizeCacheEntryUploadRequest,
|
2163
2249
|
FinalizeCacheEntryUploadResponse,
|
@@ -2178,7 +2264,10 @@ class GithubCacheServiceV2:
|
|
2178
2264
|
signed_download_url: str
|
2179
2265
|
matched_key: str
|
2180
2266
|
|
2181
|
-
GET_CACHE_ENTRY_DOWNLOAD_URL_METHOD
|
2267
|
+
GET_CACHE_ENTRY_DOWNLOAD_URL_METHOD: Method[
|
2268
|
+
GetCacheEntryDownloadUrlRequest,
|
2269
|
+
GetCacheEntryDownloadUrlResponse,
|
2270
|
+
] = Method(
|
2182
2271
|
'GetCacheEntryDownloadURL',
|
2183
2272
|
GetCacheEntryDownloadUrlRequest,
|
2184
2273
|
GetCacheEntryDownloadUrlResponse,
|
@@ -5978,13 +6067,14 @@ class FileCacheDataCache(DataCache):
|
|
5978
6067
|
|
5979
6068
|
|
5980
6069
|
########################################
|
5981
|
-
# ../github/
|
6070
|
+
# ../github/api/clients.py
|
5982
6071
|
|
5983
6072
|
|
5984
6073
|
##
|
5985
6074
|
|
5986
6075
|
|
5987
6076
|
class GithubCacheClient(abc.ABC):
|
6077
|
+
@dc.dataclass(frozen=True)
|
5988
6078
|
class Entry(abc.ABC): # noqa
|
5989
6079
|
pass
|
5990
6080
|
|
@@ -6007,18 +6097,21 @@ class GithubCacheClient(abc.ABC):
|
|
6007
6097
|
##
|
6008
6098
|
|
6009
6099
|
|
6010
|
-
class
|
6011
|
-
BASE_URL_ENV_VAR = register_github_env_var('ACTIONS_CACHE_URL')
|
6100
|
+
class BaseGithubCacheClient(GithubCacheClient, abc.ABC):
|
6012
6101
|
AUTH_TOKEN_ENV_VAR = register_github_env_var('ACTIONS_RUNTIME_TOKEN') # noqa
|
6013
6102
|
|
6014
6103
|
KEY_SUFFIX_ENV_VAR = register_github_env_var('GITHUB_RUN_ID')
|
6015
6104
|
|
6105
|
+
DEFAULT_CONCURRENCY = 4
|
6106
|
+
DEFAULT_CHUNK_SIZE = 64 * 1024 * 1024
|
6107
|
+
|
6016
6108
|
#
|
6017
6109
|
|
6018
6110
|
def __init__(
|
6019
6111
|
self,
|
6020
6112
|
*,
|
6021
|
-
|
6113
|
+
service_url: str,
|
6114
|
+
|
6022
6115
|
auth_token: ta.Optional[str] = None,
|
6023
6116
|
|
6024
6117
|
key_prefix: ta.Optional[str] = None,
|
@@ -6027,14 +6120,15 @@ class GithubCacheServiceV1BaseClient(GithubCacheClient, abc.ABC):
|
|
6027
6120
|
cache_version: int = CI_CACHE_VERSION,
|
6028
6121
|
|
6029
6122
|
loop: ta.Optional[asyncio.AbstractEventLoop] = None,
|
6123
|
+
|
6124
|
+
concurrency: int = DEFAULT_CONCURRENCY,
|
6125
|
+
chunk_size: int = DEFAULT_CHUNK_SIZE,
|
6030
6126
|
) -> None:
|
6031
6127
|
super().__init__()
|
6032
6128
|
|
6033
6129
|
#
|
6034
6130
|
|
6035
|
-
|
6036
|
-
base_url = check.non_empty_str(self.BASE_URL_ENV_VAR())
|
6037
|
-
self._service_url = GithubCacheServiceV1.get_service_url(base_url)
|
6131
|
+
self._service_url = check.non_empty_str(service_url)
|
6038
6132
|
|
6039
6133
|
if auth_token is None:
|
6040
6134
|
auth_token = self.AUTH_TOKEN_ENV_VAR()
|
@@ -6056,7 +6150,16 @@ class GithubCacheServiceV1BaseClient(GithubCacheClient, abc.ABC):
|
|
6056
6150
|
|
6057
6151
|
self._given_loop = loop
|
6058
6152
|
|
6059
|
-
|
6153
|
+
#
|
6154
|
+
|
6155
|
+
check.arg(concurrency > 0)
|
6156
|
+
self._concurrency = concurrency
|
6157
|
+
|
6158
|
+
check.arg(chunk_size > 0)
|
6159
|
+
self._chunk_size = chunk_size
|
6160
|
+
|
6161
|
+
##
|
6162
|
+
# misc
|
6060
6163
|
|
6061
6164
|
def _get_loop(self) -> asyncio.AbstractEventLoop:
|
6062
6165
|
if (loop := self._given_loop) is not None:
|
@@ -6065,21 +6168,25 @@ class GithubCacheServiceV1BaseClient(GithubCacheClient, abc.ABC):
|
|
6065
6168
|
|
6066
6169
|
#
|
6067
6170
|
|
6068
|
-
def
|
6171
|
+
def _load_json_bytes(self, b: ta.Optional[bytes]) -> ta.Optional[ta.Any]:
|
6172
|
+
if not b:
|
6173
|
+
return None
|
6174
|
+
return json.loads(b.decode('utf-8-sig'))
|
6175
|
+
|
6176
|
+
##
|
6177
|
+
# requests
|
6178
|
+
|
6179
|
+
def _build_request_headers(
|
6069
6180
|
self,
|
6070
6181
|
headers: ta.Optional[ta.Mapping[str, str]] = None,
|
6071
6182
|
*,
|
6183
|
+
no_auth: bool = False,
|
6072
6184
|
content_type: ta.Optional[str] = None,
|
6073
6185
|
json_content: bool = False,
|
6074
6186
|
) -> ta.Dict[str, str]:
|
6075
|
-
dct = {
|
6076
|
-
'Accept': ';'.join([
|
6077
|
-
'application/json',
|
6078
|
-
f'api-version={GithubCacheServiceV1.API_VERSION}',
|
6079
|
-
]),
|
6080
|
-
}
|
6187
|
+
dct = {}
|
6081
6188
|
|
6082
|
-
if (auth_token := self._auth_token):
|
6189
|
+
if not no_auth and (auth_token := self._auth_token):
|
6083
6190
|
dct['Authorization'] = f'Bearer {auth_token}'
|
6084
6191
|
|
6085
6192
|
if content_type is None and json_content:
|
@@ -6094,19 +6201,13 @@ class GithubCacheServiceV1BaseClient(GithubCacheClient, abc.ABC):
|
|
6094
6201
|
|
6095
6202
|
#
|
6096
6203
|
|
6097
|
-
def
|
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(
|
6204
|
+
async def _send_urllib_request(
|
6105
6205
|
self,
|
6106
6206
|
req: urllib.request.Request,
|
6107
6207
|
) -> ta.Tuple[http.client.HTTPResponse, ta.Optional[bytes]]:
|
6108
6208
|
def run_sync():
|
6109
|
-
|
6209
|
+
opener = urllib.request.build_opener(NonRaisingUrllibErrorProcessor)
|
6210
|
+
with opener.open(req) as resp: # noqa
|
6110
6211
|
body = resp.read()
|
6111
6212
|
return (resp, body)
|
6112
6213
|
|
@@ -6122,18 +6223,32 @@ class GithubCacheServiceV1BaseClient(GithubCacheClient, abc.ABC):
|
|
6122
6223
|
def __str__(self) -> str:
|
6123
6224
|
return repr(self)
|
6124
6225
|
|
6125
|
-
async def
|
6226
|
+
async def _send_request(
|
6126
6227
|
self,
|
6127
|
-
path: str,
|
6128
6228
|
*,
|
6229
|
+
url: ta.Optional[str] = None,
|
6230
|
+
path: ta.Optional[str] = None,
|
6231
|
+
|
6129
6232
|
method: ta.Optional[str] = None,
|
6233
|
+
|
6130
6234
|
headers: ta.Optional[ta.Mapping[str, str]] = None,
|
6235
|
+
no_auth: bool = False,
|
6131
6236
|
content_type: ta.Optional[str] = None,
|
6237
|
+
|
6132
6238
|
content: ta.Optional[bytes] = None,
|
6133
6239
|
json_content: ta.Optional[ta.Any] = None,
|
6240
|
+
|
6134
6241
|
success_status_codes: ta.Optional[ta.Container[int]] = None,
|
6242
|
+
|
6243
|
+
retry_status_codes: ta.Optional[ta.Container[int]] = None,
|
6244
|
+
num_retries: int = 0,
|
6245
|
+
retry_sleep: ta.Optional[float] = None,
|
6135
6246
|
) -> ta.Optional[ta.Any]:
|
6136
|
-
url
|
6247
|
+
if url is not None and path is not None:
|
6248
|
+
raise RuntimeError('Must not pass both url and path')
|
6249
|
+
elif path is not None:
|
6250
|
+
url = f'{self._service_url}/{path}'
|
6251
|
+
url = check.non_empty_str(url)
|
6137
6252
|
|
6138
6253
|
if content is not None and json_content is not None:
|
6139
6254
|
raise RuntimeError('Must not pass both content and json_content')
|
@@ -6146,33 +6261,52 @@ class GithubCacheServiceV1BaseClient(GithubCacheClient, abc.ABC):
|
|
6146
6261
|
if method is None:
|
6147
6262
|
method = 'POST' if content is not None else 'GET'
|
6148
6263
|
|
6264
|
+
headers = self._build_request_headers(
|
6265
|
+
headers,
|
6266
|
+
no_auth=no_auth,
|
6267
|
+
content_type=content_type,
|
6268
|
+
json_content=header_json_content,
|
6269
|
+
)
|
6270
|
+
|
6149
6271
|
#
|
6150
6272
|
|
6151
|
-
|
6152
|
-
|
6153
|
-
|
6154
|
-
|
6155
|
-
headers,
|
6156
|
-
|
6157
|
-
|
6158
|
-
),
|
6159
|
-
data=content,
|
6160
|
-
)
|
6273
|
+
for n in itertools.count():
|
6274
|
+
req = urllib.request.Request( # noqa
|
6275
|
+
url,
|
6276
|
+
method=method,
|
6277
|
+
headers=headers,
|
6278
|
+
data=content,
|
6279
|
+
)
|
6161
6280
|
|
6162
|
-
|
6281
|
+
resp, body = await self._send_urllib_request(req)
|
6163
6282
|
|
6164
|
-
|
6283
|
+
#
|
6165
6284
|
|
6166
|
-
|
6167
|
-
|
6168
|
-
|
6169
|
-
|
6170
|
-
|
6171
|
-
|
6285
|
+
if success_status_codes is not None:
|
6286
|
+
is_success = resp.status in success_status_codes
|
6287
|
+
else:
|
6288
|
+
is_success = (200 <= resp.status < 300)
|
6289
|
+
if is_success:
|
6290
|
+
return self._load_json_bytes(body)
|
6291
|
+
|
6292
|
+
#
|
6172
6293
|
|
6173
|
-
|
6294
|
+
log.debug(f'Request to url {url} got unsuccessful status code {resp.status}') # noqa
|
6174
6295
|
|
6175
|
-
|
6296
|
+
if not (
|
6297
|
+
retry_status_codes is not None and
|
6298
|
+
resp.status in retry_status_codes and
|
6299
|
+
n < num_retries
|
6300
|
+
):
|
6301
|
+
raise self.ServiceRequestError(resp.status, body)
|
6302
|
+
|
6303
|
+
if retry_sleep is not None:
|
6304
|
+
await asyncio.sleep(retry_sleep)
|
6305
|
+
|
6306
|
+
raise RuntimeError('Unreachable')
|
6307
|
+
|
6308
|
+
##
|
6309
|
+
# keys
|
6176
6310
|
|
6177
6311
|
KEY_PART_SEPARATOR = '---'
|
6178
6312
|
|
@@ -6183,73 +6317,8 @@ class GithubCacheServiceV1BaseClient(GithubCacheClient, abc.ABC):
|
|
6183
6317
|
('' if partial_suffix else self._key_suffix),
|
6184
6318
|
])
|
6185
6319
|
|
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
|
-
#
|
6320
|
+
##
|
6321
|
+
# downloading
|
6253
6322
|
|
6254
6323
|
@dc.dataclass(frozen=True)
|
6255
6324
|
class _DownloadChunk:
|
@@ -6267,7 +6336,7 @@ class GithubCacheServiceV1Client(GithubCacheServiceV1BaseClient):
|
|
6267
6336
|
},
|
6268
6337
|
)
|
6269
6338
|
|
6270
|
-
_, buf_ = await self.
|
6339
|
+
_, buf_ = await self._send_urllib_request(req)
|
6271
6340
|
|
6272
6341
|
buf = check.not_none(buf_)
|
6273
6342
|
check.equal(len(buf), chunk.size)
|
@@ -6310,7 +6379,7 @@ class GithubCacheServiceV1Client(GithubCacheServiceV1BaseClient):
|
|
6310
6379
|
#
|
6311
6380
|
# status_code = check.isinstance(curl_res['response_code'], int)
|
6312
6381
|
#
|
6313
|
-
# if not (200 <= status_code
|
6382
|
+
# if not (200 <= status_code < 300):
|
6314
6383
|
# raise RuntimeError(f'Curl chunk download {chunk} failed: {curl_res}')
|
6315
6384
|
|
6316
6385
|
async def _download_file_chunk(self, chunk: _DownloadChunk) -> None:
|
@@ -6322,11 +6391,17 @@ class GithubCacheServiceV1Client(GithubCacheServiceV1BaseClient):
|
|
6322
6391
|
):
|
6323
6392
|
await self._download_file_chunk_urllib(chunk)
|
6324
6393
|
|
6325
|
-
async def
|
6326
|
-
|
6327
|
-
|
6394
|
+
async def _download_file_chunks(
|
6395
|
+
self,
|
6396
|
+
*,
|
6397
|
+
key: str,
|
6398
|
+
url: str,
|
6399
|
+
out_file: str,
|
6400
|
+
) -> None:
|
6401
|
+
check.non_empty_str(key)
|
6402
|
+
check.non_empty_str(url)
|
6328
6403
|
|
6329
|
-
head_resp, _ = await self.
|
6404
|
+
head_resp, _ = await self._send_urllib_request(urllib.request.Request( # noqa
|
6330
6405
|
url,
|
6331
6406
|
method='HEAD',
|
6332
6407
|
))
|
@@ -6355,109 +6430,281 @@ class GithubCacheServiceV1Client(GithubCacheServiceV1BaseClient):
|
|
6355
6430
|
|
6356
6431
|
await asyncio_wait_concurrent(download_tasks, self._concurrency)
|
6357
6432
|
|
6358
|
-
|
6359
|
-
|
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)
|
6433
|
+
##
|
6434
|
+
# uploading
|
6367
6435
|
|
6368
|
-
|
6436
|
+
@dc.dataclass(frozen=True)
|
6437
|
+
class _UploadChunk:
|
6438
|
+
url: str
|
6439
|
+
key: str
|
6440
|
+
in_file: str
|
6441
|
+
offset: int
|
6442
|
+
size: int
|
6369
6443
|
|
6370
|
-
|
6371
|
-
|
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)
|
6444
|
+
UPLOAD_CHUNK_NUM_RETRIES = 10
|
6445
|
+
UPLOAD_CHUNK_RETRY_SLEEP = .5
|
6386
6446
|
|
6387
|
-
|
6447
|
+
async def _upload_file_chunk_(self, chunk: _UploadChunk) -> None:
|
6448
|
+
with open(chunk.in_file, 'rb') as f: # noqa
|
6449
|
+
f.seek(chunk.offset)
|
6450
|
+
buf = f.read(chunk.size)
|
6388
6451
|
|
6389
|
-
|
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
|
-
)
|
6452
|
+
check.equal(len(buf), chunk.size)
|
6399
6453
|
|
6400
|
-
|
6401
|
-
|
6454
|
+
await self._send_request(
|
6455
|
+
url=chunk.url,
|
6402
6456
|
|
6403
|
-
|
6457
|
+
method='PATCH',
|
6404
6458
|
|
6405
|
-
|
6459
|
+
headers={
|
6460
|
+
'Content-Range': f'bytes {chunk.offset}-{chunk.offset + chunk.size - 1}/*',
|
6461
|
+
},
|
6462
|
+
no_auth=True,
|
6463
|
+
content_type='application/octet-stream',
|
6406
6464
|
|
6407
|
-
|
6465
|
+
content=buf,
|
6408
6466
|
|
6409
|
-
|
6410
|
-
|
6411
|
-
|
6412
|
-
|
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,
|
6467
|
+
success_status_codes=[204],
|
6468
|
+
|
6469
|
+
# retry_status_codes=[405],
|
6470
|
+
num_retries=self.UPLOAD_CHUNK_NUM_RETRIES,
|
6471
|
+
retry_sleep=self.UPLOAD_CHUNK_RETRY_SLEEP,
|
6422
6472
|
)
|
6423
|
-
cache_id = check.isinstance(reserve_resp.cache_id, int)
|
6424
6473
|
|
6425
|
-
|
6474
|
+
async def _upload_file_chunk(self, chunk: _UploadChunk) -> None:
|
6475
|
+
with log_timing_context(
|
6476
|
+
f'Uploading github cache {chunk.key} '
|
6477
|
+
f'file {chunk.in_file} '
|
6478
|
+
f'chunk {chunk.offset} - {chunk.offset + chunk.size}',
|
6479
|
+
):
|
6480
|
+
await self._upload_file_chunk_(chunk)
|
6481
|
+
|
6482
|
+
def _generate_file_upload_chunks(
|
6483
|
+
self,
|
6484
|
+
*,
|
6485
|
+
in_file: str,
|
6486
|
+
url: str,
|
6487
|
+
key: str,
|
6488
|
+
|
6489
|
+
file_size: ta.Optional[int] = None,
|
6490
|
+
) -> ta.List[_UploadChunk]:
|
6491
|
+
check.state(os.path.isfile(in_file))
|
6492
|
+
|
6493
|
+
if file_size is None:
|
6494
|
+
file_size = os.stat(in_file).st_size
|
6426
6495
|
|
6427
6496
|
#
|
6428
6497
|
|
6429
|
-
|
6498
|
+
upload_chunks: ta.List[BaseGithubCacheClient._UploadChunk] = []
|
6430
6499
|
chunk_size = self._chunk_size
|
6431
6500
|
for i in range((file_size // chunk_size) + (1 if file_size % chunk_size else 0)):
|
6432
6501
|
offset = i * chunk_size
|
6433
6502
|
size = min(chunk_size, file_size - offset)
|
6434
|
-
|
6435
|
-
|
6436
|
-
|
6437
|
-
in_file,
|
6438
|
-
offset,
|
6439
|
-
size,
|
6503
|
+
upload_chunks.append(self._UploadChunk(
|
6504
|
+
url=url,
|
6505
|
+
key=key,
|
6506
|
+
in_file=in_file,
|
6507
|
+
offset=offset,
|
6508
|
+
size=size,
|
6440
6509
|
))
|
6441
6510
|
|
6511
|
+
return upload_chunks
|
6512
|
+
|
6513
|
+
async def _upload_file_chunks(
|
6514
|
+
self,
|
6515
|
+
*,
|
6516
|
+
in_file: str,
|
6517
|
+
url: str,
|
6518
|
+
key: str,
|
6519
|
+
|
6520
|
+
file_size: ta.Optional[int] = None,
|
6521
|
+
) -> None:
|
6522
|
+
upload_tasks = []
|
6523
|
+
for chunk in self._generate_file_upload_chunks(
|
6524
|
+
in_file=in_file,
|
6525
|
+
url=url,
|
6526
|
+
key=key,
|
6527
|
+
file_size=file_size,
|
6528
|
+
):
|
6529
|
+
upload_tasks.append(self._upload_file_chunk(chunk))
|
6530
|
+
|
6442
6531
|
await asyncio_wait_concurrent(upload_tasks, self._concurrency)
|
6443
6532
|
|
6444
|
-
#
|
6445
6533
|
|
6446
|
-
|
6447
|
-
|
6448
|
-
|
6449
|
-
|
6450
|
-
|
6451
|
-
|
6452
|
-
success_status_codes=[204],
|
6453
|
-
)
|
6534
|
+
########################################
|
6535
|
+
# ../github/api/v2/azure.py
|
6536
|
+
"""
|
6537
|
+
TODO:
|
6538
|
+
- ominfra? no, circdep
|
6539
|
+
"""
|
6454
6540
|
|
6455
|
-
|
6456
|
-
|
6457
|
-
|
6458
|
-
|
6459
|
-
|
6460
|
-
|
6541
|
+
|
6542
|
+
##
|
6543
|
+
|
6544
|
+
|
6545
|
+
class AzureBlockBlobUploader:
|
6546
|
+
"""
|
6547
|
+
https://learn.microsoft.com/en-us/rest/api/storageservices/put-block
|
6548
|
+
https://learn.microsoft.com/en-us/rest/api/storageservices/put-block-list
|
6549
|
+
"""
|
6550
|
+
|
6551
|
+
DEFAULT_CONCURRENCY = 4
|
6552
|
+
|
6553
|
+
@dc.dataclass(frozen=True)
|
6554
|
+
class Request:
|
6555
|
+
method: str
|
6556
|
+
url: str
|
6557
|
+
headers: ta.Optional[ta.Dict[str, str]] = None
|
6558
|
+
body: ta.Optional[bytes] = None
|
6559
|
+
|
6560
|
+
@dc.dataclass(frozen=True)
|
6561
|
+
class Response:
|
6562
|
+
status: int
|
6563
|
+
headers: ta.Optional[ta.Mapping[str, str]] = None
|
6564
|
+
data: ta.Optional[bytes] = None
|
6565
|
+
|
6566
|
+
def get_header(self, name: str) -> ta.Optional[str]:
|
6567
|
+
for k, v in (self.headers or {}).items():
|
6568
|
+
if k.lower() == name.lower():
|
6569
|
+
return v
|
6570
|
+
return None
|
6571
|
+
|
6572
|
+
def __init__(
|
6573
|
+
self,
|
6574
|
+
blob_url_with_sas: str,
|
6575
|
+
make_request: ta.Callable[[Request], ta.Awaitable[Response]],
|
6576
|
+
*,
|
6577
|
+
api_version: str = '2020-10-02',
|
6578
|
+
concurrency: int = DEFAULT_CONCURRENCY,
|
6579
|
+
) -> None:
|
6580
|
+
"""
|
6581
|
+
blob_url_with_sas should be of the form:
|
6582
|
+
https://<account>.blob.core.windows.net/<container>/<blob>?<SAS-token>
|
6583
|
+
"""
|
6584
|
+
|
6585
|
+
super().__init__()
|
6586
|
+
|
6587
|
+
self._make_request = make_request
|
6588
|
+
self._api_version = api_version
|
6589
|
+
check.arg(concurrency >= 1)
|
6590
|
+
self._concurrency = concurrency
|
6591
|
+
|
6592
|
+
parsed = urllib.parse.urlparse(blob_url_with_sas)
|
6593
|
+
self._base_url = f'{parsed.scheme}://{parsed.netloc}'
|
6594
|
+
parts = parsed.path.lstrip('/').split('/', 1)
|
6595
|
+
self._container = parts[0]
|
6596
|
+
self._blob_name = parts[1]
|
6597
|
+
self._sas = parsed.query
|
6598
|
+
|
6599
|
+
def _headers(self) -> ta.Dict[str, str]:
|
6600
|
+
"""Standard headers for Azure Blob REST calls."""
|
6601
|
+
|
6602
|
+
now = datetime.datetime.now(datetime.UTC).strftime('%a, %d %b %Y %H:%M:%S GMT')
|
6603
|
+
return {
|
6604
|
+
'x-ms-date': now,
|
6605
|
+
'x-ms-version': self._api_version,
|
6606
|
+
}
|
6607
|
+
|
6608
|
+
@dc.dataclass(frozen=True)
|
6609
|
+
class FileChunk:
|
6610
|
+
in_file: str
|
6611
|
+
offset: int
|
6612
|
+
size: int
|
6613
|
+
|
6614
|
+
async def _upload_file_chunk_(
|
6615
|
+
self,
|
6616
|
+
block_id: str,
|
6617
|
+
chunk: FileChunk,
|
6618
|
+
) -> None:
|
6619
|
+
with open(chunk.in_file, 'rb') as f: # noqa
|
6620
|
+
f.seek(chunk.offset)
|
6621
|
+
data = f.read(chunk.size)
|
6622
|
+
|
6623
|
+
check.equal(len(data), chunk.size)
|
6624
|
+
|
6625
|
+
params = {
|
6626
|
+
'comp': 'block',
|
6627
|
+
'blockid': block_id,
|
6628
|
+
}
|
6629
|
+
query = self._sas + '&' + urllib.parse.urlencode(params)
|
6630
|
+
url = f'{self._base_url}/{self._container}/{self._blob_name}?{query}'
|
6631
|
+
|
6632
|
+
log.debug(f'Uploading azure blob chunk {chunk} with block id {block_id}') # noqa
|
6633
|
+
|
6634
|
+
resp = await self._make_request(self.Request(
|
6635
|
+
'PUT',
|
6636
|
+
url,
|
6637
|
+
headers=self._headers(),
|
6638
|
+
body=data,
|
6639
|
+
))
|
6640
|
+
if resp.status not in (201, 202):
|
6641
|
+
raise RuntimeError(f'Put Block failed: {block_id=} {resp.status=}')
|
6642
|
+
|
6643
|
+
async def _upload_file_chunk(
|
6644
|
+
self,
|
6645
|
+
block_id: str,
|
6646
|
+
chunk: FileChunk,
|
6647
|
+
) -> None:
|
6648
|
+
with log_timing_context(f'Uploading azure blob chunk {chunk} with block id {block_id}'):
|
6649
|
+
await self._upload_file_chunk_(
|
6650
|
+
block_id,
|
6651
|
+
chunk,
|
6652
|
+
)
|
6653
|
+
|
6654
|
+
async def upload_file(
|
6655
|
+
self,
|
6656
|
+
chunks: ta.List[FileChunk],
|
6657
|
+
) -> ta.Dict[str, ta.Any]:
|
6658
|
+
block_ids = []
|
6659
|
+
|
6660
|
+
# 1) Stage each block
|
6661
|
+
upload_tasks = []
|
6662
|
+
for idx, chunk in enumerate(chunks):
|
6663
|
+
# Generate a predictable block ID (must be URL-safe base64)
|
6664
|
+
raw_id = f'{idx:08d}'.encode()
|
6665
|
+
block_id = base64.b64encode(raw_id).decode('utf-8')
|
6666
|
+
block_ids.append(block_id)
|
6667
|
+
|
6668
|
+
upload_tasks.append(self._upload_file_chunk(
|
6669
|
+
block_id,
|
6670
|
+
chunk,
|
6671
|
+
))
|
6672
|
+
|
6673
|
+
await asyncio_wait_concurrent(upload_tasks, self._concurrency)
|
6674
|
+
|
6675
|
+
# 2) Commit block list
|
6676
|
+
root = ET.Element('BlockList')
|
6677
|
+
for bid in block_ids:
|
6678
|
+
elm = ET.SubElement(root, 'Latest')
|
6679
|
+
elm.text = bid
|
6680
|
+
body = ET.tostring(root, encoding='utf-8', method='xml')
|
6681
|
+
|
6682
|
+
params = {'comp': 'blocklist'}
|
6683
|
+
query = self._sas + '&' + urllib.parse.urlencode(params)
|
6684
|
+
url = f'{self._base_url}/{self._container}/{self._blob_name}?{query}'
|
6685
|
+
|
6686
|
+
log.debug(f'Putting azure blob chunk list block ids {block_ids}') # noqa
|
6687
|
+
|
6688
|
+
resp = await self._make_request(self.Request(
|
6689
|
+
'PUT',
|
6690
|
+
url,
|
6691
|
+
headers={
|
6692
|
+
**self._headers(),
|
6693
|
+
'Content-Type': 'application/xml',
|
6694
|
+
},
|
6695
|
+
body=body,
|
6696
|
+
))
|
6697
|
+
if resp.status not in (200, 201):
|
6698
|
+
raise RuntimeError(f'Put Block List failed: {resp.status} {resp.data!r}')
|
6699
|
+
|
6700
|
+
ret = {
|
6701
|
+
'status_code': resp.status,
|
6702
|
+
'etag': resp.get_header('ETag'),
|
6703
|
+
}
|
6704
|
+
|
6705
|
+
log.debug(f'Uploaded azure blob chunk {ret}') # noqa
|
6706
|
+
|
6707
|
+
return ret
|
6461
6708
|
|
6462
6709
|
|
6463
6710
|
########################################
|
@@ -7708,119 +7955,351 @@ def subprocess_maybe_shell_wrap_exec(*cmd: str) -> ta.Tuple[str, ...]:
|
|
7708
7955
|
|
7709
7956
|
|
7710
7957
|
########################################
|
7711
|
-
# ../github/
|
7958
|
+
# ../github/api/v1/client.py
|
7712
7959
|
|
7713
7960
|
|
7714
7961
|
##
|
7715
7962
|
|
7716
7963
|
|
7717
|
-
class
|
7718
|
-
|
7719
|
-
class Config:
|
7720
|
-
pass
|
7964
|
+
class GithubCacheServiceV1Client(BaseGithubCacheClient):
|
7965
|
+
BASE_URL_ENV_VAR = register_github_env_var('ACTIONS_CACHE_URL')
|
7721
7966
|
|
7722
7967
|
def __init__(
|
7723
7968
|
self,
|
7724
|
-
config: Config = Config(),
|
7725
7969
|
*,
|
7726
|
-
|
7727
|
-
version: ta.Optional[CacheVersion] = None,
|
7970
|
+
base_url: ta.Optional[str] = None,
|
7728
7971
|
|
7729
|
-
|
7972
|
+
**kwargs: ta.Any,
|
7730
7973
|
) -> None:
|
7974
|
+
if base_url is None:
|
7975
|
+
base_url = check.non_empty_str(self.BASE_URL_ENV_VAR())
|
7976
|
+
service_url = GithubCacheServiceV1.get_service_url(base_url)
|
7977
|
+
|
7731
7978
|
super().__init__(
|
7732
|
-
|
7979
|
+
service_url=service_url,
|
7980
|
+
**kwargs,
|
7733
7981
|
)
|
7734
7982
|
|
7735
|
-
|
7983
|
+
#
|
7736
7984
|
|
7737
|
-
|
7738
|
-
|
7739
|
-
|
7740
|
-
|
7741
|
-
|
7985
|
+
def _build_request_headers(
|
7986
|
+
self,
|
7987
|
+
headers: ta.Optional[ta.Mapping[str, str]] = None,
|
7988
|
+
**kwargs: ta.Any,
|
7989
|
+
) -> ta.Dict[str, str]:
|
7990
|
+
return super()._build_request_headers(
|
7991
|
+
{
|
7992
|
+
'Accept': ';'.join([
|
7993
|
+
'application/json',
|
7994
|
+
f'api-version={GithubCacheServiceV1.API_VERSION}',
|
7995
|
+
]),
|
7996
|
+
**(headers or {}),
|
7997
|
+
},
|
7998
|
+
**kwargs,
|
7999
|
+
)
|
7742
8000
|
|
7743
|
-
|
8001
|
+
#
|
8002
|
+
|
8003
|
+
@dc.dataclass(frozen=True)
|
8004
|
+
class Entry(GithubCacheClient.Entry):
|
8005
|
+
artifact: GithubCacheServiceV1.ArtifactCacheEntry
|
8006
|
+
|
8007
|
+
def get_entry_url(self, entry: GithubCacheClient.Entry) -> ta.Optional[str]:
|
8008
|
+
entry1 = check.isinstance(entry, self.Entry)
|
8009
|
+
return entry1.artifact.archive_location
|
7744
8010
|
|
7745
8011
|
#
|
7746
8012
|
|
7747
|
-
|
7748
|
-
|
7749
|
-
|
7750
|
-
|
8013
|
+
def _build_get_entry_url_path(self, *keys: str) -> str:
|
8014
|
+
qp = dict(
|
8015
|
+
keys=','.join(urllib.parse.quote_plus(k) for k in keys),
|
8016
|
+
version=str(self._cache_version),
|
8017
|
+
)
|
7751
8018
|
|
7752
|
-
|
8019
|
+
return '?'.join([
|
8020
|
+
'cache',
|
8021
|
+
'&'.join([
|
8022
|
+
f'{k}={v}'
|
8023
|
+
for k, v in qp.items()
|
8024
|
+
]),
|
8025
|
+
])
|
8026
|
+
|
8027
|
+
GET_ENTRY_SUCCESS_STATUS_CODES = (200, 204)
|
8028
|
+
|
8029
|
+
#
|
8030
|
+
|
8031
|
+
async def get_entry(self, key: str) -> ta.Optional[GithubCacheClient.Entry]:
|
8032
|
+
obj = await self._send_request(
|
8033
|
+
path=self._build_get_entry_url_path(self.fix_key(key, partial_suffix=True)),
|
8034
|
+
)
|
8035
|
+
if obj is None:
|
7753
8036
|
return None
|
7754
8037
|
|
7755
|
-
|
7756
|
-
|
7757
|
-
|
8038
|
+
return self.Entry(GithubCacheServiceV1.dataclass_from_json(
|
8039
|
+
GithubCacheServiceV1.ArtifactCacheEntry,
|
8040
|
+
obj,
|
8041
|
+
))
|
7758
8042
|
|
7759
|
-
|
8043
|
+
#
|
7760
8044
|
|
7761
|
-
|
8045
|
+
async def download_file(self, entry: GithubCacheClient.Entry, out_file: str) -> None:
|
8046
|
+
entry1 = check.isinstance(entry, self.Entry)
|
8047
|
+
with log_timing_context(
|
8048
|
+
'Downloading github cache '
|
8049
|
+
f'key {entry1.artifact.cache_key} '
|
8050
|
+
f'version {entry1.artifact.cache_version} '
|
8051
|
+
f'to {out_file}',
|
8052
|
+
):
|
8053
|
+
await self._download_file_chunks(
|
8054
|
+
key=check.non_empty_str(entry1.artifact.cache_key),
|
8055
|
+
url=check.non_empty_str(entry1.artifact.archive_location),
|
8056
|
+
out_file=out_file,
|
8057
|
+
)
|
7762
8058
|
|
7763
|
-
|
8059
|
+
#
|
8060
|
+
|
8061
|
+
async def _upload_file(self, key: str, in_file: str) -> None:
|
8062
|
+
fixed_key = self.fix_key(key)
|
8063
|
+
|
8064
|
+
check.state(os.path.isfile(in_file))
|
8065
|
+
file_size = os.stat(in_file).st_size
|
8066
|
+
|
8067
|
+
#
|
8068
|
+
|
8069
|
+
reserve_req = GithubCacheServiceV1.ReserveCacheRequest(
|
8070
|
+
key=fixed_key,
|
8071
|
+
cache_size=file_size,
|
8072
|
+
version=str(self._cache_version),
|
8073
|
+
)
|
8074
|
+
reserve_resp_obj = await self._send_request(
|
8075
|
+
path='caches',
|
8076
|
+
json_content=GithubCacheServiceV1.dataclass_to_json(reserve_req),
|
8077
|
+
success_status_codes=[201],
|
8078
|
+
)
|
8079
|
+
reserve_resp = GithubCacheServiceV1.dataclass_from_json( # noqa
|
8080
|
+
GithubCacheServiceV1.ReserveCacheResponse,
|
8081
|
+
reserve_resp_obj,
|
8082
|
+
)
|
8083
|
+
cache_id = check.isinstance(reserve_resp.cache_id, int)
|
8084
|
+
|
8085
|
+
log.debug(f'Github cache file {os.path.basename(in_file)} got id {cache_id}') # noqa
|
8086
|
+
|
8087
|
+
#
|
8088
|
+
|
8089
|
+
url = f'{self._service_url}/caches/{cache_id}'
|
8090
|
+
|
8091
|
+
await self._upload_file_chunks(
|
8092
|
+
in_file=in_file,
|
8093
|
+
url=url,
|
8094
|
+
key=fixed_key,
|
8095
|
+
file_size=file_size,
|
8096
|
+
)
|
8097
|
+
|
8098
|
+
#
|
8099
|
+
|
8100
|
+
commit_req = GithubCacheServiceV1.CommitCacheRequest(
|
8101
|
+
size=file_size,
|
8102
|
+
)
|
8103
|
+
await self._send_request(
|
8104
|
+
path=f'caches/{cache_id}',
|
8105
|
+
json_content=GithubCacheServiceV1.dataclass_to_json(commit_req),
|
8106
|
+
success_status_codes=[204],
|
8107
|
+
)
|
8108
|
+
|
8109
|
+
async def upload_file(self, key: str, in_file: str) -> None:
|
8110
|
+
with log_timing_context(
|
8111
|
+
f'Uploading github cache file {os.path.basename(in_file)} '
|
8112
|
+
f'key {key}',
|
8113
|
+
):
|
8114
|
+
await self._upload_file(key, in_file)
|
8115
|
+
|
8116
|
+
|
8117
|
+
########################################
|
8118
|
+
# ../github/api/v2/client.py
|
8119
|
+
|
8120
|
+
|
8121
|
+
##
|
8122
|
+
|
8123
|
+
|
8124
|
+
class GithubCacheServiceV2Client(BaseGithubCacheClient):
|
8125
|
+
BASE_URL_ENV_VAR = register_github_env_var('ACTIONS_RESULTS_URL')
|
8126
|
+
|
8127
|
+
def __init__(
|
7764
8128
|
self,
|
7765
|
-
key: str,
|
7766
|
-
file_path: str,
|
7767
8129
|
*,
|
7768
|
-
|
7769
|
-
|
7770
|
-
|
7771
|
-
|
7772
|
-
|
7773
|
-
|
8130
|
+
base_url: ta.Optional[str] = None,
|
8131
|
+
|
8132
|
+
**kwargs: ta.Any,
|
8133
|
+
) -> None:
|
8134
|
+
if base_url is None:
|
8135
|
+
base_url = check.non_empty_str(self.BASE_URL_ENV_VAR())
|
8136
|
+
service_url = GithubCacheServiceV2.get_service_url(base_url)
|
8137
|
+
|
8138
|
+
super().__init__(
|
8139
|
+
service_url=service_url,
|
8140
|
+
**kwargs,
|
7774
8141
|
)
|
7775
8142
|
|
7776
|
-
|
8143
|
+
#
|
7777
8144
|
|
7778
|
-
|
8145
|
+
async def _send_method_request(
|
8146
|
+
self,
|
8147
|
+
method: GithubCacheServiceV2.Method[
|
8148
|
+
GithubCacheServiceV2RequestT,
|
8149
|
+
GithubCacheServiceV2ResponseT,
|
8150
|
+
],
|
8151
|
+
request: GithubCacheServiceV2RequestT,
|
8152
|
+
**kwargs: ta.Any,
|
8153
|
+
) -> ta.Optional[GithubCacheServiceV2ResponseT]:
|
8154
|
+
obj = await self._send_request(
|
8155
|
+
path=method.name,
|
8156
|
+
json_content=dc.asdict(request), # type: ignore[call-overload]
|
8157
|
+
**kwargs,
|
8158
|
+
)
|
8159
|
+
|
8160
|
+
if obj is None:
|
8161
|
+
return None
|
8162
|
+
return method.response(**obj)
|
7779
8163
|
|
7780
8164
|
#
|
7781
8165
|
|
7782
|
-
|
7783
|
-
|
7784
|
-
|
7785
|
-
|
8166
|
+
@dc.dataclass(frozen=True)
|
8167
|
+
class Entry(GithubCacheClient.Entry):
|
8168
|
+
request: GithubCacheServiceV2.GetCacheEntryDownloadUrlRequest
|
8169
|
+
response: GithubCacheServiceV2.GetCacheEntryDownloadUrlResponse
|
7786
8170
|
|
7787
|
-
|
8171
|
+
def __post_init__(self) -> None:
|
8172
|
+
check.state(self.response.ok)
|
8173
|
+
check.non_empty_str(self.response.signed_download_url)
|
8174
|
+
|
8175
|
+
def get_entry_url(self, entry: GithubCacheClient.Entry) -> ta.Optional[str]:
|
8176
|
+
entry2 = check.isinstance(entry, self.Entry)
|
8177
|
+
return check.non_empty_str(entry2.response.signed_download_url)
|
8178
|
+
|
8179
|
+
#
|
8180
|
+
|
8181
|
+
async def get_entry(self, key: str) -> ta.Optional[GithubCacheClient.Entry]:
|
8182
|
+
version = str(self._cache_version).zfill(GithubCacheServiceV2.VERSION_LENGTH)
|
8183
|
+
|
8184
|
+
req = GithubCacheServiceV2.GetCacheEntryDownloadUrlRequest(
|
8185
|
+
key=self.fix_key(key),
|
8186
|
+
restore_keys=[self.fix_key(key, partial_suffix=True)],
|
8187
|
+
version=version,
|
8188
|
+
)
|
8189
|
+
|
8190
|
+
resp = await self._send_method_request(
|
8191
|
+
GithubCacheServiceV2.GET_CACHE_ENTRY_DOWNLOAD_URL_METHOD,
|
8192
|
+
req,
|
8193
|
+
)
|
8194
|
+
if resp is None or not resp.ok:
|
7788
8195
|
return None
|
7789
8196
|
|
7790
|
-
return
|
8197
|
+
return self.Entry(
|
8198
|
+
request=req,
|
8199
|
+
response=resp,
|
8200
|
+
)
|
7791
8201
|
|
7792
|
-
|
7793
|
-
await FileCacheDataCache(self).put_data(key, data)
|
8202
|
+
#
|
7794
8203
|
|
8204
|
+
async def download_file(self, entry: GithubCacheClient.Entry, out_file: str) -> None:
|
8205
|
+
entry2 = check.isinstance(entry, self.Entry)
|
8206
|
+
with log_timing_context(
|
8207
|
+
'Downloading github cache '
|
8208
|
+
f'key {entry2.response.matched_key} '
|
8209
|
+
f'version {entry2.request.version} '
|
8210
|
+
f'to {out_file}',
|
8211
|
+
):
|
8212
|
+
await self._download_file_chunks(
|
8213
|
+
key=check.non_empty_str(entry2.response.matched_key),
|
8214
|
+
url=check.non_empty_str(entry2.response.signed_download_url),
|
8215
|
+
out_file=out_file,
|
8216
|
+
)
|
7795
8217
|
|
7796
|
-
|
7797
|
-
# ../github/cli.py
|
7798
|
-
"""
|
7799
|
-
See:
|
7800
|
-
- https://docs.github.com/en/rest/actions/cache?apiVersion=2022-11-28
|
7801
|
-
"""
|
8218
|
+
#
|
7802
8219
|
|
8220
|
+
async def _upload_file(self, key: str, in_file: str) -> None:
|
8221
|
+
fixed_key = self.fix_key(key)
|
7803
8222
|
|
7804
|
-
|
7805
|
-
|
7806
|
-
def list_referenced_env_vars(self) -> None:
|
7807
|
-
print('\n'.join(sorted(ev.k for ev in GITHUB_ENV_VARS)))
|
8223
|
+
check.state(os.path.isfile(in_file))
|
8224
|
+
file_size = os.stat(in_file).st_size
|
7808
8225
|
|
7809
|
-
|
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
|
8226
|
+
#
|
7818
8227
|
|
7819
|
-
|
7820
|
-
|
7821
|
-
|
7822
|
-
|
7823
|
-
|
8228
|
+
version = str(self._cache_version).zfill(GithubCacheServiceV2.VERSION_LENGTH)
|
8229
|
+
|
8230
|
+
reserve_resp = check.not_none(await self._send_method_request(
|
8231
|
+
GithubCacheServiceV2.CREATE_CACHE_ENTRY_METHOD, # type: ignore[arg-type]
|
8232
|
+
GithubCacheServiceV2.CreateCacheEntryRequest(
|
8233
|
+
key=fixed_key,
|
8234
|
+
version=version,
|
8235
|
+
),
|
8236
|
+
))
|
8237
|
+
check.state(reserve_resp.ok)
|
8238
|
+
|
8239
|
+
log.debug(f'Github cache file {os.path.basename(in_file)} upload reserved for file size {file_size}') # noqa
|
8240
|
+
|
8241
|
+
#
|
8242
|
+
|
8243
|
+
upload_chunks = self._generate_file_upload_chunks(
|
8244
|
+
in_file=in_file,
|
8245
|
+
url=reserve_resp.signed_upload_url,
|
8246
|
+
key=fixed_key,
|
8247
|
+
file_size=file_size,
|
8248
|
+
)
|
8249
|
+
|
8250
|
+
az_chunks = [
|
8251
|
+
AzureBlockBlobUploader.FileChunk(
|
8252
|
+
in_file=in_file,
|
8253
|
+
offset=c.offset,
|
8254
|
+
size=c.size,
|
8255
|
+
)
|
8256
|
+
for c in upload_chunks
|
8257
|
+
]
|
8258
|
+
|
8259
|
+
async def az_make_request(req: AzureBlockBlobUploader.Request) -> AzureBlockBlobUploader.Response:
|
8260
|
+
u_req = urllib.request.Request( # noqa
|
8261
|
+
req.url,
|
8262
|
+
method=req.method,
|
8263
|
+
headers=req.headers or {},
|
8264
|
+
data=req.body,
|
8265
|
+
)
|
8266
|
+
|
8267
|
+
u_resp, u_body = await self._send_urllib_request(u_req)
|
8268
|
+
|
8269
|
+
return AzureBlockBlobUploader.Response(
|
8270
|
+
status=u_resp.status,
|
8271
|
+
headers=dict(u_resp.headers),
|
8272
|
+
data=u_body,
|
8273
|
+
)
|
8274
|
+
|
8275
|
+
az_uploader = AzureBlockBlobUploader(
|
8276
|
+
reserve_resp.signed_upload_url,
|
8277
|
+
az_make_request,
|
8278
|
+
concurrency=self._concurrency,
|
8279
|
+
)
|
8280
|
+
|
8281
|
+
await az_uploader.upload_file(az_chunks)
|
8282
|
+
|
8283
|
+
#
|
8284
|
+
|
8285
|
+
commit_resp = check.not_none(await self._send_method_request(
|
8286
|
+
GithubCacheServiceV2.FINALIZE_CACHE_ENTRY_METHOD, # type: ignore[arg-type]
|
8287
|
+
GithubCacheServiceV2.FinalizeCacheEntryUploadRequest(
|
8288
|
+
key=fixed_key,
|
8289
|
+
size_bytes=file_size,
|
8290
|
+
version=version,
|
8291
|
+
),
|
8292
|
+
))
|
8293
|
+
check.state(commit_resp.ok)
|
8294
|
+
|
8295
|
+
log.debug(f'Github cache file {os.path.basename(in_file)} upload complete, entry id {commit_resp.entry_id}') # noqa
|
8296
|
+
|
8297
|
+
async def upload_file(self, key: str, in_file: str) -> None:
|
8298
|
+
with log_timing_context(
|
8299
|
+
f'Uploading github cache file {os.path.basename(in_file)} '
|
8300
|
+
f'key {key}',
|
8301
|
+
):
|
8302
|
+
await self._upload_file(key, in_file)
|
7824
8303
|
|
7825
8304
|
|
7826
8305
|
########################################
|
@@ -9619,20 +10098,129 @@ async def build_cache_served_docker_image_data_server_routes(
|
|
9619
10098
|
|
9620
10099
|
|
9621
10100
|
########################################
|
9622
|
-
# ../github/
|
10101
|
+
# ../github/cache.py
|
9623
10102
|
|
9624
10103
|
|
9625
10104
|
##
|
9626
10105
|
|
9627
10106
|
|
9628
|
-
|
9629
|
-
|
9630
|
-
|
9631
|
-
|
9632
|
-
inj.bind(FileCache, to_key=GithubCache),
|
9633
|
-
]
|
10107
|
+
class GithubCache(FileCache, DataCache):
|
10108
|
+
@dc.dataclass(frozen=True)
|
10109
|
+
class Config:
|
10110
|
+
pass
|
9634
10111
|
|
9635
|
-
|
10112
|
+
DEFAULT_CLIENT_VERSION: ta.ClassVar[int] = 2
|
10113
|
+
|
10114
|
+
DEFAULT_CLIENTS_BY_VERSION: ta.ClassVar[ta.Mapping[int, ta.Callable[..., GithubCacheClient]]] = {
|
10115
|
+
1: GithubCacheServiceV1Client,
|
10116
|
+
2: GithubCacheServiceV2Client,
|
10117
|
+
}
|
10118
|
+
|
10119
|
+
def __init__(
|
10120
|
+
self,
|
10121
|
+
config: Config = Config(),
|
10122
|
+
*,
|
10123
|
+
client: ta.Optional[GithubCacheClient] = None,
|
10124
|
+
default_client_version: ta.Optional[int] = None,
|
10125
|
+
|
10126
|
+
version: ta.Optional[CacheVersion] = None,
|
10127
|
+
|
10128
|
+
local: DirectoryFileCache,
|
10129
|
+
) -> None:
|
10130
|
+
super().__init__(
|
10131
|
+
version=version,
|
10132
|
+
)
|
10133
|
+
|
10134
|
+
self._config = config
|
10135
|
+
|
10136
|
+
if client is None:
|
10137
|
+
client_cls = self.DEFAULT_CLIENTS_BY_VERSION[default_client_version or self.DEFAULT_CLIENT_VERSION]
|
10138
|
+
client = client_cls(
|
10139
|
+
cache_version=self._version,
|
10140
|
+
)
|
10141
|
+
self._client: GithubCacheClient = client
|
10142
|
+
|
10143
|
+
self._local = local
|
10144
|
+
|
10145
|
+
#
|
10146
|
+
|
10147
|
+
async def get_file(self, key: str) -> ta.Optional[str]:
|
10148
|
+
local_file = self._local.get_cache_file_path(key)
|
10149
|
+
if os.path.exists(local_file):
|
10150
|
+
return local_file
|
10151
|
+
|
10152
|
+
if (entry := await self._client.get_entry(key)) is None:
|
10153
|
+
return None
|
10154
|
+
|
10155
|
+
tmp_file = self._local.format_incomplete_file(local_file)
|
10156
|
+
with unlinking_if_exists(tmp_file):
|
10157
|
+
await self._client.download_file(entry, tmp_file)
|
10158
|
+
|
10159
|
+
os.replace(tmp_file, local_file)
|
10160
|
+
|
10161
|
+
return local_file
|
10162
|
+
|
10163
|
+
async def put_file(
|
10164
|
+
self,
|
10165
|
+
key: str,
|
10166
|
+
file_path: str,
|
10167
|
+
*,
|
10168
|
+
steal: bool = False,
|
10169
|
+
) -> str:
|
10170
|
+
cache_file_path = await self._local.put_file(
|
10171
|
+
key,
|
10172
|
+
file_path,
|
10173
|
+
steal=steal,
|
10174
|
+
)
|
10175
|
+
|
10176
|
+
await self._client.upload_file(key, cache_file_path)
|
10177
|
+
|
10178
|
+
return cache_file_path
|
10179
|
+
|
10180
|
+
#
|
10181
|
+
|
10182
|
+
async def get_data(self, key: str) -> ta.Optional[DataCache.Data]:
|
10183
|
+
local_file = self._local.get_cache_file_path(key)
|
10184
|
+
if os.path.exists(local_file):
|
10185
|
+
return DataCache.FileData(local_file)
|
10186
|
+
|
10187
|
+
if (entry := await self._client.get_entry(key)) is None:
|
10188
|
+
return None
|
10189
|
+
|
10190
|
+
return DataCache.UrlData(check.non_empty_str(self._client.get_entry_url(entry)))
|
10191
|
+
|
10192
|
+
async def put_data(self, key: str, data: DataCache.Data) -> None:
|
10193
|
+
await FileCacheDataCache(self).put_data(key, data)
|
10194
|
+
|
10195
|
+
|
10196
|
+
########################################
|
10197
|
+
# ../github/cli.py
|
10198
|
+
"""
|
10199
|
+
See:
|
10200
|
+
- https://docs.github.com/en/rest/actions/cache?apiVersion=2022-11-28
|
10201
|
+
"""
|
10202
|
+
|
10203
|
+
|
10204
|
+
class GithubCli(ArgparseCli):
|
10205
|
+
@argparse_cmd()
|
10206
|
+
def list_referenced_env_vars(self) -> None:
|
10207
|
+
print('\n'.join(sorted(ev.k for ev in GITHUB_ENV_VARS)))
|
10208
|
+
|
10209
|
+
@argparse_cmd(
|
10210
|
+
argparse_arg('key'),
|
10211
|
+
)
|
10212
|
+
async def get_cache_entry(self) -> None:
|
10213
|
+
client = GithubCacheServiceV1Client()
|
10214
|
+
entry = await client.get_entry(self.args.key)
|
10215
|
+
if entry is None:
|
10216
|
+
return
|
10217
|
+
print(json_dumps_pretty(dc.asdict(entry))) # noqa
|
10218
|
+
|
10219
|
+
@argparse_cmd(
|
10220
|
+
argparse_arg('repository-id'),
|
10221
|
+
)
|
10222
|
+
def list_cache_entries(self) -> None:
|
10223
|
+
raise NotImplementedError
|
9636
10224
|
|
9637
10225
|
|
9638
10226
|
########################################
|
@@ -10325,6 +10913,23 @@ subprocesses = Subprocesses()
|
|
10325
10913
|
SubprocessRun._DEFAULT_SUBPROCESSES = subprocesses # noqa
|
10326
10914
|
|
10327
10915
|
|
10916
|
+
########################################
|
10917
|
+
# ../github/inject.py
|
10918
|
+
|
10919
|
+
|
10920
|
+
##
|
10921
|
+
|
10922
|
+
|
10923
|
+
def bind_github() -> InjectorBindings:
|
10924
|
+
lst: ta.List[InjectorBindingOrBindings] = [
|
10925
|
+
inj.bind(GithubCache, singleton=True),
|
10926
|
+
inj.bind(DataCache, to_key=GithubCache),
|
10927
|
+
inj.bind(FileCache, to_key=GithubCache),
|
10928
|
+
]
|
10929
|
+
|
10930
|
+
return inj.as_bindings(*lst)
|
10931
|
+
|
10932
|
+
|
10328
10933
|
########################################
|
10329
10934
|
# ../requirements.py
|
10330
10935
|
"""
|