Skip to content

Parsers

biopro_sdk.plugin.manifest_parser

ManifestParser

Parses and validates plugin pyproject.toml files.

Source code in src/biopro_sdk/plugin/manifest_parser.py
class ManifestParser:
    """Parses and validates plugin pyproject.toml files."""

    REQUIRED_KEYS = ["id", "name", "version", "description", "authors"]

    def parse(self, manifest_data: dict[str, Any]) -> dict[str, Any]:
        """Validate flat dictionary combined from pyproject.toml."""
        for key in self.REQUIRED_KEYS:
            if key not in manifest_data:
                raise ManifestValidationError(f"Missing required field: '{key}'")

        authors = manifest_data["authors"]
        if not isinstance(authors, list) or len(authors) == 0:
            raise ManifestValidationError("'authors' must be a non-empty array.")

        for idx, author in enumerate(authors):
            if not isinstance(author, dict):
                raise ManifestValidationError(f"Author at index {idx} must be an object.")
            if "name" not in author:
                raise ManifestValidationError(f"Author at index {idx} must contain 'name'.")
            if "role" not in author:
                raise ManifestValidationError("Author profile must contain 'role'.")
            if "permissions" in author:
                perms = author["permissions"]
                if not isinstance(perms, list) or not all(isinstance(p, str) for p in perms):
                    raise ManifestValidationError("Author 'permissions' must be a list of strings.")

        return manifest_data

    def parse_file(self, filepath: str | Path) -> dict[str, Any]:
        """Read and parse a pyproject.toml file."""
        filepath = Path(filepath)

        # Security/Sanity Check
        legacy_manifest = filepath.parent / "manifest.json"
        legacy_toml = filepath.parent / "plugin.toml"
        if legacy_manifest.exists() or legacy_toml.exists():
            print("\n⚠️  WARNING: Legacy manifest.json or plugin.toml found! These are deprecated.")
            print("⚠️  Please run 'biopro-sdk migrate' to transition to pyproject.toml.\n")

        try:
            with open(filepath, "rb") as f:
                data = tomllib.load(f)

            project = data.get("project", {})
            plugin = data.get("tool", {}).get("biopro", {}).get("plugin", {})

            # Merge fields to present a unified V2 style dictionary to the rest of the application
            flat_manifest = {
                "name": project.get("name"),
                "version": project.get("version"),
                "description": project.get("description"),
            }
            flat_manifest.update(plugin)

            # Ensure authors is set if not provided in tool.biopro.plugin
            if "authors" not in flat_manifest:
                flat_manifest["authors"] = project.get("authors", [])

            return self.parse(flat_manifest)
        except tomllib.TOMLDecodeError as e:
            raise ManifestValidationError(f"Invalid TOML format: {e}") from e
        except FileNotFoundError as e:
            raise ManifestValidationError(f"Plugin configuration not found: {filepath}") from e

parse(manifest_data)

Validate flat dictionary combined from pyproject.toml.

Source code in src/biopro_sdk/plugin/manifest_parser.py
def parse(self, manifest_data: dict[str, Any]) -> dict[str, Any]:
    """Validate flat dictionary combined from pyproject.toml."""
    for key in self.REQUIRED_KEYS:
        if key not in manifest_data:
            raise ManifestValidationError(f"Missing required field: '{key}'")

    authors = manifest_data["authors"]
    if not isinstance(authors, list) or len(authors) == 0:
        raise ManifestValidationError("'authors' must be a non-empty array.")

    for idx, author in enumerate(authors):
        if not isinstance(author, dict):
            raise ManifestValidationError(f"Author at index {idx} must be an object.")
        if "name" not in author:
            raise ManifestValidationError(f"Author at index {idx} must contain 'name'.")
        if "role" not in author:
            raise ManifestValidationError("Author profile must contain 'role'.")
        if "permissions" in author:
            perms = author["permissions"]
            if not isinstance(perms, list) or not all(isinstance(p, str) for p in perms):
                raise ManifestValidationError("Author 'permissions' must be a list of strings.")

    return manifest_data

