fab-build 0.0.1__tar.gz → 0.0.2.dev1__tar.gz
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.
- fab_build-0.0.2.dev1/PKG-INFO +11 -0
- fab_build-0.0.2.dev1/pyproject.toml +30 -0
- fab_build-0.0.2.dev1/pyproject.toml.orig +31 -0
- fab_build-0.0.2.dev1/src/fab/.pytest_cache/.gitignore +2 -0
- fab_build-0.0.2.dev1/src/fab/.pytest_cache/CACHEDIR.TAG +4 -0
- fab_build-0.0.2.dev1/src/fab/.pytest_cache/README.md +8 -0
- fab_build-0.0.2.dev1/src/fab/.pytest_cache/v/cache/nodeids +1 -0
- fab_build-0.0.2.dev1/src/fab/caching/__init__.py +6 -0
- fab_build-0.0.2.dev1/src/fab/caching/diskcache.py +248 -0
- fab_build-0.0.2.dev1/src/fab/caching/hit_counter.py +36 -0
- {fab_build-0.0.1 → fab_build-0.0.2.dev1/src}/fab/caching/memory.py +4 -4
- {fab_build-0.0.1 → fab_build-0.0.2.dev1/src}/fab/caching/nullcache.py +5 -3
- fab_build-0.0.2.dev1/src/fab/caching/test_diskcache.py +85 -0
- fab_build-0.0.2.dev1/src/fab/caching/test_diskcache_log.py +39 -0
- fab_build-0.0.2.dev1/src/fab/cli.py +279 -0
- fab_build-0.0.2.dev1/src/fab/executors/simple_executor.py +16 -0
- fab_build-0.0.2.dev1/src/fab/lang/.pytest_cache/.gitignore +2 -0
- fab_build-0.0.2.dev1/src/fab/lang/.pytest_cache/CACHEDIR.TAG +4 -0
- fab_build-0.0.2.dev1/src/fab/lang/.pytest_cache/README.md +8 -0
- fab_build-0.0.2.dev1/src/fab/lang/.pytest_cache/v/cache/lastfailed +3 -0
- fab_build-0.0.2.dev1/src/fab/lang/.pytest_cache/v/cache/nodeids +11 -0
- fab_build-0.0.2.dev1/src/fab/lang/astree.py +160 -0
- {fab_build-0.0.1 → fab_build-0.0.2.dev1/src}/fab/lang/buildins.py +92 -6
- {fab_build-0.0.1 → fab_build-0.0.2.dev1/src}/fab/lang/data.py +5 -1
- fab_build-0.0.2.dev1/src/fab/lang/evaluation_context.py +57 -0
- {fab_build-0.0.1 → fab_build-0.0.2.dev1/src}/fab/lang/evaluator.py +41 -14
- fab_build-0.0.2.dev1/src/fab/lang/parser.py +117 -0
- {fab_build-0.0.1 → fab_build-0.0.2.dev1/src}/fab/lang/test_parser.py +10 -0
- {fab_build-0.0.1 → fab_build-0.0.2.dev1/src}/fab/lang/util.py +3 -2
- fab_build-0.0.2.dev1/src/fab/model/__init__.py +21 -0
- fab_build-0.0.2.dev1/src/fab/model/cache.py +15 -0
- fab_build-0.0.2.dev1/src/fab/model/errors.py +19 -0
- fab_build-0.0.2.dev1/src/fab/model/executor.py +14 -0
- fab_build-0.0.2.dev1/src/fab/model/oci.py +91 -0
- fab_build-0.0.2.dev1/src/fab/model/operation.py +58 -0
- fab_build-0.0.2.dev1/src/fab/model/outputchunks.py +13 -0
- fab_build-0.0.2.dev1/src/fab/oci/__init__.py +0 -0
- fab_build-0.0.2.dev1/src/fab/oci/container_admin.py +74 -0
- fab_build-0.0.2.dev1/src/fab/oci/podman.py +383 -0
- fab_build-0.0.2.dev1/src/fab/oci/test_podman.py +9 -0
- {fab_build-0.0.1 → fab_build-0.0.2.dev1/src}/fab/operations/__init__.py +1 -0
- fab_build-0.0.2.dev1/src/fab/operations/containerized_gcc.py +256 -0
- {fab_build-0.0.1 → fab_build-0.0.2.dev1/src}/fab/operations/extract.py +4 -5
- {fab_build-0.0.1 → fab_build-0.0.2.dev1/src}/fab/operations/gcc.py +12 -35
- {fab_build-0.0.1 → fab_build-0.0.2.dev1/src}/fab/operations/http_get.py +3 -3
- fab_build-0.0.2.dev1/src/fab/operations/test_containerized_gcc.py +163 -0
- {fab_build-0.0.1 → fab_build-0.0.2.dev1/src}/fab/operations/test_http_get.py +12 -8
- fab_build-0.0.2.dev1/src/fab/util/__init__.py +0 -0
- fab_build-0.0.2.dev1/src/fab/util/bytesizestr.py +42 -0
- fab_build-0.0.2.dev1/src/fab/util/flatten_list.py +11 -0
- {fab_build-0.0.1 → fab_build-0.0.2.dev1/src}/fab/util/hash_objects.py +9 -2
- {fab_build-0.0.1 → fab_build-0.0.2.dev1/src}/fab/util/hash_path.py +8 -2
- {fab_build-0.0.1 → fab_build-0.0.2.dev1/src}/fab/util/string_slices.py +2 -2
- {fab_build-0.0.1 → fab_build-0.0.2.dev1/src}/fab/util/temp_file_structure.py +2 -2
- fab_build-0.0.2.dev1/src/fab/util/test_bytesizestr.py +5 -0
- fab_build-0.0.2.dev1/src/fab/util/test_flatten_list.py +21 -0
- {fab_build-0.0.1 → fab_build-0.0.2.dev1/src}/fab/util/test_hash_path.py +7 -3
- {fab_build-0.0.1 → fab_build-0.0.2.dev1/src}/fab/util/test_string_slices.py +2 -1
- fab_build-0.0.1/MANIFEST.in +0 -2
- fab_build-0.0.1/PKG-INFO +0 -7
- fab_build-0.0.1/fab/__init__.py +0 -4
- fab_build-0.0.1/fab/__main__.py +0 -2
- fab_build-0.0.1/fab/caching/__init__.py +0 -5
- fab_build-0.0.1/fab/caching/diskcache.py +0 -73
- fab_build-0.0.1/fab/caching/test_diskcache.py +0 -52
- fab_build-0.0.1/fab/lang/astree.py +0 -109
- fab_build-0.0.1/fab/lang/parser.py +0 -89
- fab_build-0.0.1/fab/main.py +0 -127
- fab_build-0.0.1/fab/model/__init__.py +0 -11
- fab_build-0.0.1/fab/model/cache.py +0 -14
- fab_build-0.0.1/fab/model/executor.py +0 -8
- fab_build-0.0.1/fab/model/oci.py +0 -1
- fab_build-0.0.1/fab/model/operation.py +0 -43
- fab_build-0.0.1/fab_build.egg-info/PKG-INFO +0 -7
- fab_build-0.0.1/fab_build.egg-info/SOURCES.txt +0 -45
- fab_build-0.0.1/fab_build.egg-info/dependency_links.txt +0 -1
- fab_build-0.0.1/fab_build.egg-info/entry_points.txt +0 -2
- fab_build-0.0.1/fab_build.egg-info/requires.txt +0 -4
- fab_build-0.0.1/fab_build.egg-info/top_level.txt +0 -1
- fab_build-0.0.1/pyproject.toml +0 -13
- fab_build-0.0.1/setup.cfg +0 -4
- {fab_build-0.0.1/fab/lang → fab_build-0.0.2.dev1/src/fab}/__init__.py +0 -0
- {fab_build-0.0.1/fab/util → fab_build-0.0.2.dev1/src/fab/lang}/__init__.py +0 -0
- {fab_build-0.0.1 → fab_build-0.0.2.dev1/src}/fab/lang/grammar.lark +0 -0
- {fab_build-0.0.1 → fab_build-0.0.2.dev1/src}/fab/lang/test_buildins.py +0 -0
- {fab_build-0.0.1 → fab_build-0.0.2.dev1/src}/fab/lang/test_evaluator.py +0 -0
- {fab_build-0.0.1 → fab_build-0.0.2.dev1/src}/fab/operations/http_archive.py +0 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: fab-build
|
|
3
|
+
Version: 0.0.2.dev1
|
|
4
|
+
Requires-Dist: docopt
|
|
5
|
+
Requires-Dist: blake3
|
|
6
|
+
Requires-Dist: lark
|
|
7
|
+
Requires-Dist: aiohttp
|
|
8
|
+
Requires-Dist: yarl
|
|
9
|
+
Requires-Dist: xdg-base-dirs
|
|
10
|
+
Requires-Dist: lark-rust>=0.2.1
|
|
11
|
+
Requires-Python: >=3.12
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "fab-build"
|
|
3
|
+
version = "0.0.2.dev1"
|
|
4
|
+
requires-python = ">=3.12"
|
|
5
|
+
dependencies = [
|
|
6
|
+
"docopt",
|
|
7
|
+
"blake3",
|
|
8
|
+
"lark",
|
|
9
|
+
"aiohttp",
|
|
10
|
+
"yarl",
|
|
11
|
+
"xdg-base-dirs",
|
|
12
|
+
"lark-rust>=0.2.1",
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
[project.scripts]
|
|
16
|
+
fab = "fab:cli.main"
|
|
17
|
+
|
|
18
|
+
[tool.uv.build-backend]
|
|
19
|
+
module-name = "fab"
|
|
20
|
+
|
|
21
|
+
[build-system]
|
|
22
|
+
requires = ["uv_build>=0.13.3,<0.14.0"]
|
|
23
|
+
build-backend = "uv_build"
|
|
24
|
+
|
|
25
|
+
[dependency-groups]
|
|
26
|
+
dev = [
|
|
27
|
+
"pytest>=9.1.1",
|
|
28
|
+
"pytest-aiohttp>=1.1.1",
|
|
29
|
+
"pytest-asyncio>=1.4.0",
|
|
30
|
+
]
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "fab-build"
|
|
3
|
+
version = "0.0.2.dev1"
|
|
4
|
+
requires-python = ">=3.12"
|
|
5
|
+
dependencies = [
|
|
6
|
+
"docopt",
|
|
7
|
+
"blake3",
|
|
8
|
+
"lark",
|
|
9
|
+
"aiohttp",
|
|
10
|
+
"yarl",
|
|
11
|
+
"xdg-base-dirs",
|
|
12
|
+
"lark-rust>=0.2.1",
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
[tool.uv.build-backend]
|
|
16
|
+
module-name = "fab"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
[project.scripts]
|
|
20
|
+
fab = "fab:cli.main"
|
|
21
|
+
|
|
22
|
+
[build-system]
|
|
23
|
+
requires = ["uv_build>=0.13.3,<0.14.0"]
|
|
24
|
+
build-backend = "uv_build"
|
|
25
|
+
|
|
26
|
+
[dependency-groups]
|
|
27
|
+
dev = [
|
|
28
|
+
"pytest>=9.1.1",
|
|
29
|
+
"pytest-aiohttp>=1.1.1",
|
|
30
|
+
"pytest-asyncio>=1.4.0",
|
|
31
|
+
]
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# pytest cache directory #
|
|
2
|
+
|
|
3
|
+
This directory contains data from the pytest's cache plugin,
|
|
4
|
+
which provides the `--lf` and `--ff` options, as well as the `cache` fixture.
|
|
5
|
+
|
|
6
|
+
**Do not** commit this to version control.
|
|
7
|
+
|
|
8
|
+
See [the docs](https://docs.pytest.org/en/stable/how-to/cache.html) for more information.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
[]
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import dataclasses
|
|
4
|
+
import os
|
|
5
|
+
import shutil
|
|
6
|
+
from collections.abc import Generator
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import override
|
|
10
|
+
|
|
11
|
+
import blake3
|
|
12
|
+
|
|
13
|
+
from ..model import Cache
|
|
14
|
+
from ..util.hash_path import hash_path
|
|
15
|
+
from ..util.string_slices import string_slices
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class DiskCacheError(RuntimeError):
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Log:
|
|
23
|
+
MAX_KEY_LENGTH = 32 # bytes
|
|
24
|
+
RECORD_LENGTH = len("YYYY-MM-DDTHH:MM:SS " + (MAX_KEY_LENGTH) * "00" + "\n")
|
|
25
|
+
|
|
26
|
+
def __init__(self, path: Path):
|
|
27
|
+
self.__path = path
|
|
28
|
+
self.__fd = self.__open(self.__path)
|
|
29
|
+
|
|
30
|
+
def __open(self, path: Path) -> int:
|
|
31
|
+
fd = os.open(path, os.O_APPEND | os.O_RDWR | os.O_CREAT)
|
|
32
|
+
os.lseek(fd, 0, os.SEEK_END)
|
|
33
|
+
return fd
|
|
34
|
+
|
|
35
|
+
def write(self, timestamp: datetime, op_key: bytes):
|
|
36
|
+
assert len(op_key) <= self.MAX_KEY_LENGTH
|
|
37
|
+
time_str = timestamp.isoformat(timespec="seconds")
|
|
38
|
+
os.write(
|
|
39
|
+
self.__fd,
|
|
40
|
+
f"{time_str} {op_key.hex().ljust(self.MAX_KEY_LENGTH * 2)}\n".encode(),
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
def read_new_to_old(self) -> Generator[tuple[datetime, bytes]]:
|
|
44
|
+
position = os.lseek(self.__fd, 0, os.SEEK_CUR) - self.RECORD_LENGTH
|
|
45
|
+
while position >= 0:
|
|
46
|
+
record = self.__read_record(position)
|
|
47
|
+
if record is None:
|
|
48
|
+
break
|
|
49
|
+
yield record
|
|
50
|
+
position -= self.RECORD_LENGTH
|
|
51
|
+
|
|
52
|
+
def oldest_entry(self) -> tuple[datetime, bytes] | None:
|
|
53
|
+
"""Return the oldest entry from the log"""
|
|
54
|
+
return self.__read_record(0)
|
|
55
|
+
|
|
56
|
+
def newest_entry(self) -> tuple[datetime, bytes] | None:
|
|
57
|
+
"""Return the newest entry from the log"""
|
|
58
|
+
position = os.lseek(self.__fd, 0, os.SEEK_CUR) - self.RECORD_LENGTH
|
|
59
|
+
if position < 0:
|
|
60
|
+
return None
|
|
61
|
+
return self.__read_record(position)
|
|
62
|
+
|
|
63
|
+
def record_count(self) -> int:
|
|
64
|
+
end = os.lseek(self.__fd, 0, os.SEEK_CUR)
|
|
65
|
+
return end // self.RECORD_LENGTH
|
|
66
|
+
|
|
67
|
+
def replace_with(self, other: Log):
|
|
68
|
+
os.close(self.__fd)
|
|
69
|
+
os.rename(other.__path, self.__path)
|
|
70
|
+
self.__fd = self.__open(self.__path)
|
|
71
|
+
|
|
72
|
+
def __read_record(self, position) -> tuple[datetime, bytes] | None:
|
|
73
|
+
record = os.pread(self.__fd, self.RECORD_LENGTH, position).decode()
|
|
74
|
+
if len(record) != self.RECORD_LENGTH:
|
|
75
|
+
return None
|
|
76
|
+
time_str, _, key_str = record.partition(" ")
|
|
77
|
+
timestamp = datetime.fromisoformat(time_str)
|
|
78
|
+
op_key = bytes.fromhex(key_str)
|
|
79
|
+
return timestamp, op_key
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@dataclasses.dataclass
|
|
83
|
+
class DiskCacheInfo:
|
|
84
|
+
path: Path
|
|
85
|
+
n_ops: int
|
|
86
|
+
n_blobs: int
|
|
87
|
+
size_blobs: int
|
|
88
|
+
oldest_op: datetime | None
|
|
89
|
+
newest_op: datetime | None
|
|
90
|
+
log_records: int
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class DiskCache(Cache):
|
|
94
|
+
def __init__(self, root: Path) -> None:
|
|
95
|
+
self.__root = root
|
|
96
|
+
self.__ops = root / "v1/ops"
|
|
97
|
+
self.__ops.mkdir(parents=True, exist_ok=True)
|
|
98
|
+
|
|
99
|
+
self.__blobs = root / "v1/blobs"
|
|
100
|
+
self.__blobs.mkdir(parents=True, exist_ok=True)
|
|
101
|
+
|
|
102
|
+
self.__log_path = root / "v1/log"
|
|
103
|
+
self.__log = Log(self.__log_path)
|
|
104
|
+
|
|
105
|
+
@override
|
|
106
|
+
async def has(self, op_key: bytes) -> bool:
|
|
107
|
+
op_path = self.__get_op_path(op_key)
|
|
108
|
+
result = op_path.exists()
|
|
109
|
+
if result:
|
|
110
|
+
self.__log.write(datetime.now(), op_key)
|
|
111
|
+
return result
|
|
112
|
+
|
|
113
|
+
@override
|
|
114
|
+
async def get_path(self, op_key: bytes) -> Path:
|
|
115
|
+
return self.__get_path(op_key)
|
|
116
|
+
|
|
117
|
+
def __get_path(self, op_key: bytes) -> Path:
|
|
118
|
+
op_path = self.__get_op_path(op_key)
|
|
119
|
+
if not op_path.is_dir():
|
|
120
|
+
raise DiskCacheError(f"{op_path} does not exist")
|
|
121
|
+
|
|
122
|
+
object_link = op_path / "object"
|
|
123
|
+
if not object_link.is_symlink():
|
|
124
|
+
raise DiskCacheError(f"Corrupt cache, {object_link} is not a symlink")
|
|
125
|
+
|
|
126
|
+
blob_path = object_link.resolve()
|
|
127
|
+
|
|
128
|
+
return blob_path
|
|
129
|
+
|
|
130
|
+
@override
|
|
131
|
+
async def store_path(self, op_key: bytes, path: Path) -> Path:
|
|
132
|
+
if not path.exists():
|
|
133
|
+
raise DiskCacheError(f"Cannot store non-existent path {path} in cache")
|
|
134
|
+
|
|
135
|
+
blob_key = hash_path(blake3.blake3(), path)
|
|
136
|
+
|
|
137
|
+
blob_path = self.__get_blob_path(blob_key)
|
|
138
|
+
blob_path.mkdir(parents=True, exist_ok=True)
|
|
139
|
+
obj_path = blob_path / path.name
|
|
140
|
+
if not obj_path.exists():
|
|
141
|
+
path.rename(obj_path)
|
|
142
|
+
|
|
143
|
+
op_path = self.__get_op_path(op_key)
|
|
144
|
+
if op_path.exists():
|
|
145
|
+
raise DiskCacheError(
|
|
146
|
+
f"Operation with key '{op_key.hex()}' already exists in cache"
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
op_path.mkdir(parents=True, exist_ok=True)
|
|
150
|
+
(op_path / "object").symlink_to(obj_path.relative_to(op_path, walk_up=True))
|
|
151
|
+
|
|
152
|
+
self.__log.write(datetime.now(), op_key)
|
|
153
|
+
return obj_path
|
|
154
|
+
|
|
155
|
+
def prune_to_size(self, size: int):
|
|
156
|
+
|
|
157
|
+
kept_ops: dict[bytes, datetime] = dict()
|
|
158
|
+
kept_blobs: set[bytes] = set()
|
|
159
|
+
kept_blobs_size: int = 0
|
|
160
|
+
discarded_ops: set[bytes] = set()
|
|
161
|
+
discarded_blobs: set[bytes] = set()
|
|
162
|
+
|
|
163
|
+
for timestamp, op_key in self.__log.read_new_to_old():
|
|
164
|
+
blob_path = self.__get_path(op_key)
|
|
165
|
+
blob_size = blob_path.stat().st_size
|
|
166
|
+
blob_key = bytes.fromhex(blob_path.parent.name)
|
|
167
|
+
|
|
168
|
+
if blob_key not in kept_blobs and kept_blobs_size + blob_size <= size:
|
|
169
|
+
kept_blobs.add(blob_key)
|
|
170
|
+
kept_blobs_size += blob_size
|
|
171
|
+
|
|
172
|
+
if blob_key in kept_blobs:
|
|
173
|
+
if op_key not in kept_ops:
|
|
174
|
+
kept_ops[op_key] = timestamp
|
|
175
|
+
else:
|
|
176
|
+
discarded_ops.add(op_key)
|
|
177
|
+
discarded_blobs.add(blob_key)
|
|
178
|
+
|
|
179
|
+
for op_key in discarded_ops:
|
|
180
|
+
self.__discard_op(op_key)
|
|
181
|
+
|
|
182
|
+
for blob_key in discarded_blobs:
|
|
183
|
+
self.__discard_blob(blob_key)
|
|
184
|
+
|
|
185
|
+
new_log = Log(self.__log_path.with_suffix(".new"))
|
|
186
|
+
for op_key, timestamp in reversed(kept_ops.items()):
|
|
187
|
+
new_log.write(timestamp, op_key)
|
|
188
|
+
|
|
189
|
+
self.__log.replace_with(new_log)
|
|
190
|
+
|
|
191
|
+
def prune_to_age(self, timestamp: datetime):
|
|
192
|
+
"""Remove all entries that have not been used after timestamp."""
|
|
193
|
+
# TODO, sometime...
|
|
194
|
+
raise NotImplementedError()
|
|
195
|
+
|
|
196
|
+
def get_info(self) -> DiskCacheInfo:
|
|
197
|
+
|
|
198
|
+
n_ops = 0
|
|
199
|
+
blob_keys: set[bytes] = set()
|
|
200
|
+
size_blobs = 0
|
|
201
|
+
|
|
202
|
+
for op_path in self.__scan_ops():
|
|
203
|
+
n_ops += 1
|
|
204
|
+
object_path = op_path / "object"
|
|
205
|
+
blob_key = bytes.fromhex(object_path.parent.name)
|
|
206
|
+
|
|
207
|
+
if blob_key in blob_keys:
|
|
208
|
+
continue
|
|
209
|
+
|
|
210
|
+
blob_keys.add(blob_key)
|
|
211
|
+
blob_path = object_path.resolve()
|
|
212
|
+
size_blobs += blob_path.stat().st_size
|
|
213
|
+
|
|
214
|
+
oldest_op = self.__log.oldest_entry()
|
|
215
|
+
newest_op = self.__log.newest_entry()
|
|
216
|
+
|
|
217
|
+
return DiskCacheInfo(
|
|
218
|
+
path=self.__root,
|
|
219
|
+
n_ops=n_ops,
|
|
220
|
+
n_blobs=len(blob_keys),
|
|
221
|
+
size_blobs=size_blobs,
|
|
222
|
+
oldest_op=oldest_op[0] if oldest_op else None,
|
|
223
|
+
newest_op=newest_op[0] if newest_op else None,
|
|
224
|
+
log_records=self.__log.record_count(),
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
def __get_op_path(self, op_key: bytes) -> Path:
|
|
228
|
+
key_parts = string_slices(op_key.hex(), 2, 2)
|
|
229
|
+
return self.__ops / key_parts[0] / key_parts[1] / op_key.hex()
|
|
230
|
+
|
|
231
|
+
def __get_blob_path(self, blob_key: bytes) -> Path:
|
|
232
|
+
key_parts = string_slices(blob_key.hex(), 2, 2)
|
|
233
|
+
return self.__blobs / key_parts[0] / key_parts[1] / blob_key.hex()
|
|
234
|
+
|
|
235
|
+
def __discard_op(self, op_key: bytes):
|
|
236
|
+
op_path = self.__get_op_path(op_key)
|
|
237
|
+
os.unlink(op_path / "object")
|
|
238
|
+
os.rmdir(op_path)
|
|
239
|
+
|
|
240
|
+
def __discard_blob(self, blob_key: bytes):
|
|
241
|
+
blob_path = self.__get_blob_path(blob_key)
|
|
242
|
+
shutil.rmtree(blob_path)
|
|
243
|
+
|
|
244
|
+
def __scan_ops(self) -> Generator[Path]:
|
|
245
|
+
for dir1 in os.scandir(self.__ops):
|
|
246
|
+
for dir2 in os.scandir(dir1):
|
|
247
|
+
for entry in os.scandir(dir2):
|
|
248
|
+
yield Path(entry.path)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import override
|
|
3
|
+
|
|
4
|
+
from fab.model import Cache
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class CacheHitCounter(Cache):
|
|
8
|
+
def __init__(self, base: Cache) -> None:
|
|
9
|
+
self.__base = base
|
|
10
|
+
self.__nr_hits = 0
|
|
11
|
+
self.__nr_misses = 0
|
|
12
|
+
|
|
13
|
+
@override
|
|
14
|
+
async def has(self, op_key: bytes) -> bool:
|
|
15
|
+
result = await self.__base.has(op_key)
|
|
16
|
+
if result:
|
|
17
|
+
self.__nr_hits += 1
|
|
18
|
+
else:
|
|
19
|
+
self.__nr_misses += 1
|
|
20
|
+
return result
|
|
21
|
+
|
|
22
|
+
@override
|
|
23
|
+
async def get_path(self, op_key: bytes) -> Path:
|
|
24
|
+
return await self.__base.get_path(op_key)
|
|
25
|
+
|
|
26
|
+
@override
|
|
27
|
+
async def store_path(self, op_key: bytes, path: Path) -> Path:
|
|
28
|
+
return await self.__base.store_path(op_key, path)
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def hits(self):
|
|
32
|
+
return self.__nr_hits
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def misses(self):
|
|
36
|
+
return self.__nr_misses
|
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
from pathlib import Path
|
|
2
2
|
from typing import override
|
|
3
3
|
|
|
4
|
-
|
|
5
4
|
from ..model import Cache
|
|
6
5
|
|
|
6
|
+
|
|
7
7
|
class MemoryCache(Cache):
|
|
8
8
|
def __init__(self):
|
|
9
9
|
self.__cached_paths: dict[bytes, Path] = {}
|
|
10
10
|
|
|
11
11
|
@override
|
|
12
|
-
def has(self, op_key: bytes) -> bool:
|
|
12
|
+
async def has(self, op_key: bytes) -> bool:
|
|
13
13
|
return op_key in self.__cached_paths
|
|
14
14
|
|
|
15
15
|
@override
|
|
16
|
-
def get_path(self, op_key: bytes) -> Path:
|
|
16
|
+
async def get_path(self, op_key: bytes) -> Path:
|
|
17
17
|
return self.__cached_paths[op_key]
|
|
18
18
|
|
|
19
19
|
@override
|
|
20
|
-
def store_path(self, op_key: bytes, path: Path) -> Path:
|
|
20
|
+
async def store_path(self, op_key: bytes, path: Path) -> Path:
|
|
21
21
|
self.__cached_paths[op_key] = path
|
|
22
22
|
return path
|
|
23
23
|
|
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
from pathlib import Path
|
|
2
2
|
from typing import override
|
|
3
|
+
|
|
3
4
|
from ..model import Cache
|
|
4
5
|
|
|
6
|
+
|
|
5
7
|
class NullCache(Cache):
|
|
6
8
|
@override
|
|
7
|
-
def has(self, op_key: bytes) -> bool:
|
|
9
|
+
async def has(self, op_key: bytes) -> bool:
|
|
8
10
|
return False
|
|
9
11
|
|
|
10
12
|
@override
|
|
11
|
-
def get_path(self, op_key: bytes) -> Path:
|
|
13
|
+
async def get_path(self, op_key: bytes) -> Path:
|
|
12
14
|
raise KeyError()
|
|
13
15
|
|
|
14
16
|
@override
|
|
15
|
-
def store_path(self, op_key: bytes, path: Path) -> Path:
|
|
17
|
+
async def store_path(self, op_key: bytes, path: Path) -> Path:
|
|
16
18
|
raise NotImplementedError()
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
from ..util.hash_path import hash_path
|
|
7
|
+
from ..util.temp_file_structure import TempFileStructure
|
|
8
|
+
from .diskcache import DiskCache
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def path_key(path: Path) -> str:
|
|
12
|
+
hashfunc = hashlib.sha256()
|
|
13
|
+
return hash_path(hashfunc, path).hex()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@pytest.mark.asyncio
|
|
17
|
+
async def test_store_has_get_path():
|
|
18
|
+
with TempFileStructure(
|
|
19
|
+
[
|
|
20
|
+
("objects/file1", "file1"),
|
|
21
|
+
("cache_dir/", None),
|
|
22
|
+
]
|
|
23
|
+
) as root:
|
|
24
|
+
cache = DiskCache(root / "cache_dir")
|
|
25
|
+
file = root / "objects/file1"
|
|
26
|
+
op_key = bytes.fromhex("abcdef1234567890")
|
|
27
|
+
original_key = path_key(file)
|
|
28
|
+
|
|
29
|
+
assert not await cache.has(op_key)
|
|
30
|
+
cached_file = await cache.store_path(op_key, file)
|
|
31
|
+
|
|
32
|
+
assert path_key(cached_file) == original_key
|
|
33
|
+
assert await cache.has(op_key)
|
|
34
|
+
|
|
35
|
+
retrieved_file = await cache.get_path(op_key)
|
|
36
|
+
assert path_key(retrieved_file) == original_key
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@pytest.mark.asyncio
|
|
40
|
+
async def test_store_again():
|
|
41
|
+
with TempFileStructure(
|
|
42
|
+
[
|
|
43
|
+
("objects1/file", "file"),
|
|
44
|
+
("objects2/file", "file"),
|
|
45
|
+
("cache_dir/", None),
|
|
46
|
+
]
|
|
47
|
+
) as root:
|
|
48
|
+
cache = DiskCache(root / "cache_dir")
|
|
49
|
+
file1 = root / "objects1/file"
|
|
50
|
+
file2 = root / "objects2/file"
|
|
51
|
+
op1_key = bytes.fromhex("abcd01")
|
|
52
|
+
op2_key = bytes.fromhex("abcd02")
|
|
53
|
+
cached_file1 = await cache.store_path(op1_key, file1)
|
|
54
|
+
cached_file2 = await cache.store_path(op2_key, file2)
|
|
55
|
+
|
|
56
|
+
assert path_key(cached_file1) == path_key(cached_file2)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def create_file(path: Path, content: str) -> Path:
|
|
60
|
+
with open(path, "w") as f:
|
|
61
|
+
f.write(content)
|
|
62
|
+
return path
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@pytest.mark.asyncio
|
|
66
|
+
async def test_prune_to_size():
|
|
67
|
+
with TempFileStructure() as root:
|
|
68
|
+
cache = DiskCache(root / "cache_dir")
|
|
69
|
+
op1_key = bytes.fromhex("abcd01")
|
|
70
|
+
op2_key = bytes.fromhex("abcd02")
|
|
71
|
+
file1 = await cache.store_path(
|
|
72
|
+
op1_key, create_file(root / "file1", "aa")
|
|
73
|
+
) # 2 bytes
|
|
74
|
+
file2 = await cache.store_path(
|
|
75
|
+
op2_key, create_file(root / "file2", "bb")
|
|
76
|
+
) # 2 bytes
|
|
77
|
+
|
|
78
|
+
assert cache.get_info().log_records == 2
|
|
79
|
+
cache.prune_to_size(3) # 3 bytes
|
|
80
|
+
assert cache.get_info().log_records == 1
|
|
81
|
+
|
|
82
|
+
assert not await cache.has(op1_key)
|
|
83
|
+
assert not file1.exists()
|
|
84
|
+
assert await cache.has(op2_key)
|
|
85
|
+
assert file2.exists()
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import random
|
|
2
|
+
from datetime import datetime, timedelta
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from .diskcache import Log
|
|
6
|
+
|
|
7
|
+
ONE_HOUR = timedelta(hours=1)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def random_key() -> bytes:
|
|
11
|
+
return random.randbytes(16)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def test_empty(tmp_path: Path):
|
|
15
|
+
log = Log(tmp_path / "log.log")
|
|
16
|
+
assert list(log.read_new_to_old()) == []
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_read(tmp_path: Path):
|
|
20
|
+
|
|
21
|
+
t0 = datetime(2000, 1, 1, 0, 0, 0)
|
|
22
|
+
t1 = t0 + ONE_HOUR
|
|
23
|
+
t2 = t1 + ONE_HOUR
|
|
24
|
+
|
|
25
|
+
k0 = random_key()
|
|
26
|
+
k1 = random_key()
|
|
27
|
+
k2 = random_key()
|
|
28
|
+
|
|
29
|
+
log1 = Log(tmp_path / "log.log")
|
|
30
|
+
log1.write(t0, k0)
|
|
31
|
+
log1.write(t1, k1)
|
|
32
|
+
log1.write(t2, k2)
|
|
33
|
+
|
|
34
|
+
log2 = Log(tmp_path / "log.log")
|
|
35
|
+
|
|
36
|
+
gen = log2.read_new_to_old()
|
|
37
|
+
assert next(gen) == (t2, k2)
|
|
38
|
+
assert next(gen) == (t1, k1)
|
|
39
|
+
assert next(gen) == (t0, k0)
|