fm-weck 1.5.3__py3-none-any.whl → 1.6.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 (127) hide show
  1. fm_weck/__init__.py +1 -1
  2. fm_weck/capture.py +1 -1
  3. fm_weck/cli.py +107 -1
  4. fm_weck/engine.py +31 -4
  5. fm_weck/exceptions.py +4 -0
  6. fm_weck/image_mgr.py +66 -0
  7. fm_weck/resources/fm_tools/2ls.yml +22 -0
  8. fm_weck/resources/fm_tools/afltc.yml +59 -0
  9. fm_weck/resources/fm_tools/aise.yml +28 -1
  10. fm_weck/resources/fm_tools/aprove.yml +25 -0
  11. fm_weck/resources/fm_tools/brick.yml +25 -1
  12. fm_weck/resources/fm_tools/bubaak-split.yml +18 -0
  13. fm_weck/resources/fm_tools/bubaak.yml +10 -0
  14. fm_weck/resources/fm_tools/cbmc.yml +12 -0
  15. fm_weck/resources/fm_tools/cetfuzz.yml +21 -6
  16. fm_weck/resources/fm_tools/coastal.yml +12 -0
  17. fm_weck/resources/fm_tools/concurrentwitness2test.yml +25 -2
  18. fm_weck/resources/fm_tools/cooperace.yml +39 -0
  19. fm_weck/resources/fm_tools/coveritest.yml +22 -0
  20. fm_weck/resources/fm_tools/cpa-bam-bnb.yml +16 -1
  21. fm_weck/resources/fm_tools/cpa-bam-smg.yml +17 -1
  22. fm_weck/resources/fm_tools/cpa-lockator.yml +17 -1
  23. fm_weck/resources/fm_tools/cpa-witness2test.yml +13 -3
  24. fm_weck/resources/fm_tools/cpachecker.yml +150 -10
  25. fm_weck/resources/fm_tools/cpv.yml +48 -0
  26. fm_weck/resources/fm_tools/crux.yml +12 -0
  27. fm_weck/resources/fm_tools/cseq.yml +36 -9
  28. fm_weck/resources/fm_tools/dartagnan.yml +63 -3
  29. fm_weck/resources/fm_tools/dasa.yml +70 -0
  30. fm_weck/resources/fm_tools/deagle.yml +19 -1
  31. fm_weck/resources/fm_tools/divine.yml +13 -0
  32. fm_weck/resources/fm_tools/ebf.yml +17 -2
  33. fm_weck/resources/fm_tools/emergentheta.yml +51 -5
  34. fm_weck/resources/fm_tools/esbmc-incr.yml +59 -2
  35. fm_weck/resources/fm_tools/esbmc-kind.yml +59 -2
  36. fm_weck/resources/fm_tools/fdse.yml +31 -0
  37. fm_weck/resources/fm_tools/fizzer.yml +32 -3
  38. fm_weck/resources/fm_tools/frama-c-sv.yml +21 -2
  39. fm_weck/resources/fm_tools/fshell-witness2test.yml +13 -3
  40. fm_weck/resources/fm_tools/function-res.yml +64 -0
  41. fm_weck/resources/fm_tools/fusebmc-ia.yml +20 -6
  42. fm_weck/resources/fm_tools/fusebmc.yml +25 -4
  43. fm_weck/resources/fm_tools/gazer-theta.yml +16 -3
  44. fm_weck/resources/fm_tools/gdart-llvm.yml +16 -1
  45. fm_weck/resources/fm_tools/gdart.yml +49 -2
  46. fm_weck/resources/fm_tools/goblint-par.yml +66 -0
  47. fm_weck/resources/fm_tools/goblint.yml +239 -12
  48. fm_weck/resources/fm_tools/goblitch.yml +77 -0
  49. fm_weck/resources/fm_tools/graves-par.yml +4 -1
  50. fm_weck/resources/fm_tools/graves.yml +20 -5
  51. fm_weck/resources/fm_tools/gwit.yml +13 -3
  52. fm_weck/resources/fm_tools/hornix.yml +26 -4
  53. fm_weck/resources/fm_tools/hybridtiger.yml +12 -1
  54. fm_weck/resources/fm_tools/iekke.yml +73 -0
  55. fm_weck/resources/fm_tools/infer.yml +12 -0
  56. fm_weck/resources/fm_tools/java-ranger.yml +18 -0
  57. fm_weck/resources/fm_tools/jayhorn.yml +50 -4
  58. fm_weck/resources/fm_tools/jbmc.yml +17 -1
  59. fm_weck/resources/fm_tools/jcwit.yml +12 -2
  60. fm_weck/resources/fm_tools/jdart.yml +16 -1
  61. fm_weck/resources/fm_tools/jlisa.yml +105 -0
  62. fm_weck/resources/fm_tools/klee.yml +12 -1
  63. fm_weck/resources/fm_tools/kleef.yml +18 -0
  64. fm_weck/resources/fm_tools/korn.yml +23 -1
  65. fm_weck/resources/fm_tools/lazycseq.yml +16 -1
  66. fm_weck/resources/fm_tools/legion-symcc.yml +5 -1
  67. fm_weck/resources/fm_tools/legion.yml +7 -4
  68. fm_weck/resources/fm_tools/lf-checker.yml +16 -1
  69. fm_weck/resources/fm_tools/liv.yml +48 -3
  70. fm_weck/resources/fm_tools/locksmith.yml +16 -1
  71. fm_weck/resources/fm_tools/metaval++.yml +1 -1
  72. fm_weck/resources/fm_tools/metaval.yml +121 -6
  73. fm_weck/resources/fm_tools/mlb.yml +23 -1
  74. fm_weck/resources/fm_tools/mopsa.yml +100 -7
  75. fm_weck/resources/fm_tools/muval.yml +120 -0
  76. fm_weck/resources/fm_tools/nacpa.yml +57 -2
  77. fm_weck/resources/fm_tools/nitwit.yml +13 -3
  78. fm_weck/resources/fm_tools/ogchecker.yml +47 -0
  79. fm_weck/resources/fm_tools/owic.yml +16 -2
  80. fm_weck/resources/fm_tools/pesco.yml +19 -1
  81. fm_weck/resources/fm_tools/pichecker.yml +16 -1
  82. fm_weck/resources/fm_tools/pinaka.yml +14 -1
  83. fm_weck/resources/fm_tools/predatorhp.yml +12 -0
  84. fm_weck/resources/fm_tools/proton.yml +26 -1
  85. fm_weck/resources/fm_tools/prtest.yml +23 -0
  86. fm_weck/resources/fm_tools/pysvlib-linter.yml +77 -0
  87. fm_weck/resources/fm_tools/pysvlib-validator.yml +76 -0
  88. fm_weck/resources/fm_tools/racerf.yml +27 -4
  89. fm_weck/resources/fm_tools/re3ver.yml +78 -0
  90. fm_weck/resources/fm_tools/rizzer.yml +11 -1
  91. fm_weck/resources/fm_tools/schema.yml +185 -4
  92. fm_weck/resources/fm_tools/seal.yml +67 -0
  93. fm_weck/resources/fm_tools/sikraken.yml +25 -1
  94. fm_weck/resources/fm_tools/spf.yml +14 -1
  95. fm_weck/resources/fm_tools/sv-sanitizers.yml +46 -3
  96. fm_weck/resources/fm_tools/svf-svc.yml +33 -1
  97. fm_weck/resources/fm_tools/svlibchecker.yml +82 -0
  98. fm_weck/resources/fm_tools/swat.yml +30 -0
  99. fm_weck/resources/fm_tools/symbiotic-witch.yml +31 -6
  100. fm_weck/resources/fm_tools/symbiotic.yml +64 -1
  101. fm_weck/resources/fm_tools/testcoca.yml +53 -0
  102. fm_weck/resources/fm_tools/testcov.yml +97 -0
  103. fm_weck/resources/fm_tools/theta.yml +100 -2
  104. fm_weck/resources/fm_tools/thorn.yml +22 -1
  105. fm_weck/resources/fm_tools/tracerx-wp.yml +18 -0
  106. fm_weck/resources/fm_tools/tracerx.yml +22 -0
  107. fm_weck/resources/fm_tools/uautomizer.yml +186 -19
  108. fm_weck/resources/fm_tools/ugemcutter.yml +97 -9
  109. fm_weck/resources/fm_tools/ukojak.yml +55 -9
  110. fm_weck/resources/fm_tools/uparalizer.yml +78 -0
  111. fm_weck/resources/fm_tools/ureferee.yml +83 -13
  112. fm_weck/resources/fm_tools/utaipan.yml +61 -9
  113. fm_weck/resources/fm_tools/utestgen.yml +36 -0
  114. fm_weck/resources/fm_tools/veriabs.yml +12 -0
  115. fm_weck/resources/fm_tools/veriabsl.yml +16 -0
  116. fm_weck/resources/fm_tools/verioover.yml +16 -1
  117. fm_weck/resources/fm_tools/wasp-c.yml +16 -1
  118. fm_weck/resources/fm_tools/wit4java.yml +36 -4
  119. fm_weck/resources/fm_tools/witch.yml +29 -4
  120. fm_weck/resources/fm_tools/witnesslint.yml +111 -8
  121. fm_weck/resources/fm_tools/witnessmap.yml +29 -1
  122. fm_weck/zenodo.py +381 -0
  123. {fm_weck-1.5.3.dist-info → fm_weck-1.6.0.dist-info}/METADATA +2 -1
  124. fm_weck-1.6.0.dist-info/RECORD +188 -0
  125. {fm_weck-1.5.3.dist-info → fm_weck-1.6.0.dist-info}/WHEEL +1 -1
  126. fm_weck-1.5.3.dist-info/RECORD +0 -171
  127. {fm_weck-1.5.3.dist-info → fm_weck-1.6.0.dist-info}/entry_points.txt +0 -0
