cafs-cache-cdn-client 1.0.5__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.
- cafs_cache_cdn_client/__init__.py +1 -0
- cafs_cache_cdn_client/cafs/README.md +63 -0
- cafs_cache_cdn_client/cafs/__init__.py +7 -0
- cafs_cache_cdn_client/cafs/blob/__init__.py +0 -0
- cafs_cache_cdn_client/cafs/blob/hash_.py +34 -0
- cafs_cache_cdn_client/cafs/blob/package.py +198 -0
- cafs_cache_cdn_client/cafs/blob/utils.py +37 -0
- cafs_cache_cdn_client/cafs/client.py +535 -0
- cafs_cache_cdn_client/cafs/exceptions.py +30 -0
- cafs_cache_cdn_client/cafs/types.py +19 -0
- cafs_cache_cdn_client/client.py +142 -0
- cafs_cache_cdn_client/file_utils.py +96 -0
- cafs_cache_cdn_client/repo/__init__.py +3 -0
- cafs_cache_cdn_client/repo/client.py +102 -0
- cafs_cache_cdn_client/repo/datatypes.py +34 -0
- cafs_cache_cdn_client-1.0.5.dist-info/METADATA +99 -0
- cafs_cache_cdn_client-1.0.5.dist-info/RECORD +18 -0
- cafs_cache_cdn_client-1.0.5.dist-info/WHEEL +4 -0
@@ -0,0 +1,142 @@
|
|
1
|
+
import asyncio
|
2
|
+
import functools
|
3
|
+
from collections.abc import Awaitable, Callable
|
4
|
+
from os.path import normpath
|
5
|
+
from pathlib import Path
|
6
|
+
from typing import Any, Self, TypeVar
|
7
|
+
|
8
|
+
import aiofiles.os as aio_os
|
9
|
+
|
10
|
+
from cafs_cache_cdn_client.cafs import CAFSClient
|
11
|
+
from cafs_cache_cdn_client.file_utils import (
|
12
|
+
LocalFile,
|
13
|
+
compare_file_lists,
|
14
|
+
set_file_stat,
|
15
|
+
walk,
|
16
|
+
)
|
17
|
+
from cafs_cache_cdn_client.repo import RepoClient
|
18
|
+
|
19
|
+
__all__ = ('CacheCdnClient',)
|
20
|
+
|
21
|
+
|
22
|
+
CAFS_SERVER_ROOT = '/cache'
|
23
|
+
|
24
|
+
|
25
|
+
T = TypeVar('T')
|
26
|
+
|
27
|
+
|
28
|
+
def needs_cafs_client(func: Callable[..., Awaitable[T]]) -> Callable[..., Awaitable[T]]:
|
29
|
+
@functools.wraps(func)
|
30
|
+
async def wrapper(self: 'CacheCdnClient', *args: Any, **kwargs: Any) -> T:
|
31
|
+
await self._init_cafs_client()
|
32
|
+
return await func(self, *args, **kwargs)
|
33
|
+
|
34
|
+
return wrapper
|
35
|
+
|
36
|
+
|
37
|
+
class CacheCdnClient:
|
38
|
+
_cafs_client: CAFSClient | None = None
|
39
|
+
_repo_client: RepoClient
|
40
|
+
|
41
|
+
__connection_per_cafs_server: int
|
42
|
+
__cafs_client_lock = asyncio.Lock()
|
43
|
+
|
44
|
+
def __init__(self, server: str, connection_per_cafs_server: int = 1) -> None:
|
45
|
+
self._repo_client = RepoClient(server)
|
46
|
+
self.__connection_per_cafs_server = connection_per_cafs_server
|
47
|
+
|
48
|
+
async def _init_cafs_client(self) -> None:
|
49
|
+
async with self.__cafs_client_lock:
|
50
|
+
if self._cafs_client:
|
51
|
+
return
|
52
|
+
blob_urls = await self._repo_client.get_blob_urls()
|
53
|
+
self._cafs_client = await CAFSClient(
|
54
|
+
CAFS_SERVER_ROOT,
|
55
|
+
blob_urls,
|
56
|
+
connection_per_server=self.__connection_per_cafs_server,
|
57
|
+
).__aenter__()
|
58
|
+
|
59
|
+
@needs_cafs_client
|
60
|
+
async def push(
|
61
|
+
self,
|
62
|
+
repo: str,
|
63
|
+
ref: str,
|
64
|
+
directory: Path | str,
|
65
|
+
ttl_hours: int = 0,
|
66
|
+
comment: str | None = None,
|
67
|
+
) -> None:
|
68
|
+
if isinstance(directory, str):
|
69
|
+
directory = Path(directory)
|
70
|
+
if not directory.is_dir():
|
71
|
+
raise ValueError(f'{directory} is not a directory')
|
72
|
+
files = walk(directory)
|
73
|
+
hashes = await self._cafs_client.stream_batch(
|
74
|
+
[directory / file.path for file in files]
|
75
|
+
)
|
76
|
+
await self._repo_client.post_ref_info(
|
77
|
+
repo,
|
78
|
+
ref,
|
79
|
+
{
|
80
|
+
'archive': False,
|
81
|
+
'ttl': ttl_hours * 60 * 60 * 10**9,
|
82
|
+
'comment': comment,
|
83
|
+
'files': [
|
84
|
+
{
|
85
|
+
'blob': blob,
|
86
|
+
'path': file.path.as_posix(),
|
87
|
+
'mtime': file.mtime,
|
88
|
+
'mode': file.mode,
|
89
|
+
}
|
90
|
+
for blob, file in zip(hashes, files)
|
91
|
+
],
|
92
|
+
},
|
93
|
+
)
|
94
|
+
|
95
|
+
async def check(self, repo: str, ref: str) -> bool:
|
96
|
+
return await self._repo_client.is_ref_exist(repo, ref)
|
97
|
+
|
98
|
+
async def delete(self, repo: str, ref: str) -> None:
|
99
|
+
await self._repo_client.delete_ref(repo, ref)
|
100
|
+
|
101
|
+
async def attach(self, repo: str, ref: str, file_path: Path) -> None:
|
102
|
+
await self._repo_client.attach_file(repo, ref, file_path)
|
103
|
+
|
104
|
+
@needs_cafs_client
|
105
|
+
async def pull(self, repo: str, ref: str, directory: Path | str) -> None:
|
106
|
+
if isinstance(directory, str):
|
107
|
+
directory = Path(directory)
|
108
|
+
await aio_os.makedirs(directory, exist_ok=True)
|
109
|
+
ref_info = await self._repo_client.get_ref_info(repo, ref)
|
110
|
+
remote_files = [
|
111
|
+
LocalFile(
|
112
|
+
path=Path(normpath(file['path'])),
|
113
|
+
mtime=file['mtime'],
|
114
|
+
mode=file['mode'],
|
115
|
+
blob=file['blob'],
|
116
|
+
)
|
117
|
+
for file in ref_info['files']
|
118
|
+
]
|
119
|
+
local_files = walk(directory)
|
120
|
+
to_remove, to_add, to_update = await compare_file_lists(
|
121
|
+
local_files, remote_files, directory
|
122
|
+
)
|
123
|
+
for file in to_remove:
|
124
|
+
await aio_os.unlink(directory / file.path)
|
125
|
+
if to_add:
|
126
|
+
await self._cafs_client.pull_batch(
|
127
|
+
[(file.blob, directory / file.path) for file in to_add]
|
128
|
+
)
|
129
|
+
for file in to_add + to_update:
|
130
|
+
set_file_stat(file, directory)
|
131
|
+
|
132
|
+
async def tag(self, repo: str, ref: str, tag: str) -> None:
|
133
|
+
await self._repo_client.tag_ref(repo, ref, tag)
|
134
|
+
|
135
|
+
async def __aenter__(self) -> Self:
|
136
|
+
return self
|
137
|
+
|
138
|
+
async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
139
|
+
async with self.__cafs_client_lock:
|
140
|
+
if not self._cafs_client:
|
141
|
+
return
|
142
|
+
await self._cafs_client.__aexit__(exc_type, exc_val, exc_tb)
|
@@ -0,0 +1,96 @@
|
|
1
|
+
import os
|
2
|
+
import sys
|
3
|
+
import time
|
4
|
+
from dataclasses import dataclass
|
5
|
+
from pathlib import Path
|
6
|
+
|
7
|
+
from cafs_cache_cdn_client.cafs.blob.hash_ import calc_hash_file
|
8
|
+
|
9
|
+
__all__ = (
|
10
|
+
'LocalFile',
|
11
|
+
'walk',
|
12
|
+
'compare_file_lists',
|
13
|
+
'set_file_stat',
|
14
|
+
)
|
15
|
+
|
16
|
+
|
17
|
+
if sys.platform == 'win32':
|
18
|
+
|
19
|
+
def is_same_mtime(
|
20
|
+
t1: int,
|
21
|
+
t2: int,
|
22
|
+
) -> bool:
|
23
|
+
return t1 // 100 == t2 // 100
|
24
|
+
else:
|
25
|
+
|
26
|
+
def is_same_mtime(
|
27
|
+
t1: int,
|
28
|
+
t2: int,
|
29
|
+
) -> bool:
|
30
|
+
return t1 == t2
|
31
|
+
|
32
|
+
|
33
|
+
@dataclass
|
34
|
+
class LocalFile:
|
35
|
+
path: Path
|
36
|
+
mtime: int
|
37
|
+
mode: int
|
38
|
+
blob: str | None = None
|
39
|
+
|
40
|
+
|
41
|
+
def walk(directory: Path) -> list[LocalFile]:
|
42
|
+
results = []
|
43
|
+
for root, _, files in directory.walk():
|
44
|
+
for file in files:
|
45
|
+
file_ = root / file
|
46
|
+
file_stat = file_.stat()
|
47
|
+
results.append(
|
48
|
+
LocalFile(
|
49
|
+
path=file_.relative_to(directory),
|
50
|
+
mtime=file_stat.st_mtime_ns,
|
51
|
+
mode=file_stat.st_mode & 0o777,
|
52
|
+
)
|
53
|
+
)
|
54
|
+
return results
|
55
|
+
|
56
|
+
|
57
|
+
async def compare_file_lists(
|
58
|
+
src_files: list[LocalFile],
|
59
|
+
dst_files: list[LocalFile],
|
60
|
+
directory: Path,
|
61
|
+
) -> tuple[list[LocalFile], list[LocalFile], list[LocalFile]]:
|
62
|
+
src_files_dict = {file.path: file for file in src_files}
|
63
|
+
dst_files_dict = {file.path: file for file in dst_files}
|
64
|
+
to_remove = src_files_dict.keys() - dst_files_dict.keys()
|
65
|
+
to_add = dst_files_dict.keys() - src_files_dict.keys()
|
66
|
+
to_update = set()
|
67
|
+
for same_file in src_files_dict.keys() & dst_files_dict.keys():
|
68
|
+
if not dst_files_dict[same_file].blob:
|
69
|
+
to_remove.add(same_file)
|
70
|
+
to_add.add(same_file)
|
71
|
+
continue
|
72
|
+
if not src_files_dict[same_file].blob:
|
73
|
+
src_files_dict[same_file].blob = await calc_hash_file(directory / same_file)
|
74
|
+
if src_files_dict[same_file].blob != dst_files_dict[same_file].blob:
|
75
|
+
to_remove.add(same_file)
|
76
|
+
to_add.add(same_file)
|
77
|
+
continue
|
78
|
+
if (
|
79
|
+
not is_same_mtime(
|
80
|
+
src_files_dict[same_file].mtime, dst_files_dict[same_file].mtime
|
81
|
+
)
|
82
|
+
or src_files_dict[same_file].mode != dst_files_dict[same_file].mode
|
83
|
+
):
|
84
|
+
to_update.add(same_file)
|
85
|
+
|
86
|
+
return (
|
87
|
+
[src_files_dict[file] for file in to_remove],
|
88
|
+
[dst_files_dict[file] for file in to_add],
|
89
|
+
[src_files_dict[file] for file in to_update],
|
90
|
+
)
|
91
|
+
|
92
|
+
|
93
|
+
def set_file_stat(file: LocalFile, directory: Path) -> None:
|
94
|
+
file_ = directory / file.path
|
95
|
+
file_.chmod(file.mode)
|
96
|
+
os.utime(file_, ns=(time.time_ns(), file.mtime))
|
@@ -0,0 +1,102 @@
|
|
1
|
+
from collections.abc import Iterable
|
2
|
+
from http import HTTPMethod
|
3
|
+
from pathlib import Path
|
4
|
+
from typing import Any, cast
|
5
|
+
from urllib.parse import quote, urljoin
|
6
|
+
|
7
|
+
import aiofiles
|
8
|
+
import aiohttp
|
9
|
+
import yarl
|
10
|
+
|
11
|
+
import cafs_cache_cdn_client.repo.datatypes as dt
|
12
|
+
|
13
|
+
__all__ = ('RepoClient',)
|
14
|
+
|
15
|
+
|
16
|
+
class RepoClient:
|
17
|
+
server_base_url: str
|
18
|
+
|
19
|
+
def __init__(self, server: str) -> None:
|
20
|
+
self.server_base_url = server
|
21
|
+
|
22
|
+
async def _request(
|
23
|
+
self,
|
24
|
+
endpoint: str,
|
25
|
+
params: Iterable[tuple[str, str]] | None = None,
|
26
|
+
method: HTTPMethod = HTTPMethod.GET,
|
27
|
+
data: dict | bytes | aiohttp.FormData | None = None,
|
28
|
+
headers: dict[str, str] | None = None,
|
29
|
+
json_request: bool = True,
|
30
|
+
) -> Any:
|
31
|
+
if params:
|
32
|
+
endpoint += '?' + '&'.join(f'{k}={quote(v)}' for k, v in params)
|
33
|
+
headers = headers.copy() if headers else {}
|
34
|
+
if json_request:
|
35
|
+
headers['Content-Type'] = 'application/json'
|
36
|
+
url_ = yarl.URL(urljoin(self.server_base_url, endpoint), encoded=True)
|
37
|
+
if json_request:
|
38
|
+
data_arg = {'json': data}
|
39
|
+
else:
|
40
|
+
data_arg = {'data': data}
|
41
|
+
async with aiohttp.ClientSession(
|
42
|
+
headers=headers, requote_redirect_url=False
|
43
|
+
) as session:
|
44
|
+
async with session.request(
|
45
|
+
method, url_, **data_arg, raise_for_status=True
|
46
|
+
) as resp:
|
47
|
+
if resp.headers.get('Content-Type') == 'application/json':
|
48
|
+
return await resp.json()
|
49
|
+
return await resp.read()
|
50
|
+
|
51
|
+
async def get_settings(self) -> dt.SettingsResponse:
|
52
|
+
return cast(
|
53
|
+
dt.SettingsResponse, await self._request('/settings', method=HTTPMethod.GET)
|
54
|
+
)
|
55
|
+
|
56
|
+
async def get_blob_urls(self) -> list[str]:
|
57
|
+
settings = await self.get_settings()
|
58
|
+
return settings['blob_urls']
|
59
|
+
|
60
|
+
async def is_ref_exist(self, repo: str, ref: str) -> bool:
|
61
|
+
try:
|
62
|
+
await self._request(f'/repository/{repo}/{ref}', method=HTTPMethod.HEAD)
|
63
|
+
return True
|
64
|
+
except aiohttp.ClientResponseError as e:
|
65
|
+
if e.status == 404:
|
66
|
+
return False
|
67
|
+
raise
|
68
|
+
|
69
|
+
async def delete_ref(self, repo: str, ref: str) -> None:
|
70
|
+
try:
|
71
|
+
await self._request(f'/repository/{repo}/{ref}', method=HTTPMethod.DELETE)
|
72
|
+
except aiohttp.ClientResponseError as e:
|
73
|
+
if e.status == 404:
|
74
|
+
return
|
75
|
+
raise
|
76
|
+
|
77
|
+
async def get_ref_info(self, repo: str, ref: str) -> dt.RefInfoResponse:
|
78
|
+
return cast(
|
79
|
+
dt.RefInfoResponse,
|
80
|
+
await self._request(f'/repository/{repo}/{ref}', method=HTTPMethod.GET),
|
81
|
+
)
|
82
|
+
|
83
|
+
async def tag_ref(self, repo: str, ref: str, tag: str) -> None:
|
84
|
+
await self._request(
|
85
|
+
f'/repository/{repo}/{ref}/tag', method=HTTPMethod.PUT, data={'tag': tag}
|
86
|
+
)
|
87
|
+
|
88
|
+
async def post_ref_info(self, repo: str, ref: str, data: dt.RefInfoBody) -> None:
|
89
|
+
await self._request(
|
90
|
+
f'/repository/{repo}/{ref}', method=HTTPMethod.POST, data=data
|
91
|
+
)
|
92
|
+
|
93
|
+
async def attach_file(self, repo: str, ref: str, file_path: Path) -> None:
|
94
|
+
async with aiofiles.open(file_path, 'rb') as f:
|
95
|
+
form_data = aiohttp.FormData()
|
96
|
+
form_data.add_field('file', f, filename=file_path.name)
|
97
|
+
await self._request(
|
98
|
+
f'/repository/{repo}/{ref}/attach',
|
99
|
+
method=HTTPMethod.POST,
|
100
|
+
data=form_data,
|
101
|
+
json_request=False,
|
102
|
+
)
|
@@ -0,0 +1,34 @@
|
|
1
|
+
from typing import TypedDict
|
2
|
+
|
3
|
+
__all__ = (
|
4
|
+
'SettingsResponse',
|
5
|
+
'RefInfoResponse',
|
6
|
+
'RefInfoBody',
|
7
|
+
'FileMetadata',
|
8
|
+
)
|
9
|
+
|
10
|
+
|
11
|
+
class FileMetadata(TypedDict):
|
12
|
+
blob: str
|
13
|
+
path: str
|
14
|
+
mtime: int
|
15
|
+
mode: int
|
16
|
+
|
17
|
+
|
18
|
+
class SettingsResponse(TypedDict):
|
19
|
+
blob_urls: list[str]
|
20
|
+
|
21
|
+
|
22
|
+
class RefInfoResponse(TypedDict):
|
23
|
+
revision: int
|
24
|
+
archive: bool
|
25
|
+
ttl: int
|
26
|
+
updated: int
|
27
|
+
files: list[FileMetadata]
|
28
|
+
|
29
|
+
|
30
|
+
class RefInfoBody(TypedDict):
|
31
|
+
archive: bool
|
32
|
+
ttl: int
|
33
|
+
comment: str | None
|
34
|
+
files: list[FileMetadata]
|
@@ -0,0 +1,99 @@
|
|
1
|
+
Metadata-Version: 2.3
|
2
|
+
Name: cafs-cache-cdn-client
|
3
|
+
Version: 1.0.5
|
4
|
+
Summary: Async Cache CDN client implementation
|
5
|
+
Keywords: cafs,cache
|
6
|
+
Author: Konstantin Belov
|
7
|
+
Author-email: k.belov@gaijin.team
|
8
|
+
Requires-Python: >=3.11,<4.0
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
10
|
+
Classifier: Development Status :: 5 - Production/Stable
|
11
|
+
Classifier: Programming Language :: Python :: 3.11
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
13
|
+
Classifier: Programming Language :: Python :: 3.13
|
14
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
15
|
+
Requires-Dist: aiofiles
|
16
|
+
Requires-Dist: aiohttp
|
17
|
+
Requires-Dist: blake3
|
18
|
+
Requires-Dist: zstandard
|
19
|
+
Description-Content-Type: text/markdown
|
20
|
+
|
21
|
+
# Cache CDN Client
|
22
|
+
A Python client library for interacting with the Cache CDN service based on CAFS, allowing efficient pushing and pulling of cached content.
|
23
|
+
## Installation
|
24
|
+
``` bash
|
25
|
+
pip install cafs-cache-cdn-client
|
26
|
+
```
|
27
|
+
## Features
|
28
|
+
- Asynchronous API for high-performance operations
|
29
|
+
- Push local directories to cache
|
30
|
+
- Pull cached content to local directories
|
31
|
+
- Check existence of cached references
|
32
|
+
- Tag references for easier access
|
33
|
+
- Attach additional files to existing references
|
34
|
+
- Delete references when no longer needed
|
35
|
+
|
36
|
+
## Usage Example
|
37
|
+
```python
|
38
|
+
import asyncio
|
39
|
+
import logging
|
40
|
+
from pathlib import Path
|
41
|
+
from cafs_cache_cdn_client import CacheCdnClient
|
42
|
+
|
43
|
+
# Configure logging to see detailed operation information
|
44
|
+
logging.basicConfig(level=logging.DEBUG)
|
45
|
+
|
46
|
+
|
47
|
+
async def main():
|
48
|
+
# Initialize the client with the server URL
|
49
|
+
# The connection_per_cafs_server parameter controls concurrency
|
50
|
+
client = CacheCdnClient(
|
51
|
+
'http://cache-server.example.com:8300',
|
52
|
+
connection_per_cafs_server=10
|
53
|
+
)
|
54
|
+
|
55
|
+
# Use as an async context manager to ensure proper resource cleanup
|
56
|
+
async with client:
|
57
|
+
# Push a local directory to cache with a 2-hour TTL
|
58
|
+
await client.push('project_name', 'build_artifacts',
|
59
|
+
'/path/to/build/output', ttl_hours=2,
|
60
|
+
comment='Build artifacts from CI run #123')
|
61
|
+
|
62
|
+
# Check if a reference exists
|
63
|
+
exists = await client.check('project_name', 'build_artifacts')
|
64
|
+
print(f"Reference exists: {exists}")
|
65
|
+
|
66
|
+
# Pull cached content to a local directory
|
67
|
+
await client.pull('project_name', 'build_artifacts',
|
68
|
+
'/path/to/destination')
|
69
|
+
|
70
|
+
# Tag a reference for easier access later
|
71
|
+
await client.tag('project_name', 'build_artifacts', 'latest_stable')
|
72
|
+
|
73
|
+
# Attach an additional file to an existing reference
|
74
|
+
await client.attach('project_name', 'build_artifacts',
|
75
|
+
Path('/path/to/metadata.json'))
|
76
|
+
|
77
|
+
# Delete a reference when no longer needed
|
78
|
+
await client.delete('project_name', 'old_artifacts')
|
79
|
+
|
80
|
+
|
81
|
+
# Run the example
|
82
|
+
if __name__ == '__main__':
|
83
|
+
asyncio.run(main())
|
84
|
+
```
|
85
|
+
|
86
|
+
## API Reference
|
87
|
+
### `CacheCdnClient`
|
88
|
+
- **Constructor**: `CacheCdnClient(server: str, connection_per_cafs_server: int = 1)`
|
89
|
+
- `server`: URL of the cache server
|
90
|
+
- `connection_per_cafs_server`: Number of concurrent connections per CAFS server
|
91
|
+
|
92
|
+
- **Methods**:
|
93
|
+
- `push(repo: str, ref: str, directory: Path | str, ttl_hours: int = 0, comment: str | None = None)` - Push a local directory to cache
|
94
|
+
- `pull(repo: str, ref: str, directory: Path | str)` - Pull cached content to a local directory
|
95
|
+
- `check(repo: str, ref: str) -> bool` - Check if a reference exists
|
96
|
+
- `tag(repo: str, ref: str, tag: str)` - Create a tag for a reference
|
97
|
+
- `attach(repo: str, ref: str, file_path: Path)` - Attach a file to an existing reference
|
98
|
+
- `delete(repo: str, ref: str)` - Delete a reference
|
99
|
+
|
@@ -0,0 +1,18 @@
|
|
1
|
+
cafs_cache_cdn_client/__init__.py,sha256=N-Gpb6OMWWaEruRwbPjkrZ0Uta8gsQFmq1yFhGcvtcc,35
|
2
|
+
cafs_cache_cdn_client/cafs/README.md,sha256=bcn9CEX4O74jY4kRi2oJb37oi55aneyQxDji76SU4Aw,2554
|
3
|
+
cafs_cache_cdn_client/cafs/__init__.py,sha256=Ae2WHVqgI9dEuWG5cX-8nbuUH_wpW-5dACt6JGM3FcE,123
|
4
|
+
cafs_cache_cdn_client/cafs/blob/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
5
|
+
cafs_cache_cdn_client/cafs/blob/hash_.py,sha256=tI4qenyHnEEtKnNUOq4a25L1GnlKjoieyPGQDoz14rA,736
|
6
|
+
cafs_cache_cdn_client/cafs/blob/package.py,sha256=pOMF5Dw42APHqIlRfcs1YPscqjcbO32Z35aEG0SqoHo,5865
|
7
|
+
cafs_cache_cdn_client/cafs/blob/utils.py,sha256=tqAOvPWIL-sOwXwPiH9Kbsqn5VekJtlf6UO8V9TTtwM,1021
|
8
|
+
cafs_cache_cdn_client/cafs/client.py,sha256=TJxwc_PFFmSeCxfTjybh--NrCDzuzuxh3Si42Lwm4Tc,18232
|
9
|
+
cafs_cache_cdn_client/cafs/exceptions.py,sha256=I4E3lFrQ_ysR8gDIh3E0Gz4OMihfLD6vLj_KwKzxnTU,654
|
10
|
+
cafs_cache_cdn_client/cafs/types.py,sha256=q7zqDQ8yKRn-HRSsXDuzawmikAEcyoYQ-_p2WmuDFy4,324
|
11
|
+
cafs_cache_cdn_client/client.py,sha256=d7Dw2U9h8dPYRDPu0mbnXc62SlLy4ruvQk2Alj3cVk8,4584
|
12
|
+
cafs_cache_cdn_client/file_utils.py,sha256=kDaak0n3emLIn0DXB02yWFkUhYxuLyTmH93WPszKEUQ,2642
|
13
|
+
cafs_cache_cdn_client/repo/__init__.py,sha256=JzCxKznnLV7WElJuznG_jL2s9tYPecJL5uJRnPZsW5M,58
|
14
|
+
cafs_cache_cdn_client/repo/client.py,sha256=FEiLefNmvJKwA2gz6dj2gcvt3uKRzLqBARGy4dvXP98,3545
|
15
|
+
cafs_cache_cdn_client/repo/datatypes.py,sha256=aTlzFoEibfAOQTqcsg-RmFjnwenS_rBCEMKtN6glOhY,531
|
16
|
+
cafs_cache_cdn_client-1.0.5.dist-info/METADATA,sha256=m-dhAfuTdQz97XW280baoDd2vZDxEQW0dtJF-eLqbFI,3679
|
17
|
+
cafs_cache_cdn_client-1.0.5.dist-info/WHEEL,sha256=fGIA9gx4Qxk2KDKeNJCbOEwSrmLtjWCwzBz351GyrPQ,88
|
18
|
+
cafs_cache_cdn_client-1.0.5.dist-info/RECORD,,
|