parse_file(filepath)

Read and parse a pyproject.toml file.

Source code in src/biopro_sdk/plugin/manifest_parser.py
def parse_file(self, filepath: str | Path) -> dict[str, Any]:
    """Read and parse a pyproject.toml file."""
    filepath = Path(filepath)

    # Security/Sanity Check
    legacy_manifest = filepath.parent / "manifest.json"
    legacy_toml = filepath.parent / "plugin.toml"
    if legacy_manifest.exists() or legacy_toml.exists():
        print("\n⚠️  WARNING: Legacy manifest.json or plugin.toml found! These are deprecated.")
        print("⚠️  Please run 'biopro-sdk migrate' to transition to pyproject.toml.\n")

    try:
        with open(filepath, "rb") as f:
            data = tomllib.load(f)

        project = data.get("project", {})
        plugin = data.get("tool", {}).get("biopro", {}).get("plugin", {})

        # Merge fields to present a unified V2 style dictionary to the rest of the application
        flat_manifest = {
            "name": project.get("name"),
            "version": project.get("version"),
            "description": project.get("description"),
        }
        flat_manifest.update(plugin)

        # Ensure authors is set if not provided in tool.biopro.plugin
        if "authors" not in flat_manifest:
            flat_manifest["authors"] = project.get("authors", [])

        return self.parse(flat_manifest)
    except tomllib.TOMLDecodeError as e:
        raise ManifestValidationError(f"Invalid TOML format: {e}") from e
    except FileNotFoundError as e:
        raise ManifestValidationError(f"Plugin configuration not found: {filepath}") from e

ManifestValidationError

Bases: Exception

Raised when a manifest fails validation.

Source code in src/biopro_sdk/plugin/manifest_parser.py
6
7
8
9
class ManifestValidationError(Exception):
    """Raised when a manifest fails validation."""

    pass

biopro_sdk.plugin.security_parser

ManifestHashMismatch

Bases: SecurityValidationError

Raised when the cryptographic binding hash between pyproject.toml and security.json fails.

Source code in src/biopro_sdk/plugin/security_parser.py
class ManifestHashMismatch(SecurityValidationError):
    """Raised when the cryptographic binding hash between pyproject.toml and security.json fails."""

    pass

SecurityParser

Parses and validates plugin security.json files (V1 Security Ledger Schema).

Source code in src/biopro_sdk/plugin/security_parser.py
class SecurityParser:
    """Parses and validates plugin security.json files (V1 Security Ledger Schema)."""

    REQUIRED_FIELDS = ["security_version", "plugin_id", "manifest_hash", "hashes"]

    def parse(self, security_data: dict[str, Any]) -> dict[str, Any]:
        """Parse and validate security dictionary."""
        # 1. Check required fields
        for key in self.REQUIRED_FIELDS:
            if key not in security_data:
                raise SecurityValidationError(f"Missing required security field: '{key}'")

        # 2. Enforce V1 security version (No backwards compatibility)
        if security_data.get("security_version") != 1:
            raise SecurityValidationError("Only security_version: 1 is supported.")

        # 3. Validate hashes is a dictionary
        hashes = security_data["hashes"]
        if not isinstance(hashes, dict):
            raise SecurityValidationError("'hashes' must be a dictionary.")

        # 4. Validate exclusions is a list if present
        if "exclusions" in security_data:
            exclusions = security_data["exclusions"]
            if not isinstance(exclusions, list):
                raise SecurityValidationError("'exclusions' must be a list of strings.")

        return security_data

    def parse_file(self, filepath: str) -> dict[str, Any]:
        """Read and parse a security.json file."""
        try:
            with open(filepath, encoding="utf-8") as f:
                data = json.load(f)
            return self.parse(data)
        except json.JSONDecodeError as e:
            raise SecurityValidationError(f"Invalid JSON format: {e}") from e
        except FileNotFoundError as e:
            raise SecurityValidationError(f"Security file not found: {filepath}") from e

    def verify_manifest_binding(self, manifest_filepath: Path, security_data: dict[str, Any]) -> None:
        """Verify the cryptographic hash binding of pyproject.toml against security.json."""
        if not manifest_filepath.exists():
            raise SecurityValidationError(f"Manifest file not found: {manifest_filepath}")

        # Calculate SHA-256 of pyproject.toml exactly as written on disk
        manifest_bytes = manifest_filepath.read_bytes()
        computed_hash = hashlib.sha256(manifest_bytes).hexdigest()

        expected_hash = security_data["manifest_hash"]
        if computed_hash != expected_hash:
            raise ManifestHashMismatch(
                "Cryptographic bind mismatch: pyproject.toml SHA-256 does not match security.json manifest_hash. "
                f"Expected: {expected_hash}, Computed: {computed_hash}"
            )