@@ -33,6 +33,43 @@ maintainers:
33
33
  url: https://www.sosy-lab.org/people/lingsch-rosenfeld/
34
34
 
35
35
  versions:
36
+ - version: "2.1.2-correctness-witnesses-v2"
37
+ doi: 10.5281/zenodo.17792484
38
+ benchexec_toolinfo_options: ["--witness", "${witness}", "--expectCorrectnessWitness", "--ignoreSelfLoops", "--expectedWitnessVersion", "2"]
39
+ required_ubuntu_packages:
40
+ - python3-lxml
41
+ - python3-jsonschema
42
+ - python3-yaml
43
+ - python3-pycparser
44
+ - python3-clang
45
+ - version: "2.1.2-violation-witnesses-v2"
46
+ doi: 10.5281/zenodo.17792484
47
+ benchexec_toolinfo_options: ["--witness", "${witness}", "--expectViolationWitness", "--ignoreSelfLoops", "--expectedWitnessVersion", "2"]
48
+ required_ubuntu_packages:
49
+ - python3-lxml
50
+ - python3-jsonschema
51
+ - python3-yaml
52
+ - python3-pycparser
53
+ - python3-clang
54
+ - version: "2.1.2-correctness-witnesses-v1"
55
+ doi: 10.5281/zenodo.17792484
56
+ benchexec_toolinfo_options: ["--witness", "${witness}", "--expectCorrectnessWitness", "--ignoreSelfLoops", "--expectedWitnessVersion", "1"]
57
+ required_ubuntu_packages:
58
+ - python3-lxml
59
+ - python3-jsonschema
60
+ - python3-yaml
61
+ - python3-pycparser
62
+ - python3-clang
63
+ - version: "2.1.2-violation-witnesses-v1"
64
+ doi: 10.5281/zenodo.17792484
65
+ benchexec_toolinfo_options: ["--witness", "${witness}", "--expectViolationWitness", "--ignoreSelfLoops", "--expectedWitnessVersion", "1"]
66
+ required_ubuntu_packages:
67
+ - python3-lxml
68
+ - python3-jsonschema
69
+ - python3-yaml
70
+ - python3-pycparser
71
+ - python3-clang
72
+
36
73
  - version: "svcomp25-correctness-graphml"
