def load_capsule(path: Union[str, Path], key=None, inference_mode=True) -> BaseCapsule: """Load a capsule from the filesystem. :param path: The path to the capsule file :param key: The AES key to decrypt the capsule with, or None if the capsule is not encrypted :param inference_mode: If True, the backends for this capsule will be started. If False, the capsule will never be able to run inference, but it will still have it's various readable attributes. """ path = Path(path) loaded_files = {} if key is None: # Capsule is unencrypted and already a zip file capsule_data = path.read_bytes() else: # Decrypt the capsule into its original form, a zip file capsule_data = decrypt_file(path, key) file_like = BytesIO(capsule_data) code = None with ZipFile(file_like, "r") as capsule_file: if CAPSULE_FILE_NAME not in capsule_file.namelist(): raise RuntimeError(f"Capsule {path} has no {CAPSULE_FILE_NAME}") if META_FILE_NAME not in capsule_file.namelist(): raise IncompatibleCapsuleError( f"Capsule {path} has no {META_FILE_NAME}") for name in capsule_file.namelist(): if name == CAPSULE_FILE_NAME: # Every capsule has a capsule.py file defining the capsule's # behavior code = capsule_file.read(CAPSULE_FILE_NAME) else: # Load all other files as well loaded_files[name] = capsule_file.read(name) # Read the meta.conf and get the OpenVisionCapsules API compatibility # version meta_conf = configparser.ConfigParser() meta_conf.read_string(loaded_files[META_FILE_NAME].decode("utf-8")) compatibility_version = meta_conf["about"]["api_compatibility_version"] match = MAJOR_MINOR_SEMVER_PATTERN.fullmatch(compatibility_version) if match is None: raise InvalidCapsuleError( f"Invalid API compatibility version format " f"'{compatibility_version}'. Version must be in the format " f"'[major].[minor]'.") try: major, minor = map(int, (match[1], match[2])) except ValueError: raise InvalidCapsuleError( f"Compatibility versions must be numbers, got " f"{major}.{minor}.") if major != MAJOR_COMPATIBLE_VERSION: raise IncompatibleCapsuleError( f"Capsule {path} is not compatible with this software. The " f"capsule's OpenVisionCapsules required major version is " f"{major} but this software uses OpenVisionCapsules " f"{MAJOR_COMPATIBLE_VERSION}.{MINOR_COMPATIBLE_VERSION}.") if minor > MINOR_COMPATIBLE_VERSION: raise IncompatibleCapsuleError( f"Capsule {path} requires a version of OpenVisionCapsules " f"that is too new for this software. The capsule requires at " f"least version {major}.{minor} but this software uses " f"OpenVisionCapsules " f"{MAJOR_COMPATIBLE_VERSION}.{MINOR_COMPATIBLE_VERSION}.") # With the capsule's code loaded, initialize the object capsule_module = ModuleType(path.stem) try: # Allow the capsule.py to import other files in the capsule capsule_dir_path = (path.parent / path.stem).absolute() sys.meta_path.insert(1, ZipFinder(capsule_file, capsule_dir_path)) # Run the capsule compiled = compile(code, capsule_dir_path / "capsule.py", "exec") exec(compiled, capsule_module.__dict__) except Exception as e: raise InvalidCapsuleError( "Could not execute the code in the capsule!\n" f"File: {path}\n" f"Error: {e}") finally: # Remove custom import code sys.meta_path.pop(1) # noinspection PyUnresolvedReferences new_capsule: BaseCapsule = capsule_module.Capsule( capsule_files=loaded_files, inference_mode=inference_mode) try: _validate_capsule(new_capsule) except InvalidCapsuleError as e: logging.warning(f"Failed to load capsule {path}") new_capsule.close() raise e return new_capsule
def load_capsule_from_bytes(data: bytes, source_path: Optional[Path] = None, key: Optional[str] = None, inference_mode: bool = True) -> BaseCapsule: """Loads a capsule from the given bytes. :param data: The data of the capsule :param source_path: The path to the capsule's source code, if it's available at runtime :param key: The AES key to decrypt the capsule with, or None if the capsule is not encrypted :param inference_mode: If True, the backends for this capsule will be started. If False, the capsule will never be able to run inference, but it will still have it's various readable attributes. :return: The loaded capsule object """ module_name = capsule_module_name(data) if source_path is None: # The source is unavailable. Use a dummy path source_path = Path("/", module_name) if key is not None: # Decrypt the capsule into its original form, a zip file data = decrypt(key, data) file_like = BytesIO(data) loaded_files = {} code = None with ZipFile(file_like, "r") as capsule_file: if CAPSULE_FILE_NAME not in capsule_file.namelist(): raise InvalidCapsuleError(f"Missing file {CAPSULE_FILE_NAME}") if META_FILE_NAME not in capsule_file.namelist(): raise InvalidCapsuleError(f"Missing file {META_FILE_NAME}") for name in capsule_file.namelist(): if name == CAPSULE_FILE_NAME: # Every capsule has a capsule.py file defining the capsule's # behavior code = capsule_file.read(CAPSULE_FILE_NAME) else: # Load all other files as well loaded_files[name] = capsule_file.read(name) # Read the meta.conf and get the OpenVisionCapsules API compatibility # version meta_conf = configparser.ConfigParser() meta_conf.read_string(loaded_files[META_FILE_NAME].decode("utf-8")) compatibility_version = meta_conf["about"]["api_compatibility_version"] match = MAJOR_MINOR_SEMVER_PATTERN.fullmatch(compatibility_version) if match is None: raise InvalidCapsuleError( f"Invalid API compatibility version format " f"'{compatibility_version}'. Version must be in the format " f"'[major].[minor]'.") try: major, minor = map(int, (match[1], match[2])) except ValueError: raise InvalidCapsuleError( f"Compatibility versions must be numbers, got " f"{major}.{minor}.") if major != MAJOR_COMPATIBLE_VERSION: raise IncompatibleCapsuleError( f"The capsule is not compatible with this software. The " f"capsule's OpenVisionCapsules required major version is " f"{major} but this software uses OpenVisionCapsules " f"{MAJOR_COMPATIBLE_VERSION}.{MINOR_COMPATIBLE_VERSION}.") elif (MAJOR_COMPATIBLE_VERSION == 0 and minor != MINOR_COMPATIBLE_VERSION): # Because vcap has not yet reached a 1.0 API, while the major # version is 0 then minor version will be required to match. raise IncompatibleCapsuleError( "The capsule is not compatible with this software. The " "capsule's OpenVisionCapsules required version is " f"{major}.{minor}, but this software uses OpenVisionCapsules " f"{MAJOR_COMPATIBLE_VERSION}.{MINOR_COMPATIBLE_VERSION}") if minor > MINOR_COMPATIBLE_VERSION: raise IncompatibleCapsuleError( f"The capsule requires a version of OpenVisionCapsules " f"that is too new for this software. The capsule requires at " f"least version {major}.{minor} but this software uses " f"OpenVisionCapsules " f"{MAJOR_COMPATIBLE_VERSION}.{MINOR_COMPATIBLE_VERSION}.") # With the capsule's code loaded, initialize the object capsule_module = ModuleType(module_name) # Allow the capsule.py to import other files in the capsule sys.meta_path.insert(1, ZipFinder(capsule_file, source_path, module_name)) try: # Run the capsule compiled = compile(code, source_path / CAPSULE_FILE_NAME, "exec") exec(compiled, capsule_module.__dict__) except Exception as e: raise InvalidCapsuleError( f"Could not execute the code in the capsule!\n" f"Error: {e}") finally: # Remove custom import code sys.meta_path.pop(1) # noinspection PyUnresolvedReferences new_capsule: BaseCapsule = capsule_module.Capsule( capsule_files=loaded_files, inference_mode=inference_mode) try: _validate_capsule(new_capsule) except InvalidCapsuleError as e: new_capsule.close() raise e return new_capsule