parse(security_data)

Parse and validate security dictionary.

Source code in src/biopro_sdk/plugin/security_parser.py
def parse(self, security_data: dict[str, Any]) -> dict[str, Any]:
    """Parse and validate security dictionary."""
    # 1. Check required fields
    for key in self.REQUIRED_FIELDS:
        if key not in security_data:
            raise SecurityValidationError(f"Missing required security field: '{key}'")

    # 2. Enforce V1 security version (No backwards compatibility)
    if security_data.get("security_version") != 1:
        raise SecurityValidationError("Only security_version: 1 is supported.")

    # 3. Validate hashes is a dictionary
    hashes = security_data["hashes"]
    if not isinstance(hashes, dict):
        raise SecurityValidationError("'hashes' must be a dictionary.")

    # 4. Validate exclusions is a list if present
    if "exclusions" in security_data:
        exclusions = security_data["exclusions"]
        if not isinstance(exclusions, list):
            raise SecurityValidationError("'exclusions' must be a list of strings.")

    return security_data

parse_file(filepath)

Read and parse a security.json file.

Source code in src/biopro_sdk/plugin/security_parser.py
def parse_file(self, filepath: str) -> dict[str, Any]:
    """Read and parse a security.json file."""
    try:
        with open(filepath, encoding="utf-8") as f:
            data = json.load(f)
        return self.parse(data)
    except json.JSONDecodeError as e:
        raise SecurityValidationError(f"Invalid JSON format: {e}") from e
    except FileNotFoundError as e:
        raise SecurityValidationError(f"Security file not found: {filepath}") from e

verify_manifest_binding(manifest_filepath, security_data)

Verify the cryptographic hash binding of pyproject.toml against security.json.

Source code in src/biopro_sdk/plugin/security_parser.py
def verify_manifest_binding(self, manifest_filepath: Path, security_data: dict[str, Any]) -> None:
    """Verify the cryptographic hash binding of pyproject.toml against security.json."""
    if not manifest_filepath.exists():
        raise SecurityValidationError(f"Manifest file not found: {manifest_filepath}")

    # Calculate SHA-256 of pyproject.toml exactly as written on disk
    manifest_bytes = manifest_filepath.read_bytes()
    computed_hash = hashlib.sha256(manifest_bytes).hexdigest()

    expected_hash = security_data["manifest_hash"]
    if computed_hash != expected_hash:
        raise ManifestHashMismatch(
            "Cryptographic bind mismatch: pyproject.toml SHA-256 does not match security.json manifest_hash. "
            f"Expected: {expected_hash}, Computed: {computed_hash}"
        )

SecurityValidationError

Bases: Exception

Raised when a security ledger fails validation.

Source code in src/biopro_sdk/plugin/security_parser.py
class SecurityValidationError(Exception):
    """Raised when a security ledger fails validation."""

    pass