37
74
  doi: 10.5281/zenodo.15050742
38
75
  benchexec_toolinfo_options: ["--witness", "${witness}", "--expectCorrectnessWitness", "--ignoreSelfLoops", "--expectedWitnessVersion", "1.0"]
@@ -41,6 +78,8 @@ versions:
41
78
  - python3-jsonschema
42
79
  - python3-yaml
43
80
  - python3-pycparser
81
+ base_container_images:
82
+ - docker.io/ubuntu:24.04
44
83
  - version: "svcomp25-violation-graphml"
45
84
  doi: 10.5281/zenodo.15050742
46
85
  benchexec_toolinfo_options: ["--witness", "${witness}", "--expectViolationWitness", "--ignoreSelfLoops", "--expectedWitnessVersion", "1.0"]
@@ -49,6 +88,8 @@ versions:
49
88
  - python3-jsonschema
50
89
  - python3-yaml
51
90
  - python3-pycparser
91
+ base_container_images:
92
+ - docker.io/ubuntu:24.04
52
93
  - version: "svcomp25-correctness-yaml"
53
94
  doi: 10.5281/zenodo.15050742
54
95
  benchexec_toolinfo_options: ["--witness", "${witness}", "--expectCorrectnessWitness", "--ignoreSelfLoops", "--expectedWitnessVersion", "2.0"]
@@ -57,6 +98,8 @@ versions:
57
98
  - python3-jsonschema
58
99
  - python3-yaml
59
100
  - python3-pycparser
101
+ base_container_images:
102
+ - docker.io/ubuntu:24.04
60
103
  - version: "svcomp25-violation-yaml"
61
104
  doi: 10.5281/zenodo.15050742
62
105
  benchexec_toolinfo_options: ["--witness", "${witness}", "--expectViolationWitness", "--ignoreSelfLoops", "--expectedWitnessVersion", "2.0"]
@@ -65,6 +108,9 @@ versions:
65
108
  - python3-jsonschema
66
109
  - python3-yaml
67
110
  - python3-pycparser
111
+ base_container_images:
112
+ - docker.io/ubuntu:24.04
113
+
68
114
  - version: "svcomp24-correctness-graphml"
69
115
  doi: 10.5281/zenodo.10213801
