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.
Files changed (87) hide show
  1. fab_build-0.0.2.dev1/PKG-INFO +11 -0
  2. fab_build-0.0.2.dev1/pyproject.toml +30 -0
  3. fab_build-0.0.2.dev1/pyproject.toml.orig +31 -0
  4. fab_build-0.0.2.dev1/src/fab/.pytest_cache/.gitignore +2 -0
  5. fab_build-0.0.2.dev1/src/fab/.pytest_cache/CACHEDIR.TAG +4 -0
  6. fab_build-0.0.2.dev1/src/fab/.pytest_cache/README.md +8 -0
  7. fab_build-0.0.2.dev1/src/fab/.pytest_cache/v/cache/nodeids +1 -0
  8. fab_build-0.0.2.dev1/src/fab/caching/__init__.py +6 -0
  9. fab_build-0.0.2.dev1/src/fab/caching/diskcache.py +248 -0
  10. fab_build-0.0.2.dev1/src/fab/caching/hit_counter.py +36 -0
  11. {fab_build-0.0.1 → fab_build-0.0.2.dev1/src}/fab/caching/memory.py +4 -4
  12. {fab_build-0.0.1 → fab_build-0.0.2.dev1/src}/fab/caching/nullcache.py +5 -3
  13. fab_build-0.0.2.dev1/src/fab/caching/test_diskcache.py +85 -0
  14. fab_build-0.0.2.dev1/src/fab/caching/test_diskcache_log.py +39 -0
  15. fab_build-0.0.2.dev1/src/fab/cli.py +279 -0
  16. fab_build-0.0.2.dev1/src/fab/executors/simple_executor.py +16 -0
  17. fab_build-0.0.2.dev1/src/fab/lang/.pytest_cache/.gitignore +2 -0
  18. fab_build-0.0.2.dev1/src/fab/lang/.pytest_cache/CACHEDIR.TAG +4 -0
  19. fab_build-0.0.2.dev1/src/fab/lang/.pytest_cache/README.md +8 -0
  20. fab_build-0.0.2.dev1/src/fab/lang/.pytest_cache/v/cache/lastfailed +3 -0
  21. fab_build-0.0.2.dev1/src/fab/lang/.pytest_cache/v/cache/nodeids +11 -0
  22. fab_build-0.0.2.dev1/src/fab/lang/astree.py +160 -0
  23. {fab_build-0.0.1 → fab_build-0.0.2.dev1/src}/fab/lang/buildins.py +92 -6
  24. {fab_build-0.0.1 → fab_build-0.0.2.dev1/src}/fab/lang/data.py +5 -1
  25. fab_build-0.0.2.dev1/src/fab/lang/evaluation_context.py +57 -0
  26. {fab_build-0.0.1 → fab_build-0.0.2.dev1/src}/fab/lang/evaluator.py +41 -14
  27. fab_build-0.0.2.dev1/src/fab/lang/parser.py +117 -0
  28. {fab_build-0.0.1 → fab_build-0.0.2.dev1/src}/fab/lang/test_parser.py +10 -0
  29. {fab_build-0.0.1 → fab_build-0.0.2.dev1/src}/fab/lang/util.py +3 -2
  30. fab_build-0.0.2.dev1/src/fab/model/__init__.py +21 -0
  31. fab_build-0.0.2.dev1/src/fab/model/cache.py +15 -0
  32. fab_build-0.0.2.dev1/src/fab/model/errors.py +19 -0
  33. fab_build-0.0.2.dev1/src/fab/model/executor.py +14 -0
  34. fab_build-0.0.2.dev1/src/fab/model/oci.py +91 -0
  35. fab_build-0.0.2.dev1/src/fab/model/operation.py +58 -0
  36. fab_build-0.0.2.dev1/src/fab/model/outputchunks.py +13 -0
  37. fab_build-0.0.2.dev1/src/fab/oci/__init__.py +0 -0
  38. fab_build-0.0.2.dev1/src/fab/oci/container_admin.py +74 -0
  39. fab_build-0.0.2.dev1/src/fab/oci/podman.py +383 -0
  40. fab_build-0.0.2.dev1/src/fab/oci/test_podman.py +9 -0
  41. {fab_build-0.0.1 → fab_build-0.0.2.dev1/src}/fab/operations/__init__.py +1 -0
  42. fab_build-0.0.2.dev1/src/fab/operations/containerized_gcc.py +256 -0
  43. {fab_build-0.0.1 → fab_build-0.0.2.dev1/src}/fab/operations/extract.py +4 -5
  44. {fab_build-0.0.1 → fab_build-0.0.2.dev1/src}/fab/operations/gcc.py +12 -35
  45. {fab_build-0.0.1 → fab_build-0.0.2.dev1/src}/fab/operations/http_get.py +3 -3
  46. fab_build-0.0.2.dev1/src/fab/operations/test_containerized_gcc.py +163 -0
  47. {fab_build-0.0.1 → fab_build-0.0.2.dev1/src}/fab/operations/test_http_get.py +12 -8
  48. fab_build-0.0.2.dev1/src/fab/util/__init__.py +0 -0
  49. fab_build-0.0.2.dev1/src/fab/util/bytesizestr.py +42 -0
  50. fab_build-0.0.2.dev1/src/fab/util/flatten_list.py +11 -0
  51. {fab_build-0.0.1 → fab_build-0.0.2.dev1/src}/fab/util/hash_objects.py +9 -2
  52. {fab_build-0.0.1 → fab_build-0.0.2.dev1/src}/fab/util/hash_path.py +8 -2
  53. {fab_build-0.0.1 → fab_build-0.0.2.dev1/src}/fab/util/string_slices.py +2 -2
  54. {fab_build-0.0.1 → fab_build-0.0.2.dev1/src}/fab/util/temp_file_structure.py +2 -2
  55. fab_build-0.0.2.dev1/src/fab/util/test_bytesizestr.py +5 -0
  56. fab_build-0.0.2.dev1/src/fab/util/test_flatten_list.py +21 -0
  57. {fab_build-0.0.1 → fab_build-0.0.2.dev1/src}/fab/util/test_hash_path.py +7 -3
  58. {fab_build-0.0.1 → fab_build-0.0.2.dev1/src}/fab/util/test_string_slices.py +2 -1
  59. fab_build-0.0.1/MANIFEST.in +0 -2
  60. fab_build-0.0.1/PKG-INFO +0 -7
  61. fab_build-0.0.1/fab/__init__.py +0 -4
  62. fab_build-0.0.1/fab/__main__.py +0 -2
  63. fab_build-0.0.1/fab/caching/__init__.py +0 -5
  64. fab_build-0.0.1/fab/caching/diskcache.py +0 -73
  65. fab_build-0.0.1/fab/caching/test_diskcache.py +0 -52
  66. fab_build-0.0.1/fab/lang/astree.py +0 -109
  67. fab_build-0.0.1/fab/lang/parser.py +0 -89
  68. fab_build-0.0.1/fab/main.py +0 -127
  69. fab_build-0.0.1/fab/model/__init__.py +0 -11
  70. fab_build-0.0.1/fab/model/cache.py +0 -14
  71. fab_build-0.0.1/fab/model/executor.py +0 -8
  72. fab_build-0.0.1/fab/model/oci.py +0 -1
  73. fab_build-0.0.1/fab/model/operation.py +0 -43
  74. fab_build-0.0.1/fab_build.egg-info/PKG-INFO +0 -7
  75. fab_build-0.0.1/fab_build.egg-info/SOURCES.txt +0 -45
  76. fab_build-0.0.1/fab_build.egg-info/dependency_links.txt +0 -1
  77. fab_build-0.0.1/fab_build.egg-info/entry_points.txt +0 -2
  78. fab_build-0.0.1/fab_build.egg-info/requires.txt +0 -4
  79. fab_build-0.0.1/fab_build.egg-info/top_level.txt +0 -1
  80. fab_build-0.0.1/pyproject.toml +0 -13
  81. fab_build-0.0.1/setup.cfg +0 -4
  82. {fab_build-0.0.1/fab/lang → fab_build-0.0.2.dev1/src/fab}/__init__.py +0 -0
  83. {fab_build-0.0.1/fab/util → fab_build-0.0.2.dev1/src/fab/lang}/__init__.py +0 -0
  84. {fab_build-0.0.1 → fab_build-0.0.2.dev1/src}/fab/lang/grammar.lark +0 -0
  85. {fab_build-0.0.1 → fab_build-0.0.2.dev1/src}/fab/lang/test_buildins.py +0 -0
  86. {fab_build-0.0.1 → fab_build-0.0.2.dev1/src}/fab/lang/test_evaluator.py +0 -0
  87. {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,2 @@
1
+ # Created by pytest automatically.
2
+ *
@@ -0,0 +1,4 @@
1
+ Signature: 8a477f597d28d172789f06886806bc55
2
+ # This file is a cache directory tag created by pytest.
3
+ # For information about cache directory tags, see:
4
+ # https://bford.info/cachedir/spec.html
@@ -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,6 @@
1
+ from .diskcache import DiskCache
2
+ from .hit_counter import CacheHitCounter
3
+ from .memory import MemoryCache
4
+ from .nullcache import NullCache
5
+
6
+ __all__ = ["MemoryCache", "NullCache", "DiskCache", "CacheHitCounter"]
@@ -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)