rbx.cp 0.6.1__py3-none-any.whl → 0.8.0__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.
Files changed (50) hide show
  1. rbx/box/cd.py +32 -4
  2. rbx/box/cli.py +82 -34
  3. rbx/box/code.py +131 -82
  4. rbx/box/contest/main.py +25 -0
  5. rbx/box/creation.py +3 -0
  6. rbx/box/global_package.py +74 -0
  7. rbx/box/linting.py +76 -7
  8. rbx/box/package.py +6 -19
  9. rbx/box/presets/__init__.py +4 -4
  10. rbx/box/remote.py +19 -0
  11. rbx/box/sanitizers/warning_stack.py +3 -3
  12. rbx/box/solutions.py +13 -7
  13. rbx/box/stats.py +10 -0
  14. rbx/box/stresses.py +45 -64
  15. rbx/box/stressing/finder_parser.py +11 -16
  16. rbx/box/tasks.py +33 -22
  17. rbx/box/tooling/boca/scraper.py +1 -1
  18. rbx/grading/caching.py +98 -47
  19. rbx/grading/debug_context.py +31 -0
  20. rbx/grading/grading_context.py +96 -0
  21. rbx/grading/judge/cacher.py +93 -21
  22. rbx/grading/judge/sandbox.py +6 -3
  23. rbx/grading/judge/sandboxes/timeit.py +1 -1
  24. rbx/grading/judge/storage.py +169 -35
  25. rbx/grading/profiling.py +126 -0
  26. rbx/grading/steps.py +44 -16
  27. rbx/grading/steps_with_caching.py +52 -26
  28. rbx/resources/presets/default/contest/.gitignore +2 -0
  29. rbx/resources/presets/default/contest/contest.rbx.yml +18 -4
  30. rbx/resources/presets/default/contest/statement/contest.rbx.tex +25 -86
  31. rbx/resources/presets/default/contest/statement/icpc.sty +322 -0
  32. rbx/resources/presets/default/contest/statement/instructions.tex +40 -0
  33. rbx/resources/presets/default/contest/statement/logo.png +0 -0
  34. rbx/resources/presets/default/contest/statement/template.rbx.tex +45 -36
  35. rbx/resources/presets/default/preset.rbx.yml +8 -6
  36. rbx/resources/presets/default/problem/problem.rbx.yml +20 -17
  37. rbx/resources/presets/default/problem/statement/icpc.sty +322 -0
  38. rbx/resources/presets/default/problem/statement/template.rbx.tex +47 -79
  39. {rbx_cp-0.6.1.dist-info → rbx_cp-0.8.0.dist-info}/METADATA +4 -1
  40. {rbx_cp-0.6.1.dist-info → rbx_cp-0.8.0.dist-info}/RECORD +48 -41
  41. rbx/resources/presets/default/contest/statement/olymp.sty +0 -250
  42. rbx/resources/presets/default/problem/statement/olymp.sty +0 -250
  43. /rbx/resources/presets/default/problem/{gen.cpp → gens/gen.cpp} +0 -0
  44. /rbx/resources/presets/default/problem/{tests → manual_tests}/samples/000.in +0 -0
  45. /rbx/resources/presets/default/problem/{tests → manual_tests}/samples/001.in +0 -0
  46. /rbx/resources/presets/default/problem/{random.py → testplan/random.py} +0 -0
  47. /rbx/resources/presets/default/problem/{random.txt → testplan/random.txt} +0 -0
  48. {rbx_cp-0.6.1.dist-info → rbx_cp-0.8.0.dist-info}/LICENSE +0 -0
  49. {rbx_cp-0.6.1.dist-info → rbx_cp-0.8.0.dist-info}/WHEEL +0 -0
  50. {rbx_cp-0.6.1.dist-info → rbx_cp-0.8.0.dist-info}/entry_points.txt +0 -0
@@ -10,6 +10,7 @@ import typer
10
10
  from rbx import console
11
11
  from rbx.box import package
12
12
  from rbx.box.schema import CodeItem, ExpectedOutcome
13
+ from rbx.box.solutions import expand_solutions
13
14
  from rbx.grading.steps import CheckerResult, Outcome, RunLog, TestcaseLog
14
15
 
