primitive 0.1.1__py3-none-any.whl → 0.1.2__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.
@@ -0,0 +1,447 @@
1
+ import csv
2
+ import io
3
+ import json
4
+ import platform
5
+ from shutil import which
6
+ import subprocess
7
+ from typing import Dict, List
8
+ import click
9
+ from loguru import logger
10
+ from primitive.utils.memory_size import MemorySize
11
+ from gql import gql
12
+ from aiohttp import client_exceptions
13
+ from ..utils.config import update_config_file
14
+
15
+ import typing
16
+
17
+ if typing.TYPE_CHECKING:
18
+ pass
19
+
20
+
21
+ from primitive.utils.actions import BaseAction
22
+
23
+
24
+ class Hardware(BaseAction):
25
+ def __init__(self, *args, **kwargs) -> None:
26
+ super().__init__(*args, **kwargs)
27
+ self.previous_state = {
28
+ "isHealthy": False,
29
+ "isQuarantined": False,
30
+ "isAvailable": False,
31
+ "isOnline": False,
32
+ }
33
+
34
+ def _get_darwin_system_profiler_values(self) -> Dict[str, str]:
35
+ system_profiler_hardware_data_type = subprocess.check_output(
36
+ ["system_profiler", "SPHardwareDataType", "-json"]
37
+ )
38
+ system_profiler_hardware_data = json.loads(system_profiler_hardware_data_type)
39
+ data_type = system_profiler_hardware_data.get("SPHardwareDataType")[0]
40
+ return {
41
+ "apple_model_name": data_type.get("machine_model"),
42
+ "apple_model_identifier": data_type.get("machine_name"),
43
+ "apple_model_number": data_type.get("model_number"),
44
+ "physical_memory": data_type.get("physical_memory"),
45
+ "apple_serial_number": data_type.get("serial_number"),
46
+ }
47
+
48
+ def _get_supported_metal_device(self) -> int | None:
49
+ """
50
+ Checks if metal hardware is supported. If so, the index
51
+ of the supported metal device is returned
52
+ """
53
+ supported_metal_device = None
54
+ is_system_profiler_available = bool(which("system_profiler"))
55
+ if is_system_profiler_available:
56
+ system_profiler_display_data_type_command = (
57
+ "system_profiler SPDisplaysDataType -json"
58
+ )
59
+ try:
60
+ system_profiler_display_data_type_output = subprocess.check_output(
61
+ system_profiler_display_data_type_command.split(" ")
62
+ )
63
+ except subprocess.CalledProcessError as exception:
64
+ message = f"Error running system_profiler: {exception}"
65
+ logger.error(message)
66
+ return supported_metal_device
67
+
68
+ try:
69
+ system_profiler_display_data_type_json = json.loads(
70
+ system_profiler_display_data_type_output
71
+ )
72
+ except json.JSONDecodeError as exception:
73
+ message = f"Error decoding JSON: {exception}"
74
+ logger.error(message)
75
+ return supported_metal_device
76
+
77
+ # Checks if any attached displays have metal support
78
+ # Note, other devices here could be AMD GPUs or unconfigured Nvidia GPUs
79
+ for index, display in enumerate(
80
+ system_profiler_display_data_type_json["SPDisplaysDataType"]
81
+ ):
82
+ if "spdisplays_mtlgpufamilysupport" in display:
83
+ supported_metal_device = index
84
+ return supported_metal_device
85
+
86
+ return supported_metal_device
87
+
88
+ def _get_gpu_config(self) -> List:
89
+ """
90
+ For Nvidia based systems, nvidia-smi will be used to profile the gpu/s.
91
+ For Metal based systems, we will gather information from SPDisplaysDataType.
92
+ """
93
+ gpu_config = []
94
+
95
+ # Check nvidia gpu availability
96
+ is_nvidia_smi_available = bool(which("nvidia-smi"))
97
+ if is_nvidia_smi_available:
98
+ nvidia_smi_query_gpu_csv_command = "nvidia-smi --query-gpu=gpu_name,driver_version,memory.total --format=csv" # noqa
99
+ try:
100
+ nvidia_smi_query_gpu_csv_output = subprocess.check_output(
101
+ nvidia_smi_query_gpu_csv_command.split(" "),
102
+ )
103
+ except subprocess.CalledProcessError as exception:
104
+ message = f"Command {nvidia_smi_query_gpu_csv_command} failed with exception: {exception}" # noqa
105
+ logger.error(message)
106
+ raise exception
107
+
108
+ try:
109
+ nvidia_smi_query_gpu_csv_decoded = (
110
+ nvidia_smi_query_gpu_csv_output.decode("utf-8")
111
+ .replace("\r", "")
112
+ .replace(", ", ",")
113
+ .lstrip("\n")
114
+ )
115
+ except UnicodeDecodeError as exception:
116
+ message = f"Error decoding: {exception}"
117
+ logger.error(message)
118
+ raise exception
119
+
120
+ nvidia_smi_query_gpu_csv_dict_reader = csv.DictReader(
121
+ io.StringIO(nvidia_smi_query_gpu_csv_decoded)
122
+ )
123
+
124
+ for gpu_info in nvidia_smi_query_gpu_csv_dict_reader:
125
+ # Refactor key into B
126
+ memory_total_in_mebibytes = gpu_info.pop("memory.total [MiB]")
127
+ memory_size = MemorySize(memory_total_in_mebibytes)
128
+ gpu_info["memory_total"] = memory_size.to_bytes()
129
+
130
+ gpu_config.append(gpu_info)
131
+
132
+ if platform.system() == "Darwin":
133
+ # Check Metal gpu availability
134
+ supported_metal_device = self._get_supported_metal_device()
135
+ if supported_metal_device is not None:
136
+ # Since Apple's SoC contains Metal,
137
+ # we query the system itself for total memory
138
+ system_profiler_hardware_data_type_command = (
139
+ "system_profiler SPHardwareDataType -json"
140
+ )
141
+
142
+ try:
143
+ system_profiler_hardware_data_type_output = subprocess.check_output(
144
+ system_profiler_hardware_data_type_command.split(" ")
145
+ )
146
+ except subprocess.CalledProcessError as exception:
147
+ message = f"Error running {system_profiler_hardware_data_type_command}: {exception}" # noqa
148
+ logger.error(message)
149
+ raise exception
150
+
151
+ try:
152
+ system_profiler_hardware_data_type_json = json.loads(
153
+ system_profiler_hardware_data_type_output
154
+ )
155
+ except json.JSONDecodeError as exception:
156
+ message = f"Error decoding JSON: {exception}" # noqa
157
+ logger.error(message)
158
+ raise exception
159
+
160
+ metal_device_json = system_profiler_hardware_data_type_json[
161
+ "SPHardwareDataType"
162
+ ][supported_metal_device]
163
+
164
+ gpu_info = {}
165
+ gpu_info["name"] = metal_device_json.get("chip_type")
166
+
167
+ # Refactor key into B
168
+ physical_memory = metal_device_json.get("physical_memory")
169
+ memory_size = MemorySize(physical_memory)
170
+ gpu_info["memory_total"] = memory_size.to_bytes()
171
+
172
+ gpu_config.append(gpu_info)
173
+
174
+ # Raise an error if there is no valid gpu config
175
+ if not gpu_config:
176
+ message = "No valid gpu configuration"
177
+ logger.error(message)
178
+ raise NotImplementedError(message)
179
+
180
+ return gpu_config
181
+
182
+ def _get_windows_computer_service_product_values(self) -> Dict[str, str]:
183
+ windows_computer_service_product_csv_command = (
184
+ "cmd.exe /C wmic csproduct get Name, Vendor, Version, UUID /format:csv"
185
+ )
186
+ windows_computer_service_product_csv_output = subprocess.check_output(
187
+ windows_computer_service_product_csv_command.split(" "),
188
+ stderr=subprocess.DEVNULL,
189
+ )
190
+ windows_computer_service_product_csv_decoded = (
191
+ windows_computer_service_product_csv_output.decode("utf-8")
192
+ .replace("\r", "")
193
+ .lstrip("\n")
194
+ )
195
+ windows_computer_service_product_dict = csv.DictReader(
196
+ io.StringIO(windows_computer_service_product_csv_decoded)
197
+ )
198
+ csp_info = list(windows_computer_service_product_dict)[0]
199
+ return {
200
+ "windows_model_name": csp_info.get("Name", ""),
201
+ "windows_model_vendor": csp_info.get("Vendor", ""),
202
+ "windows_model_version": csp_info.get("Version", ""),
203
+ "windows_model_uuid": csp_info.get("UUID", ""),
204
+ }
205
+
206
+ def _get_windows_cpu_values(self) -> Dict[str, str]:
207
+ windows_cpu_csv_command = (
208
+ "cmd.exe /C wmic cpu get Name, MaxClockSpeed /format:csv" # noqa
209
+ )
210
+ windows_cpu_csv_output = subprocess.check_output(
211
+ windows_cpu_csv_command.split(" "),
212
+ stderr=subprocess.DEVNULL,
213
+ )
214
+ windows_cpu_csv_decoded = (
215
+ windows_cpu_csv_output.decode("utf-8").replace("\r", "").lstrip("\n")
216
+ )
217
+ windows_cpu_dict = csv.DictReader(io.StringIO(windows_cpu_csv_decoded))
218
+ cpu_info = list(windows_cpu_dict)[0]
219
+ return {
220
+ "cpu_brand": cpu_info.get("Name", "").strip(),
221
+ "cpu_max_clock_speed": cpu_info.get("MaxClockSpeed", ""),
222
+ }
223
+
224
+ def _get_ubuntu_values(self) -> Dict[str, str]:
225
+ get_machine_id_command = "cat /etc/machine-id"
226
+ machine_id = subprocess.check_output(
227
+ get_machine_id_command.split(" "),
228
+ stderr=subprocess.DEVNULL,
229
+ ).decode("utf-8")
230
+ if machine_id:
231
+ return {"linux_machine_id": machine_id}
232
+ return {}
233
+
234
+ def get_system_info(self):
235
+ os_family = platform.system()
236
+ system_info = {}
237
+ if os_family == "Darwin":
238
+ system_info = {**system_info, **self._get_darwin_system_profiler_values()}
239
+ system_info["cpu_brand"] = (
240
+ subprocess.check_output(["sysctl", "-n", "machdep.cpu.brand_string"])
241
+ .strip()
242
+ .decode("utf-8")
243
+ )
244
+ system_info["apple_mac_os_version"] = platform.mac_ver()[0]
245
+ elif os_family == "Linux":
246
+ # Support for Linux-based VMs in Windows
247
+ if "WSL2" in platform.platform():
248
+ system_info = {
249
+ **system_info,
250
+ **self._get_windows_computer_service_product_values(),
251
+ **self._get_windows_cpu_values(),
252
+ }
253
+ else:
254
+ system_info = {**system_info, **self._get_ubuntu_values()}
255
+ elif os_family == "Windows":
256
+ system_info = {
257
+ **system_info,
258
+ **self._get_windows_computer_service_product_values(),
259
+ **self._get_windows_cpu_values(),
260
+ }
261
+
262
+ system_info["name"] = platform.node()
263
+ system_info["os_family"] = os_family
264
+ system_info["os_release"] = platform.release()
265
+ system_info["os_version"] = platform.version()
266
+ system_info["platform"] = platform.platform()
267
+ system_info["processor"] = platform.processor()
268
+ system_info["machine"] = platform.machine()
269
+ system_info["architecture"] = platform.architecture()[0]
270
+ system_info["cpu_cores"] = str(platform.os.cpu_count()) # type: ignore exits
271
+ system_info["gpu_config"] = self._get_gpu_config()
272
+ return system_info
273
+
274
+ def register(self):
275
+ system_info = self.get_system_info()
276
+ mutation = gql(
277
+ """
278
+ mutation registerHardware($input: RegisterHardwareInput!) {
279
+ registerHardware(input: $input) {
280
+ ... on Hardware {
281
+ fingerprint
282
+ }
283
+ ... on OperationInfo {
284
+ messages {
285
+ kind
286
+ message
287
+ field
288
+ code
289
+ }
290
+ }
291
+ }
292
+ }
293
+ """
294
+ )
295
+ input = {"systemInfo": system_info}
296
+ variables = {"input": input}
297
+ result = self.primitive.session.execute(mutation, variable_values=variables)
298
+ if messages := result.get("registerHardware").get("messages"):
299
+ for message in messages:
300
+ logger.enable("primitive")
301
+ if message.get("kind") == "ERROR":
302
+ logger.error(message.get("message"))
303
+ else:
304
+ logger.debug(message.get("message"))
305
+ return False
306
+
307
+ fingerprint = result.get("registerHardware").get("fingerprint")
308
+
309
+ self.primitive.host_config["fingerprint"] = fingerprint
310
+ self.primitive.full_config[self.primitive.host] = self.primitive.host_config
311
+ update_config_file(new_config=self.primitive.full_config)
312
+
313
+ # then check in that the hardware, validate that it is saved correctly
314
+ # and headers are set correctly
315
+ self.primitive.get_host_config()
316
+ self.check_in_http(is_healthy=True)
317
+ return True
318
+
319
+ def update_hardware_system_info(self):
320
+ """
321
+ Updates hardware system information and returns the GraphQL response.
322
+
323
+ Returns:
324
+ dict: GraphQL response
325
+ Raises:
326
+ Exception: If no fingerprint is found or an error occurs
327
+ """
328
+
329
+ fingerprint = self.primitive.host_config.get("fingerprint", None)
330
+ if not fingerprint:
331
+ message = (
332
+ "No fingerprint found. Please register: primitive hardware register"
333
+ )
334
+ raise Exception(message)
335
+
336
+ system_info = self.get_system_info()
337
+ new_state = {
338
+ "systemInfo": system_info,
339
+ }
340
+
341
+ mutation = gql(
342
+ """
343
+ mutation hardwareUpdate($input: HardwareUpdateInput!) {
344
+ hardwareUpdate(input: $input) {
345
+ ... on Hardware {
346
+ systemInfo
347
+ }
348
+ ... on OperationInfo {
349
+ messages {
350
+ kind
351
+ message
352
+ field
353
+ code
354
+ }
355
+ }
356
+ }
357
+ }
358
+ """
359
+ )
360
+
361
+ input = new_state
362
+ variables = {"input": input}
363
+ try:
364
+ result = self.primitive.session.execute(mutation, variable_values=variables)
365
+ except client_exceptions.ClientConnectorError as exception:
366
+ message = " [*] Failed to update hardware system info! "
367
+ logger.error(message)
368
+ raise exception
369
+
370
+ message = " [*] Updated hardware system info successfully! "
371
+ logger.info(message)
372
+
373
+ return result
374
+
375
+ def check_in_http(
376
+ self,
377
+ is_healthy: bool = True,
378
+ is_quarantined: bool = False,
379
+ is_available: bool = False,
380
+ is_online: bool = True,
381
+ ):
382
+ fingerprint = self.primitive.host_config.get("fingerprint", None)
383
+ if not fingerprint:
384
+ message = (
385
+ "No fingerprint found. Please register: primitive hardware register"
386
+ )
387
+ raise Exception(message)
388
+
389
+ new_state = {
390
+ "isHealthy": is_healthy,
391
+ "isQuarantined": is_quarantined,
392
+ "isAvailable": is_available,
393
+ "isOnline": is_online,
394
+ }
395
+
396
+ mutation = gql(
397
+ """
398
+ mutation checkIn($input: CheckInInput!) {
399
+ checkIn(input: $input) {
400
+ ... on Hardware {
401
+ createdAt
402
+ updatedAt
403
+ lastCheckIn
404
+ }
405
+ ... on OperationInfo {
406
+ messages {
407
+ kind
408
+ message
409
+ field
410
+ code
411
+ }
412
+ }
413
+ }
414
+ }
415
+ """ # noqa
416
+ )
417
+ input = new_state
418
+ variables = {"input": input}
419
+ try:
420
+ result = self.primitive.session.execute(mutation, variable_values=variables)
421
+ previous_state = self.previous_state
422
+ self.previous_state = new_state.copy()
423
+
424
+ message = " [*] Checked in successfully: "
425
+ for key, value in new_state.items():
426
+ if value != previous_state.get(key, None):
427
+ if value is True:
428
+ message = (
429
+ message
430
+ + click.style(f"{key}: ")
431
+ + click.style("💤")
432
+ + click.style(" ==> ✅ ", fg="green")
433
+ )
434
+ else:
435
+ message = (
436
+ message
437
+ + click.style(f"{key}: ")
438
+ + click.style("✅")
439
+ + click.style(" ==> 💤 ", fg="yellow")
440
+ )
441
+ logger.info(message)
442
+
443
+ return result
444
+ except client_exceptions.ClientConnectorError as exception:
445
+ message = " [*] Failed to check in! "
446
+ logger.error(message)
447
+ raise exception
@@ -0,0 +1,53 @@
1
+ import click
2
+
3
+ from ..utils.printer import print_result
4
+
5
+ import typing
6
+
7
+ if typing.TYPE_CHECKING:
8
+ from ..client import Primitive
9
+
10
+
11
+ @click.group()
12
+ @click.pass_context
13
+ def cli(context):
14
+ """Hardware"""
15
+ pass
16
+
17
+
18
+ @cli.command("systeminfo")
19
+ @click.pass_context
20
+ def systeminfo_command(context):
21
+ """Get System Info"""
22
+ primitive: Primitive = context.obj.get("PRIMITIVE")
23
+ message = primitive.hardware.get_system_info()
24
+ print_result(message=message, context=context)
25
+
26
+
27
+ @cli.command("register")
28
+ @click.pass_context
29
+ def register_command(context):
30
+ """Register Hardware with Primitive"""
31
+ primitive: Primitive = context.obj.get("PRIMITIVE")
32
+ result = primitive.hardware.register()
33
+ color = "green" if result else "red"
34
+ if result:
35
+ message = "Hardware registered successfully"
36
+ else:
37
+ message = (
38
+ "There was an error registering this device. Please review the above logs."
39
+ )
40
+ print_result(message=message, context=context, fg=color)
41
+
42
+
43
+ @cli.command("checkin")
44
+ @click.pass_context
45
+ def checkin_command(context):
46
+ """Checkin Hardware with Primitive"""
47
+ primitive: Primitive = context.obj.get("PRIMITIVE")
48
+ result = primitive.hardware.check_in_http()
49
+ if messages := result.get("checkIn").get("messages"):
50
+ print_result(message=messages, context=context, fg="yellow")
51
+ else:
52
+ message = "Hardware checked in successfully"
53
+ print_result(message=message, context=context, fg="green")
@@ -0,0 +1,6 @@
1
+ from primitive.utils.actions import BaseAction
2
+
3
+
4
+ class Lint(BaseAction):
5
+ def run_lint(self):
6
+ print("lint 100%")
@@ -0,0 +1,15 @@
1
+ import click
2
+
3
+
4
+ import typing
5
+
6
+ if typing.TYPE_CHECKING:
7
+ from ..client import Primitive
8
+
9
+
10
+ @click.command("lint")
11
+ @click.pass_context
12
+ def cli(context):
13
+ """Lint"""
14
+ primitive: Primitive = context.obj.get("PRIMITIVE")
15
+ primitive.lint.run_lint()
File without changes
@@ -0,0 +1,55 @@
1
+ from typing import List, Optional
2
+ from gql import gql
3
+
4
+
5
+ from primitive.utils.actions import BaseAction
6
+
7
+
8
+ class Projects(BaseAction):
9
+ def get_job_run(self, id: str):
10
+ query = gql(
11
+ """
12
+ query jobRun($id: GlobalID!) {
13
+ jobRun(id: $id) {
14
+ id
15
+ organization {
16
+ id
17
+ }
18
+ }
19
+ }
20
+ """
21
+ )
22
+ variables = {"id": id}
23
+ result = self.primitive.session.execute(query, variable_values=variables)
24
+ return result
25
+
26
+ def job_run_update(
27
+ self,
28
+ id: str,
29
+ status: str = None,
30
+ conclusion: str = None,
31
+ file_ids: Optional[List[str]] = [],
32
+ ):
33
+ mutation = gql(
34
+ """
35
+ mutation jobRunUpdate($input: JobRunUpdateInput!) {
36
+ jobRunUpdate(input: $input) {
37
+ ... on JobRun {
38
+ id
39
+ status
40
+ conclusion
41
+ }
42
+ }
43
+ }
44
+ """
45
+ )
46
+ input = {"id": id}
47
+ if status:
48
+ input["status"] = status
49
+ if conclusion:
50
+ input["conclusion"] = conclusion
51
+ if file_ids and len(file_ids) > 0:
52
+ input["files"] = file_ids
53
+ variables = {"input": input}
54
+ result = self.primitive.session.execute(mutation, variable_values=variables)
55
+ return result
File without changes
@@ -0,0 +1,48 @@
1
+ from gql import gql
2
+
3
+
4
+ from primitive.utils.actions import BaseAction
5
+
6
+
7
+ class Simulations(BaseAction):
8
+ def trace_create(
9
+ self,
10
+ id_code: str,
11
+ module: str,
12
+ var_type: str,
13
+ var_size: int,
14
+ reference: str,
15
+ bit_index: str,
16
+ timescale_unit: str,
17
+ timescale_magnitude: int,
18
+ organization: str,
19
+ file: str,
20
+ job_run: str,
21
+ ):
22
+ mutation = gql(
23
+ """
24
+ mutation createTrace($input: TraceCreateInput!) {
25
+ traceCreate(input: $input) {
26
+ ... on Trace {
27
+ id
28
+ }
29
+ }
30
+ }
31
+ """
32
+ )
33
+ input = {
34
+ "idCode": id_code,
35
+ "module": module,
36
+ "varType": var_type,
37
+ "varSize": var_size,
38
+ "reference": reference,
39
+ "bitIndex": bit_index,
40
+ "timescaleUnit": timescale_unit,
41
+ "timescaleMagnitude": timescale_magnitude,
42
+ "organization": organization,
43
+ "file": file,
44
+ "jobRun": job_run,
45
+ }
46
+ variables = {"input": input}
47
+ result = self.primitive.session.execute(mutation, variable_values=variables)
48
+ return result
@@ -0,0 +1,9 @@
1
+ import typing
2
+
3
+ if typing.TYPE_CHECKING:
4
+ import primitive.client
5
+
6
+
7
+ class BaseAction:
8
+ def __init__(self, primitive: "primitive.client.Primitive") -> None:
9
+ self.primitive = primitive
@@ -0,0 +1,34 @@
1
+ import json
2
+ from pathlib import Path
3
+ from typing import Dict
4
+
5
+ HOME_DIRECTORY = Path.home()
6
+ PRIMITIVE_CREDENTIALS_FILEPATH = Path(
7
+ HOME_DIRECTORY / ".config" / "primitive" / "credentials.json"
8
+ )
9
+
10
+
11
+ def create_directory(filepath: Path = PRIMITIVE_CREDENTIALS_FILEPATH):
12
+ filepath.mkdir(parents=True, exist_ok=True)
13
+
14
+
15
+ def create_config_file(filepath: Path = PRIMITIVE_CREDENTIALS_FILEPATH):
16
+ filepath.parent.mkdir(parents=True, exist_ok=True)
17
+ filepath.touch()
18
+ with filepath.open("w") as json_file:
19
+ json.dump({}, json_file)
20
+
21
+
22
+ def update_config_file(
23
+ filepath: Path = PRIMITIVE_CREDENTIALS_FILEPATH, new_config: Dict = {}
24
+ ):
25
+ existing_config = read_config_file(filepath=filepath)
26
+ merged_config = {**existing_config, **new_config}
27
+ with filepath.open("w") as json_file:
28
+ json.dump(merged_config, json_file, indent=2)
29
+
30
+
31
+ def read_config_file(filepath: Path = PRIMITIVE_CREDENTIALS_FILEPATH) -> Dict:
32
+ if not filepath.exists():
33
+ create_config_file(filepath=filepath)
34
+ return json.loads(filepath.read_text())
primitive/utils/git.py ADDED
@@ -0,0 +1,15 @@
1
+ import os
2
+ from loguru import logger
3
+
4
+
5
+ def download_source(github_access_token, git_repository, git_ref) -> None:
6
+ # Download code to current directory
7
+ logger.debug(f"Downloading source code from {git_repository} {git_ref}")
8
+ url = f"https://api.github.com/repos/{git_repository}/tarball/{git_ref}"
9
+ # TODO: switch to supbrocess.run or subprocess.Popen
10
+ result = os.system(
11
+ f"curl -s -L -H 'Accept: application/vnd.github+json' -H 'Authorization: Bearer {github_access_token}' -H 'X-GitHub-Api-Version: 2022-11-28' {url} | tar zx --strip-components 1 -C ."
12
+ )
13
+
14
+ if result != 0:
15
+ raise Exception("Failed to import repository.")