70
116
  benchexec_toolinfo_options: ["--expectCorrectnessWitness", "--ignoreSelfLoops", "--expectedWitnessVersion", "1.0"]
@@ -99,8 +145,65 @@ versions:
99
145
  - python3-pycparser
100
146
 
101
147
  competition_participations:
148
+ - competition: "SV-COMP 2026"
149
+ track: "Validation of Correctness Witnesses v2"
150
+ label:
151
+ - auxiliary
152
+ tool_version: "2.1.2-correctness-witnesses-v2"
153
+ jury_member:
154
+ orcid: 0000-0002-8172-3184
155
+ name: Marian Lingsch-Rosenfeld
156
+ institution: LMU Munich
157
+ country: Germany
158
+ url: https://www.sosy-lab.org/people/lingsch-rosenfeld/
159
+ participants:
160
+ - orcid: 0000-0002-8172-3184
161
+ name: Marian Lingsch-Rosenfeld
162
+ - competition: "SV-COMP 2026"
163
+ track: "Validation of Correctness Witnesses v1"
164
+ label:
165
+ - auxiliary
166
+ tool_version: "2.1.2-correctness-witnesses-v1"
167
+ jury_member:
168
+ orcid: 0000-0002-8172-3184
169
+ name: Marian Lingsch-Rosenfeld
170
+ institution: LMU Munich
171
+ country: Germany
172
+ url: https://www.sosy-lab.org/people/lingsch-rosenfeld/
173
+ participants:
174
+ - orcid: 0000-0002-8172-3184
175
+ name: Marian Lingsch-Rosenfeld
176
+ - competition: "SV-COMP 2026"
177
+ track: "Validation of Violation Witnesses v2"
178
+ label:
179
+ - auxiliary
180
+ tool_version: "2.1.2-violation-witnesses-v2"
181
+ jury_member:
182
+ orcid: 0000-0002-8172-3184
183
+ name: Marian Lingsch-Rosenfeld
184
+ institution: LMU Munich
185
+ country: Germany
186
+ url: https://www.sosy-lab.org/people/lingsch-rosenfeld/
187
+ participants:
188
+ - orcid: 0000-0002-8172-3184
189
+ name: Marian Lingsch-Rosenfeld
190
+ - competition: "SV-COMP 2026"
191
+ track: "Validation of Violation Witnesses v1"
192
+ label:
193
+ - auxiliary
194
+ tool_version: "2.1.2-violation-witnesses-v1"
195
+ jury_member:
196
+ orcid: 0000-0002-8172-3184
197
+ name: Marian Lingsch-Rosenfeld
198
+ institution: LMU Munich
199
+ country: Germany
200
+ url: https://www.sosy-lab.org/people/lingsch-rosenfeld/
201
+ participants:
202
+ - orcid: 0000-0002-8172-3184
203
+ name: Marian Lingsch-Rosenfeld
204
+
102
205
  - competition: "SV-COMP 2025"
103
- track: "Validation of Correctness Witnesses 1.0"
206
+ track: "Validation of Correctness Witnesses v1"
104
207
  label:
105
208
  - auxiliary
106
209
  tool_version: "svcomp25-correctness-graphml"
@@ -111,7 +214,7 @@ competition_participations:
111
214
  country: Germany
112
215
  url: https://www.sosy-lab.org/people/lingsch-rosenfeld/
113
216
  - competition: "SV-COMP 2025"
114
- track: "Validation of Correctness Witnesses 2.0"
217
+ track: "Validation of Correctness Witnesses v2"
115
218
  label:
116
219
  - auxiliary
117
220
  tool_version: "svcomp25-correctness-yaml"
@@ -122,7 +225,7 @@ competition_participations:
122
225
  country: Germany
123
226
  url: https://www.sosy-lab.org/people/lingsch-rosenfeld/
124
227
  - competition: "SV-COMP 2025"
125
- track: "Validation of Violation Witnesses 1.0"
228
+ track: "Validation of Violation Witnesses v1"
126
229
  label:
127
230
  - auxiliary
128
231
  tool_version: "svcomp25-violation-graphml"
@@ -133,7 +236,7 @@ competition_participations:
133
236
  country: Germany
134
237
  url: https://www.sosy-lab.org/people/lingsch-rosenfeld/
135
238
  - competition: "SV-COMP 2025"
136
- track: "Validation of Violation Witnesses 2.0"
239
+ track: "Validation of Violation Witnesses v2"
137
240
  label:
138
241
  - auxiliary
139
242
  tool_version: "svcomp25-violation-yaml"
@@ -144,7 +247,7 @@ competition_participations:
144
247
  country: Germany
145
248
  url: https://www.sosy-lab.org/people/lingsch-rosenfeld/
146
249
  - competition: "SV-COMP 2024"
147
- track: "Validation of Correctness Witnesses 1.0"
250
+ track: "Validation of Correctness Witnesses v1"
148
251
  label:
149
252
  - auxiliary