15
16
  LARK_GRAMMAR = r"""
@@ -26,11 +27,12 @@ negation: _NOT "(" disjunction ")"
26
27
  // Expressions
27
28
  logical: eval matcher expected_outcome -> matching
28
29
  | eval equality (eval | outcome) -> equating
30
+ | eval -> eval_only
29
31
 
30
- eval: "[" solution checking? "]"
32
+ eval: "[" solution checking? "]" | solution
31
33
 
32
34
  // Eval
33
- solution: _filename | WILDCARD
35
+ solution: _solution_filename | WILDCARD
34
36
  checking: "ON"i (checking_mode? checker | ":nil")
35
37
  checking_mode: MODE ":"
36
38
  MODE: "2" | "3"
@@ -55,6 +57,7 @@ WILDCARD: "$"
55
57
 
56
58
  // File name
57
59
  _filename: FILENAME | "\"" FILENAME "\""
60
+ _solution_filename: _filename | "@" _filename
58
61
  FILENAME: /[\/A-Za-z0-9\-_\.]/+
59
62
 
60
63
  // Names (Variables)
@@ -217,20 +220,7 @@ def get_all_solutions(tree: lark.ParseTree) -> List[str]:
217
220
 
218
221
  def get_all_solution_items(tree: lark.ParseTree) -> List[CodeItem]:
219
222
  solution_names = get_all_solutions(tree)
220
- res = []
221
-
222
- for solution_name in solution_names:
223
- found_solution = package.get_solution_or_nil(solution_name)
224
- if found_solution is None:
225
- res.append(
226
- CodeItem(
227
- path=pathlib.Path(solution_name),
228
- language=None,
229
- compilationFiles=None,
230
- )
231
- )
232
- continue
233
- res.append(found_solution)
223
+ res = typing.cast(List[CodeItem], expand_solutions(solution_names))
234
224
 
235
225
  main_solution = package.get_main_solution()
236
226
  if main_solution is None:
@@ -391,6 +381,11 @@ class FinderTreeRunner(lark.Transformer):
391
381
 
392
382
  return FinderOutcome(truth_value=truth_value, results=results)
393
383
 
384
+ def eval_only(self, eval_result: FinderResult) -> FinderOutcome:
385
+ return self.matching(
386
+ eval_result, is_positive=True, expected_outcome=ExpectedOutcome.INCORRECT
387
+ )
388
+
394
389
  def negation(self, value: FinderOutcome) -> FinderOutcome:
395
390
  return dataclasses.replace(value, truth_value=not value.truth_value)
396
391
 
rbx/box/tasks.py CHANGED
@@ -5,7 +5,8 @@ from rbx.box import checkers, package, state
5
5
  from rbx.box.code import CommunicationItem, run_communication, run_item
6
6
  from rbx.box.environment import EnvironmentSandbox, ExecutionConfig, VerificationLevel
7
7
  from rbx.box.retries import Retrier, get_retrier_config
8
- from rbx.box.schema import Solution, Testcase
8
+ from rbx.box.schema import CodeItem, Testcase
9
+ from rbx.grading import profiling
9
10
  from rbx.grading.judge.sandbox import SandboxBase
10
11
  from rbx.grading.limits import Limits
11
12
  from rbx.grading.steps import (
@@ -39,7 +40,7 @@ def get_limits_for_language(
39
40
 
40
41
 
41
42
  async def run_solution_on_testcase(
42
- solution: Solution,
43
+ solution: CodeItem,
43
44
  compiled_digest: str,
44
45
  checker_digest: Optional[str],
45
46
  testcase: Testcase,
@@ -52,6 +53,8 @@ async def run_solution_on_testcase(
52
53
  use_timelimit: bool = True,
53
54
  capture_pipes: bool = False,
54
55
  nruns: int = 0,
56
+ filestem: Optional[str] = None,
57
+ is_stress: bool = False,
55
58
  ) -> Evaluation:
56
59
  if interactor_digest is not None:
57
60
  return await _run_communication_solution_on_testcase(
@@ -68,6 +71,8 @@ async def run_solution_on_testcase(
68
71
  use_timelimit=use_timelimit,
69
72
  capture_pipes=capture_pipes,
70
73
  nruns=nruns,
74
+ filestem=filestem,
75
+ is_stress=is_stress,
71
76
  )
72
77
 
73
78
  async def run_fn(retry_index: int) -> Evaluation:
@@ -85,29 +90,32 @@ async def run_solution_on_testcase(
85
90
  assert testcase.outputPath is not None
86
91
  output_path = testcase.outputPath
87
92
  else:
88
- output_path = output_dir / testcase.inputPath.with_suffix('.out').name
93
+ stem = filestem or testcase.inputPath.stem
94
+ output_path = output_dir / pathlib.PosixPath(stem).with_suffix('.out')
89
95
  error_path = output_path.with_suffix('.err')
90
96
  log_path = output_path.with_suffix('.log')
91
97
  eval_path = output_path.with_suffix('.eval')
92
98
  output_path.parent.mkdir(parents=True, exist_ok=True)
93
99
 
94
- run_log = await run_item(
95
- solution,
96
- DigestOrSource.create(compiled_digest),
97
- stdin=DigestOrSource.create(testcase.inputPath),
98
- stdout=DigestOrDest.create(output_path),
99
- stderr=DigestOrDest.create(error_path),
100
- extra_config=extra_config,
101
- retry_index=retry_index,
102
- )
100
+ with profiling.PushContext('tasks.run_solution_on_testcase'):
101
+ run_log = await run_item(
102
+ solution,
103
+ DigestOrSource.create(compiled_digest),
104
+ stdin=DigestOrSource.create(testcase.inputPath),
105
+ stdout=DigestOrDest.create(output_path),
106
+ stderr=DigestOrDest.create(error_path),
107
+ extra_config=extra_config,
108
+ retry_index=retry_index,
109
+ )
103
110
 
104
111
  if checker_digest is not None:
105
- checker_result = await checkers.check(
106
- checker_digest,
107
- run_log,
108
- testcase,
109
- program_output=output_path,
110
- )
112
+ with profiling.PushContext('tasks.run_solution_on_testcase.check'):
113
+ checker_result = await checkers.check(
114
+ checker_digest,
115
+ run_log,
116
+ testcase,
117
+ program_output=output_path,
118
+ )
111
119
  else:
112
120
  checker_result = checkers.check_with_no_output(run_log)
113
121
 
@@ -134,7 +142,7 @@ async def run_solution_on_testcase(
134
142
  if not use_retries:
135
143
  return await run_fn(0)
136
144
 
137
- retrier = Retrier(get_retrier_config(nruns))
145
+ retrier = Retrier(get_retrier_config(nruns), is_stress=is_stress)
138
146
  return await retrier.repeat(run_fn)
139
147
 
140
148
 
@@ -156,7 +164,7 @@ def _get_execution_config(
156
164
 
157
165
 
158
166
  async def _run_communication_solution_on_testcase(
159
- solution: Solution,
167
+ solution: CodeItem,
160
168
  compiled_digest: str,
161
169
  interactor_digest: str,
162
170
  checker_digest: Optional[str],
@@ -169,6 +177,8 @@ async def _run_communication_solution_on_testcase(
169
177
  use_timelimit: bool = True,
170
178
  capture_pipes: bool = False,
171
179
  nruns: int = 0,
180
+ filestem: Optional[str] = None,
181
+ is_stress: bool = False,
172
182
  ) -> Evaluation:
173
183
  capture_pipes = capture_pipes or state.STATE.debug_logs
174
184
 
@@ -200,7 +210,8 @@ async def _run_communication_solution_on_testcase(
200
210
  assert testcase.outputPath is not None
201
211
  output_path = testcase.outputPath
202
212
  else:
203
- output_path = output_dir / testcase.inputPath.with_suffix('.out').name
213
+ stem = filestem or testcase.inputPath.stem
214
+ output_path = output_dir / pathlib.PosixPath(stem).with_suffix('.out')
204
215
  solution_error_path = output_path.with_suffix('.sol.err')
205
216
  interactor_error_path = output_path.with_suffix('.int.err')
206
217
  log_path = output_path.with_suffix('.log')
@@ -294,5 +305,5 @@ async def _run_communication_solution_on_testcase(
294
305
  if not use_retries:
295
306
  return await run_fn(0)
296
307
 
297
- retrier = Retrier(get_retrier_config(nruns))
308
+ retrier = Retrier(get_retrier_config(nruns), is_stress=is_stress)
298
309
  return await retrier.repeat(run_fn)
@@ -364,7 +364,7 @@ class BocaScraper:
364
364
  return final_path
365
365
 
366
366
 
367
- @functools.lru_cache
367
+ @functools.cache
368
368
  def get_boca_scraper(
369
369
  base_url: Optional[str] = None,
370
370
  username: Optional[str] = None,
rbx/grading/caching.py CHANGED
@@ -3,13 +3,18 @@ import io
3
3
  import os
4
4
  import pathlib
5
5
  import shelve
6
+ import shutil
7
+ import tempfile
6
8
  from typing import Any, Dict, List, Optional
7
9
 
8
10
  from pydantic import BaseModel
9
11
 
10
12
  from rbx import console
13
+ from rbx.grading import grading_context
14
+ from rbx.grading.judge.cacher import FileCacher
11
15
  from rbx.grading.judge.digester import digest_cooperatively
12
- from rbx.grading.judge.storage import Storage, copyfileobj
16
+ from rbx.grading.judge.storage import copyfileobj
17
+ from rbx.grading.profiling import Profiler
13
18
  from rbx.grading.steps import DigestHolder, GradingArtifacts, GradingLogsHolder
14
19
 
15
20
  VERBOSE = False
@@ -89,11 +94,15 @@ def _build_digest_list(artifacts_list: List[GradingArtifacts]) -> List[DigestHol
89
94
  return digests
90
95
 
91
96
 
92
- def _build_fingerprint_list(artifacts_list: List[GradingArtifacts]) -> List[str]:
97
+ def _build_fingerprint_list(
98
+ artifacts_list: List[GradingArtifacts], cacher: FileCacher
99
+ ) -> List[str]:
93
100
  fingerprints = []
94
101
  for artifacts in artifacts_list:
95
102
  for input in artifacts.inputs:
96
- if input.src is None:
103
+ if input.src is None or not input.hash:
104
+ continue
105
+ if cacher.digest_from_symlink(input.src) is not None:
97
106
  continue
98
107
  with input.src.open('rb') as f:
99
108
  fingerprints.append(digest_cooperatively(f))
@@ -124,9 +133,10 @@ def _build_logs_list(artifacts_list: List[GradingArtifacts]) -> List[GradingLogs
124
133
 
125
134
  def _build_cache_fingerprint(
126
135
  artifacts_list: List[GradingArtifacts],
136
+ cacher: FileCacher,
127
137
  ) -> CacheFingerprint:
128
138
  digests = [digest.value for digest in _build_digest_list(artifacts_list)]
129
- fingerprints = _build_fingerprint_list(artifacts_list)
139
+ fingerprints = _build_fingerprint_list(artifacts_list, cacher)
130
140
  output_fingerprints = _build_output_fingerprint_list(artifacts_list)
131
141
  logs = _build_logs_list(artifacts_list)
132
142
  return CacheFingerprint(
@@ -155,6 +165,7 @@ def _build_cache_input(
155
165
  commands: List[str],
156
166
  artifact_list: List[GradingArtifacts],
157
167
  extra_params: Dict[str, Any],
168
+ cacher: FileCacher,
158
169
  ) -> CacheInput:
159
170
  cloned_artifact_list = [
160
171
  artifacts.model_copy(deep=True) for artifacts in artifact_list
@@ -164,6 +175,15 @@ def _build_cache_input(
164
175
  # part of the cache key.
165
176
  artifacts.logs = None
166
177
 
178
+ for input in artifacts.inputs:
179
+ if input.src is None:
180
+ continue
181
+ inferred_digest = cacher.digest_from_symlink(input.src)
182
+ if inferred_digest is not None:
183
+ # Consume cache from digest instead of file.
184
+ input.digest = DigestHolder(value=inferred_digest)
185
+ input.src = None
186
+
167
187
  for output in artifacts.outputs:
168
188
  if output.hash:
169
189
  # Cleanup dest field from hash artifacts
@@ -185,7 +205,7 @@ def _build_cache_key(input: CacheInput) -> str:
185
205
  return digest_cooperatively(fobj)
186
206
 
187
207
 
188
- def _copy_hashed_files(artifact_list: List[GradingArtifacts], storage: Storage):
208
+ def _copy_hashed_files(artifact_list: List[GradingArtifacts], cacher: FileCacher):
189
209
  for artifact in artifact_list:
190
210
  for output in artifact.outputs:
191
211
  if not output.hash or output.dest is None:
@@ -194,19 +214,27 @@ def _copy_hashed_files(artifact_list: List[GradingArtifacts], storage: Storage):
194
214
  if output.optional and output.digest.value is None:
195
215
  continue
196
216
  assert output.digest.value is not None
197
- with storage.get_file(output.digest.value) as fobj:
198
- with output.dest.open('wb') as f:
199
- copyfileobj(fobj, f, maxlen=output.maxlen)
217
+ if (
218
+ path_to_symlink := cacher.path_for_symlink(output.digest.value)
219
+ ) is not None:
220
+ # Use a symlink to the file in the persistent cache, if available.
221
+ output.dest.unlink(missing_ok=True)
222
+ output.dest.symlink_to(path_to_symlink)
223
+ else:
224
+ # Otherwise, copy it.
225
+ with cacher.get_file(output.digest.value) as fobj:
226
+ with output.dest.open('wb') as f:
227
+ copyfileobj(fobj, f, maxlen=output.maxlen)
200
228
  if output.executable:
201
229
  output.dest.chmod(0o755)
202
230
 
203
231
 
204
- def is_artifact_ok(artifact: GradingArtifacts, storage: Storage) -> bool:
232
+ def is_artifact_ok(artifact: GradingArtifacts, cacher: FileCacher) -> bool:
205
233
  for output in artifact.outputs:
206
234
  if output.optional or output.intermediate:
207
235
  continue
208
236
  if output.digest is not None:
209
- if output.digest.value is None or not storage.exists(output.digest.value):
237
+ if output.digest.value is None or not cacher.exists(output.digest.value):
210
238
  return False
211
239
  return True
212
240
  assert output.dest is not None
@@ -219,9 +247,9 @@ def is_artifact_ok(artifact: GradingArtifacts, storage: Storage) -> bool:
219
247
  return True
220
248
 
221
249
 
222
- def are_artifacts_ok(artifacts: List[GradingArtifacts], storage: Storage) -> bool:
250
+ def are_artifacts_ok(artifacts: List[GradingArtifacts], cacher: FileCacher) -> bool:
223
251
  for artifact in artifacts:
224
- if not is_artifact_ok(artifact, storage):
252
+ if not is_artifact_ok(artifact, cacher):
225
253
  return False
226
254
  return True
227
255
 
@@ -244,53 +272,70 @@ class DependencyCacheBlock:
244
272
  self._key = None
245
273
 
246
274
  def __enter__(self):
247
- input = _build_cache_input(
248
- commands=self.commands,
249
- artifact_list=self.artifact_list,
250
- extra_params=self.extra_params,
251
- )
252
- if VERBOSE:
253
- console.console.log(f'Cache input is: {input}')
254
- self._key = _build_cache_key(input)
255
- if VERBOSE:
256
- console.console.log(f'Cache key is: {self._key}')
257
- found = self.cache.find_in_cache(
258
- self.commands, self.artifact_list, self.extra_params, key=self._key
259
- )
260
- return found
261
-
262
- def __exit__(self, exc_type, exc_val, exc_tb):
263
- if exc_type is None:
264
- self.cache.store_in_cache(
275
+ with Profiler('enter_in_cache'):
276
+ if grading_context.is_no_cache():
277
+ return False
278
+ input = _build_cache_input(
279
+ commands=self.commands,
280
+ artifact_list=self.artifact_list,
281
+ extra_params=self.extra_params,
282
+ cacher=self.cache.cacher,
283
+ )
284
+ if VERBOSE:
285
+ console.console.log(f'Cache input is: {input}')
286
+ self._key = _build_cache_key(input)
287
+ if VERBOSE:
288
+ console.console.log(f'Cache key is: {self._key}')
289
+ found = self.cache.find_in_cache(
265
290
  self.commands, self.artifact_list, self.extra_params, key=self._key
266
291
  )
267
- if exc_type is NoCacheException:
268
- return True
269
- return None
292
+ return found
293
+
294
+ def __exit__(self, exc_type, exc_val, exc_tb):
295
+ with Profiler('exit_in_cache'):
296
+ if grading_context.is_no_cache():
297
+ return True if exc_type is NoCacheException else None
298
+ if exc_type is None:
299
+ self.cache.store_in_cache(
300
+ self.commands, self.artifact_list, self.extra_params, key=self._key
301
+ )
302
+ if exc_type is NoCacheException:
303
+ return True
304
+ return None
270
305
 
271
306
 
272
307
  class DependencyCache:
273
308
  root: pathlib.Path
274
- storage: Storage
309
+ cacher: FileCacher
275
310
 
276
- def __init__(self, root: pathlib.Path, storage: Storage):
311
+ def __init__(self, root: pathlib.Path, cacher: FileCacher):
277
312
  self.root = root
278
- self.storage = storage
313
+ self.cacher = cacher
279
314
  self.db = shelve.open(self._cache_name())
315
+ tmp_dir = pathlib.Path(tempfile.mkdtemp())
316
+ self.transient_db = shelve.open(tmp_dir / '.cache_db')
280
317
  atexit.register(lambda: self.db.close())
318
+ atexit.register(lambda: self.transient_db.close())
319
+ atexit.register(lambda: shutil.rmtree(tmp_dir))
281
320
 
282
321
  def _cache_name(self) -> str:
283
322
  return str(self.root / '.cache_db')
284
323
 
324
+ def get_db(self) -> shelve.Shelf:
325
+ if grading_context.is_transient():
326
+ return self.transient_db
327
+ return self.db
328
+
285
329
  def _find_in_cache(self, key: str) -> Optional[CacheFingerprint]:
286
- return self.db.get(key)
330
+ return self.get_db().get(key)
287
331
 
288
332
  def _store_in_cache(self, key: str, fingerprint: CacheFingerprint):
289
- self.db[key] = fingerprint
333
+ self.get_db()[key] = fingerprint
290
334
 
291
335
  def _evict_from_cache(self, key: str):
292
- if key in self.db:
293
- del self.db[key]
336
+ db = self.get_db()
337
+ if key in db:
338
+ del db[key]
294
339
 
295
340
  def __call__(
296
341
  self,
@@ -309,7 +354,10 @@ class DependencyCache:
309
354
  key: Optional[str] = None,
310
355
  ) -> bool:
311
356
  input = _build_cache_input(
312
- commands=commands, artifact_list=artifact_list, extra_params=extra_params
357
+ commands=commands,
358
+ artifact_list=artifact_list,
359
+ extra_params=extra_params,
360
+ cacher=self.cacher,
313
361
  )
314
362
  key = key or _build_cache_key(input)
315
363
 
@@ -317,7 +365,7 @@ class DependencyCache:
317
365
  if fingerprint is None:
318
366
  return False
319
367
 
320
- reference_fingerprint = _build_cache_fingerprint(artifact_list)
368
+ reference_fingerprint = _build_cache_fingerprint(artifact_list, self.cacher)
321
369
 
322
370
  if not _fingerprints_match(fingerprint, reference_fingerprint):
323
371
  self._evict_from_cache(key)
@@ -334,7 +382,7 @@ class DependencyCache:
334
382
  for digest, reference_digest in zip(fingerprint.digests, reference_digests):
335
383
  reference_digest.value = digest
336
384
 
337
- if not are_artifacts_ok(artifact_list, self.storage):
385
+ if not are_artifacts_ok(artifact_list, self.cacher):
338
386
  # Rollback digest changes.
339
387
  for old_digest_value, reference_digest in zip(
340
388
  old_digest_values, reference_digests
@@ -344,7 +392,7 @@ class DependencyCache:
344
392
  return False
345
393
 
346
394
  # Copy hashed files to file system.
347
- _copy_hashed_files(artifact_list, self.storage)
395
+ _copy_hashed_files(artifact_list, self.cacher)
348
396
 
349
397
  # Apply logs changes.
350
398
  for logs, reference_logs in zip(fingerprint.logs, reference_fingerprint.logs):
@@ -366,11 +414,14 @@ class DependencyCache:
366
414
  key: Optional[str] = None,
367
415
  ):
368
416
  input = _build_cache_input(
369
- commands=commands, artifact_list=artifact_list, extra_params=extra_params
417
+ commands=commands,
418
+ artifact_list=artifact_list,
419
+ extra_params=extra_params,
420
+ cacher=self.cacher,
370
421
  )
371
422
  key = key or _build_cache_key(input)
372
423
 
373
- if not are_artifacts_ok(artifact_list, self.storage):
424
+ if not are_artifacts_ok(artifact_list, self.cacher):
374
425
  return
375
426
 
376
- self._store_in_cache(key, _build_cache_fingerprint(artifact_list))
427
+ self._store_in_cache(key, _build_cache_fingerprint(artifact_list, self.cacher))
@@ -0,0 +1,31 @@
1
+ import contextvars
2
+ import dataclasses
3
+ from dataclasses import dataclass
4
+
5
+
6
+ @dataclass(frozen=True)
7
+ class DebugContext:
8
+ enable: bool = False
9
+
10
+
11
+ debug_var = contextvars.ContextVar('debug', default=DebugContext())
12
+
13
+
14
+ def get_debug_context() -> DebugContext:
15
+ return debug_var.get()
16
+
17
+
18
+ class Debug:
19
+ def __init__(self, *args, **kwargs):
20
+ self.args = args
21
+ self.kwargs = kwargs
22
+ self.token = None
23
+
24
+ def __enter__(self):
25
+ self.token = debug_var.set(
26
+ dataclasses.replace(debug_var.get(), *self.args, **self.kwargs)
27
+ )
28
+
29
+ def __exit__(self, exc_type, exc_value, traceback):
30
+ if self.token is not None:
31
+ debug_var.reset(self.token)
@@ -0,0 +1,96 @@
1
+ import contextvars
2
+ from enum import Enum
3
+ from typing import Callable, Optional, Union
4
+
5
+ Condition = Union[bool, Callable[[], bool]]
6
+
7
+
8
+ class ConditionedContext:
9
+ def __init__(self, when: Condition = True):
10
+ self.when = when
11
+
12
+ def should_enter(self) -> bool:
13
+ if isinstance(self.when, bool):
14
+ return self.when
15
+ return self.when()
16
+
17
+
18
+ class CacheLevel(Enum):
19
+ NO_CACHE = 0
20
+ CACHE_TRANSIENTLY = 1
21
+ CACHE_COMPILATION = 2
22
+ CACHE_ALL = 3
23
+
24
+
25
+ cache_level_var = contextvars.ContextVar('cache_level', default=CacheLevel.CACHE_ALL)
26
+
27
+
28
+ def is_compilation_only() -> bool:
29
+ return cache_level_var.get() == CacheLevel.CACHE_COMPILATION
30
+
31
+
32
+ def is_transient() -> bool:
33
+ return cache_level_var.get().value <= CacheLevel.CACHE_TRANSIENTLY.value
34
+
35
+
36
+ def is_no_cache() -> bool:
37
+ return cache_level_var.get().value <= CacheLevel.NO_CACHE.value
38
+
39
+
40
+ class cache_level(ConditionedContext):
41
+ def __init__(self, level: CacheLevel, when: Condition = True):
42
+ super().__init__(when)
43
+ self.level = level
44
+ self.token = None
45
+
46
+ def __enter__(self):
47
+ if self.should_enter():
48
+ self.token = cache_level_var.set(self.level)
49
+ return self
50
+
51
+ def __exit__(self, exc_type, exc_val, exc_tb):
52
+ if self.token is not None:
53
+ cache_level_var.reset(self.token)
54
+ return None
55
+
56
+
57
+ compression_level_var = contextvars.ContextVar('compression_level', default=5)
58
+ use_compression_var = contextvars.ContextVar('use_compression', default=False)
59
+
60
+
61
+ def get_compression_level() -> int:
62
+ return compression_level_var.get()
63
+
64
+
65
+ def should_compress() -> bool:
66
+ return use_compression_var.get()
67
+
68
+
69
+ class compression(ConditionedContext):
70
+ def __init__(
71
+ self,
72
+ level: Optional[int] = None,
73
+ use_compression: Optional[bool] = None,
74
+ when: Condition = True,
75
+ ):
76
+ super().__init__(when)
77
+ self.level = level
78
+ self.use_compression = use_compression
79
+ self.level_token = None
80
+ self.use_compression_token = None
81
+
82
+ def __enter__(self):
83
+ if not self.should_enter():
84
+ return self
85
+ if self.level is not None:
86
+ self.level_token = compression_level_var.set(self.level)
87
+ if self.use_compression is not None:
88
+ self.use_compression_token = use_compression_var.set(self.use_compression)
89
+ return self
90
+
91
+ def __exit__(self, exc_type, exc_val, exc_tb):
92
+ if self.level_token is not None:
93
+ compression_level_var.reset(self.level_token)
94
+ if self.use_compression_token is not None:
95
+ use_compression_var.reset(self.use_compression_token)
96
+ return None