150
253
  tool_version: "svcomp24-correctness-graphml"
@@ -155,7 +258,7 @@ competition_participations:
155
258
  country: Germany
156
259
  url: https://www.sosy-lab.org/people/lingsch-rosenfeld/
157
260
  - competition: "SV-COMP 2024"
158
- track: "Validation of Correctness Witnesses 2.0"
261
+ track: "Validation of Correctness Witnesses v2"
159
262
  label:
160
263
  - auxiliary
161
264
  tool_version: "svcomp24-correctness-yaml"
@@ -166,7 +269,7 @@ competition_participations:
166
269
  country: Germany
167
270
  url: https://www.sosy-lab.org/people/lingsch-rosenfeld/
168
271
  - competition: "SV-COMP 2024"
169
- track: "Validation of Violation Witnesses 1.0"
272
+ track: "Validation of Violation Witnesses v1"
170
273
  label:
171
274
  - auxiliary
172
275
  tool_version: "svcomp24-violation-graphml"
@@ -177,7 +280,7 @@ competition_participations:
177
280
  country: Germany
178
281
  url: https://www.sosy-lab.org/people/lingsch-rosenfeld/
179
282
  - competition: "SV-COMP 2024"
180
- track: "Validation of Violation Witnesses 2.0"
283
+ track: "Validation of Violation Witnesses v2"
181
284
  label:
182
285
  - auxiliary
183
286
  tool_version: "svcomp24-violation-yaml"
@@ -33,13 +33,41 @@ maintainers:
33
33
  url: https://www.sosy-lab.org/people/lingsch-rosenfeld/
34
34
 
35
35
  versions:
36
+ - version: "0.0.2"
37
+ doi: 10.5281/zenodo.17769876
38
+ benchexec_toolinfo_options: []
39
+ required_ubuntu_packages:
40
+ - python3-yaml
41
+ base_container_images:
42
+ - docker.io/ubuntu:24.04
43
+ full_container_images:
44
+ - registry.gitlab.com/sosy-lab/benchmarking/competition-scripts/user:2026
36
45
  - version: "svcomp25"
37
46
  doi: 10.5281/zenodo.14205153
38
47
  benchexec_toolinfo_options: []
39
48
  required_ubuntu_packages:
40
49
  - python3-yaml
41
-
50
+ base_container_images:
51
+ - docker.io/ubuntu:24.04
52
+ full_container_images:
53
+ - registry.gitlab.com/sosy-lab/benchmarking/competition-scripts/user:2026
42
54
  competition_participations:
55
+ - competition: "SV-COMP 2026"
56
+ track: "Verification"
57
+ label:
58
+ - auxiliary
59
+ tool_version: "0.0.2"
60
+ jury_member:
61
+ orcid: 0000-0002-8172-3184
62
+ name: Marian Lingsch-Rosenfeld
63
+ institution: LMU Munich
64
+ country: Germany
65
+ url: https://www.sosy-lab.org/people/lingsch-rosenfeld/
66
+ participants:
67
+ - orcid: 0000-0003-4832-7662
68
+ name: Dirk Beyer
69
+ - orcid: 0000-0002-8172-3184
70
+ name: Marian Lingsch-Rosenfeld
43
71
  - competition: "SV-COMP 2025"
44
72
  track: "Verification"
45
73
  label:
fm_weck/zenodo.py ADDED
@@ -0,0 +1,381 @@
1
+ # This file is part of fm-weck: executing fm-tools in containerized environments.
2
+ # https://gitlab.com/sosy-lab/software/fm-weck
3
+ #
4
+ # SPDX-FileCopyrightText: 2024 Dirk Beyer <https://www.sosy-lab.org>
5
+ #
6
+ # SPDX-License-Identifier: Apache-2.0
7
+ import json
8
+ import logging
9
+ import os
10
+ from datetime import datetime
11
+ from pathlib import Path
12
+
13
+ import httpx
14
+ from dotenv import find_dotenv, load_dotenv, set_key
15
+ from tqdm import tqdm
16
+
17
+ from fm_weck.config import Config
18
+ from fm_weck.exceptions import ZenodoError
19
+ from fm_weck.image_mgr import ImageMgr
20
+
21
+
22
+ class TokenAuth(httpx.Auth):
23
+ def __init__(self, token: str):
24
+ self.token = token
25
+
26
+ def auth_flow(self, request):
27
+ request.headers["Authorization"] = f"Bearer {self.token}"
28
+ yield request
29
+
30
+
31
+ class ZenodoSession:
32
+ def __init__(self, access_token: str | None = None, is_sandbox: bool = False):
33
+ if is_sandbox:
34
+ self.base_url = "https://sandbox.zenodo.org/api"
35
+ else:
36
+ self.base_url = "https://zenodo.org/api"
37
+ self.deposit_url = self.base_url + "/deposit/depositions"
38
+ self.download_url = self.base_url + "/records"
39
+ self.access_token = access_token
40
+ if self.access_token is not None:
41
+ self.client = httpx.Client(auth=TokenAuth(self.access_token))
42
+ else:
43
+ self.client = httpx.Client()
44
+
45
+ def _require_access_token(self):
46
+ if self.access_token is None:
47
+ raise ZenodoError("An API access token is required to interact with the Zenodo API.")
48
+
49
+ def publish(self, image_info: tuple):
50
+ """
51
+ Publish an image on Zenodo.
52
+ """
53
+ self._require_access_token()
54
+
55
+ deposition_id = self.upload(image_info)
56
+ publish_url = self.deposit_url + "/" + deposition_id + "/" + "actions/publish"
57
+ r = httpx.post(publish_url)
58
+
59
+ if r.status_code >= 400:
60
+ self.delete_deposition(deposition_id)
61
+ raise ZenodoError(f"Error trying to publish an image: {r.json()['message']}")
62
+
63
+ return r.json()["doi"]
64
+
65
+ def upload(self, image_info: tuple[str, str, Path]) -> str:
66
+ """
67
+ Upload an image to Zenodo and leave it in draft mode (unpublished).
68
+ """
69
+ self._require_access_token()
70
+
71
+ image_name = image_info[0]
72
+ image_version = image_info[1]
73
+ image_path = image_info[2]
74
+ # Upload the actual file
75
+ upload_name = image_name + "_" + image_version + ".tar"
76
+ upload_name = upload_name.replace("/", "-")
77
+ metadata = self.get_upload_metadata(upload_name, image_version)
78
+ record_id = self.check_existing_records(metadata)
79
+ logging.debug(f"Record ID: {record_id}")
80
+ # Check if we are adding a new version or creating a new record
81
+ if record_id:
82
+ r = self.client.post(self.deposit_url + "/" + record_id + "/actions/newversion")
83
+ else:
84
+ logging.debug(f"{metadata=}")
85
+ r = self.client.post(
86
+ self.deposit_url,
87
+ json=metadata,
88
+ headers={"Content-Type": "application/json", "Authorization": f"Bearer {self.access_token}"},
89
+ )
90
+
91
+ if r.status_code >= 400:
92
+ logging.debug(f"{r.status_code} - {r.text}")
93
+ raise ZenodoError(f"Error trying to upload the deposition metadata to Zenodo: {r.json()['message']}")
94
+ bucket_url = r.json()["links"]["bucket"]
95
+ deposition_id = str(r.json()["id"])
96
+
97
+ if record_id:
98
+ self.prepare_new_version_upload(metadata, deposition_id)
99
+
100
+ r = self.upload_with_progress_bar(image_path, upload_name, bucket_url)
101
+ if r.status_code >= 400:
102
+ self.delete_deposition(deposition_id)
103
+ raise ZenodoError(f"Error trying to upload image to Zenodo: {r.json()['message']}")
104
+
105
+ return deposition_id
106
+
107
+ def upload_with_progress_bar(self, image_path, upload_name, bucket_url):
108
+ file_size = os.path.getsize(image_path)
109
+
110
+ with (
111
+ open(image_path, "rb") as image,
112
+ tqdm(total=file_size, unit="B", unit_scale=True, desc="Uploading") as progress_bar,
113
+ ):
114
+
115
+ def file_iterator():
116
+ chunk_size = 1024 * 1024 * 5 # 5 MB
117
+ while True:
118
+ chunk = image.read(chunk_size)
119
+ if not chunk:
120
+ break
121
+ yield chunk
122
+ progress_bar.update(len(chunk))
123
+
124
+ r = self.client.put(
125
+ f"{bucket_url}/{upload_name}",
126
+ data=file_iterator(),
127
+ headers={"Content-Length": str(file_size)},
128
+ timeout=300,
129
+ )
130
+
131
+ return r
132
+
133
+ def download(self, doi: str, download_path: str | None, no_progress_bar: bool = False) -> Path:
134
+ """
135
+ Download an image from Zenodo using its DOI.
136
+ """
137
+ download_url = self.resolve_download_info(doi)
138
+ image_name = download_url.split("/")[-2] + ".tar"
139
+ if no_progress_bar:
140
+ r = self.client.get(download_url)
141
+ if r.status_code >= 400:
142
+ raise ZenodoError(f"Error trying to download image: {r.json()['message']}")
143
+ content = r.content
144
+ else:
145
+ content = self.download_with_progress_bar(download_url)
146
+
147
+ return self.save_image(image_name, content, download_path)
148
+
149
+ def download_with_progress_bar(self, url: str):
150
+ content = bytearray()
151
+
152
+ with self.client.stream("GET", url) as response:
153
+ response.raise_for_status()
154
+
155
+ total_size = int(response.headers.get("Content-Length", 0))
156
+ chunk_size = 1024 * 1024 * 5 # 5 MB
157
+
158
+ with tqdm(total=total_size, unit="B", unit_scale=True, desc="Downloading") as progress_bar:
159
+ for chunk in response.iter_bytes(chunk_size):
160
+ content.extend(chunk)
161
+ progress_bar.update(len(chunk))
162
+
163
+ return content
164
+
165
+ def resolve_download_info(self, doi: str) -> str:
166
+ """
167
+ Extract the download URL from the deposition metadata.
168
+
169
+ :return: The download URL of the image.
170
+ """
171
+ doi_id = doi.split(".")[-1]
172
+ record_url = self.download_url + "/" + doi_id
173
+ r = self.client.get(record_url)
174
+ if r.status_code >= 400:
175
+ raise ZenodoError(f"Error trying to access the image record: {r.json()['message']}")
176
+
177
+ file_name = r.json()["files"][0]["key"]
178
+
179
+ return record_url + "/files/" + file_name + "/content"
180
+
181
+ def save_image(self, image_name: str, image: bytes, save_path):
182
+ save_path = Path().cwd() / image_name if not save_path else Path().cwd() / Path(save_path) / image_name
183
+ save_path.parent.mkdir(parents=True, exist_ok=True)
184
+ save_path.write_bytes(image)
185
+ return save_path
186
+
187
+ def delete_deposition(self, deposition_id: str):
188
+ r = self.client.delete(self.deposit_url + "/" + deposition_id)
189
+ if r.status_code >= 400:
190
+ raise ZenodoError(f"Error trying to delete a deposition resource from Zenodo: {r.json()['message']}")
191
+
192
+ def check_existing_records(self, metadata: dict):
193
+ """
194
+ Check if a record with the same name already exists in the depositions.
195
+ If it exists, prompt whether the image should be uploaded as a new version.
196
+
197
+ :return: The deposition ID in case of a positive prompt response, or an empty string.
198
+ """
199
+ r = self.client.get(self.deposit_url)
200
+ if r.status_code >= 400:
201
+ raise ZenodoError(f"Error trying to access the image record: {r.json()['message']}")
202
+
203
+ for record in r.json():
204
+ if metadata["metadata"]["title"] == record["title"]:
205
+ while True:
206
+ response = (
207
+ input(
208
+ (
209
+ "The record already exists. Do you want to upload this as a new version "
210
+ "of the existing record?\n"
211
+ "(y)es: upload as a new version of an existing record\n"
212
+ "(n)o: create a new record\n"
213
+ "(a)bort: cancel the upload\n"
214
+ )
215
+ )
216
+ .strip()
217
+ .lower()
218
+ )
219
+
220
+ if response == "y":
221
+ return str(record["id"])
222
+ elif response == "n":
223
+ break
224
+ elif response == "a":
225
+ exit(0)
226
+ return ""
227
+
228
+ def prepare_new_version_upload(self, metadata: dict, deposition_id: str) -> None:
229
+ """
230
+ Set up the metadata for the new version and delete the old versions' files from that version.
231
+ """
232
+ r = self.client.put(
233
+ self.deposit_url + "/" + deposition_id,
234
+ data=json.dumps(metadata), # type: ignore
235
+ )
236
+ if r.status_code >= 400:
237
+ raise ZenodoError(f"Error trying to upload the new version metadata to Zenodo: {r.json()['message']}")
238
+
239
+ for file in r.json()["files"]:
240
+ r = self.client.delete(
241
+ self.deposit_url + "/" + deposition_id + "/files/" + file["id"],
242
+ )
243
+ if r.status_code >= 400:
244
+ raise ZenodoError(
245
+ f"Error trying to delete old version files from the new version: {r.json()['message']}"
246
+ )
247
+
248
+ def get_upload_metadata(self, image_name: str, image_version: str) -> dict:
249
+ description = (
250
+ f"This is an image file.\n"
251
+ f"<p>In case of manual download, the image can be imported locally using the command: "
252
+ f"<code>[docker|podman] load --input {image_name}</code></p>"
253
+ )
254
+
255
+ upload_metadata = {
256
+ "metadata": {
257
+ "title": image_name,
258
+ "version": image_version,
259
+ "upload_type": "software",
260
+ "creators": [{"name": "fm-weck"}],
261
+ "publication_date": datetime.now().date().isoformat(),
262
+ "access_right": "open",
263
+ "license": "Apache-2.0",
264
+ "description": description,
265
+ }
266
+ }
267
+ return upload_metadata
268
+
269
+
270
+ class ZenodoMgr(object):
271
+ """
272
+ Zenodo API manager.
273
+
274
+ Args:
275
+ engine (object): The fm-weck engine object.
276
+ access_token (str, optional): The access token for authentication with the API. Defaults to None.
277
+ image (str, optional): The image to be published. Required if no doi is given. Defaults to None.
278
+ doi (str, optional): The DOI of the image to be pulled. Required if no image is given. Defaults to None.
279
+
280
+ Raises:
281
+ ValueError: If a chosen image is invalid or does not exist.
282
+ """
283
+
284
+ def __init__(
285
+ self,
286
+ engine,
287
+ access_token: str | None,
288
+ image: str | None,
289
+ doi: str | None,
290
+ is_sandbox: bool = False,
291
+ config: Config | None = None,
292
+ ):
293
+ access_token = access_token or ZenodoMgr.load_zenodo_token(config)
294
+
295
+ self.session = ZenodoSession(access_token, is_sandbox)
296
+ self.image = image
297
+ self.engine = engine
298
+ if image:
299
+ self.image_info = ImageMgr().prepare_image_for_zenodo(engine, image)
300
+ self.doi = doi
301
+
302
+ def publish(self):
303
+ """
304
+ Publish an image on Zenodo.
305
+
306
+ Raises:
307
+ ZenodoError: If Zenodo API returns an error.
308
+ """
309
+ if self.image is None:
310
+ raise ValueError("No image specified for publishing.")
311
+ print("Publishing image to Zenodo...")
312
+ doi = self.session.publish(self.image_info)
313
+ print("Image successfully published.")
314
+ ImageMgr().tag_image(self.engine, self.image, doi)
315
+
316
+ def push(self):
317
+ """
318
+ Upload an image to Zenodo and leave it in draft mode (unpublished).
319
+
320
+ Raises:
321
+ ZenodoError: If Zenodo API returns an error.
322
+ """
323
+ if self.image is None:
324
+ raise ValueError("No image specified for pushing.")
325
+ logging.info(f"Uploading image to Zenodo... {self.image_info}")
326
+ self.session.upload(self.image_info)
327
+ print("Image successfully uploaded.")
328
+
329
+ def pull(self, download_path: str | None, force_pull: bool = False):
330
+ """
331
+ Download an image from Zenodo using its DOI.
332
+
333
+ Raises:
334
+ ValueError: If no DOI is provided.
335
+ ZenodoError: If Zenodo API returns an error.
336
+ """
337
+ if self.doi:
338
+ doi = ImageMgr().check_if_doi_exists_locally(self.engine, self.doi)
339
+ else:
340
+ raise ValueError("No DOI provided.")
341
+
342
+ if not force_pull and not doi:
343
+ print("Image already exists locally. Pulling aborted.")
344
+ return
345
+
346
+ print("Pulling image from Zenodo...")
347
+ tar_path = self.session.download(self.doi, download_path)
348
+ ImageMgr().load_image(self.engine, tar_path, self.doi)
349
+ print("Image successfully pulled.")
350
+ tar_path.unlink()
351
+
352
+ @staticmethod
353
+ def save_zenodo_token(config, token):
354
+ token_path = find_dotenv(filename=".zenodo_token")
355
+ token_path = Path.cwd() / config.get("defaults").get("token_location") if not token_path else Path(token_path)
356
+
357
+ print(f"This action will overwrite the token at: {token_path}\nDo you wish to continue? [y/N]")
358
+ if input().lower() == "y":
359
+ load_dotenv(token_path)
360
+ set_key(token_path, "ZENODO_TOKEN", token)
361
+ print(f"Token saved to: {token_path}")
362
+ else:
363
+ print("Action aborted.")
364
+
365
+ @staticmethod
366
+ def load_zenodo_token(config):
367
+ """
368
+ Load the Zenodo token from the Zenodo config file.
369
+
370
+ Raises:
371
+ ZenodoError: If no Zenodo token is found.
372
+ """
373
+ token_path = find_dotenv(filename=".zenodo_token")
374
+ token_path = Path.cwd() / config.get("defaults").get("token_location") if not token_path else Path(token_path)
375
+ load_dotenv(Path(token_path))
376
+ token = os.getenv("ZENODO_TOKEN")
377
+
378
+ if token:
379
+ print(f"Using token from: {token_path}\n")
380
+
381
+ return token
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fm-weck
3
- Version: 1.5.3
3
+ Version: 1.6.0
4
4
  Author-email: Henrik Wachowitz <henrik.wachowitz@ifi.lmu.de>
5
5
  Maintainer-email: Henrik Wachowitz <henrik.wachowitz@ifi.lmu.de>
6
6
  Classifier: Development Status :: 4 - Beta
@@ -11,6 +11,7 @@ Classifier: Programming Language :: Python :: 3.11
11
11
  Classifier: Programming Language :: Python :: 3.12
12
12
  Requires-Python: >=3.10
13
13
  Requires-Dist: argcomplete
14
+ Requires-Dist: dotenv
14
15
  Requires-Dist: fm-tools>=1.0.0
15
16
  Requires-Dist: pyyaml>=6.0
16
17
  Requires-Dist: tabulate