import importlib from dataclasses import field from pathlib import Path from typing import List, Union from pydantic import ValidationError from pydantic.dataclasses import dataclass from typing_extensions import Self from ..report.schemas.v3_0 import main as v3_0 from .base import ReportSchemaVersion, SafetyBaseModel, ScanType from .file import FileModel from .metadata import MetadataModel from .project import ProjectModel from .telemetry import TelemetryModel from pydantic.version import VERSION as PYDANTIC_VERSION def version_validator(func): if PYDANTIC_VERSION.startswith("1."): from pydantic import validator return validator('verison', pre=True, always=True) else: from pydantic import field_validator return field_validator('verison', mode='before') @dataclass class ReportModel(SafetyBaseModel): """ Used as an entrypoint to keep backwards compatibility with old formats. Use this model if you want to generate a standard JSON report. """ DEFAULT_SCHEMA_VERSION = ReportSchemaVersion.v3_0 telemetry: TelemetryModel metadata: MetadataModel files: List[FileModel] projects: List[ProjectModel] = field(default_factory=lambda: []) version: ReportSchemaVersion = DEFAULT_SCHEMA_VERSION @version_validator def validate_version(cls, version: ReportSchemaVersion) -> ReportSchemaVersion: versions = list(ReportSchemaVersion) if version not in (versions): raise ValueError(f"Invalid version, allowed versions are {versions}") return version def as_v30(self) -> v3_0.Report: full = self.metadata.scan_type is ScanType.scan projects_data = [p.as_v30(full=full) for p in self.projects] if not all(isinstance(p, type(projects_data[0])) for p in projects_data): raise ValueError("All objects in projects_data must be of the same type") results = v3_0.ScanResults( files=[f.as_v30() for f in self.files], projects=projects_data, # type: ignore ) meta_data = self.metadata.as_v30() if not isinstance(meta_data, v3_0.Meta) or not isinstance(results, v3_0.ScanResults): raise TypeError("Expected instance of v3_0.Meta and v3_0.ScanResults") report = v3_0.Report(meta=meta_data, scan_results=results) return report @classmethod def from_v30(cls, obj: v3_0.SchemaModelV30) -> 'ReportModel': if not isinstance(obj, v3_0.Report): raise TypeError('Expected instance of v3_0.Report') return ReportModel( version=ReportSchemaVersion(obj.meta.schema_version), telemetry=TelemetryModel.from_v30(obj.meta.telemetry), metadata=MetadataModel.from_v30(obj.meta), projects=[ProjectModel.from_v30(p) for p in obj.scan_results.projects], files=[FileModel.from_v30(f) for f in obj.scan_results.files], ) @classmethod def parse_report( cls, raw_report: Union[str, Path], schema: ReportSchemaVersion ) -> Union['ReportModel', ValidationError]: parse = "parse_raw" if isinstance(raw_report, Path): raw_report = raw_report.expanduser().resolve() parse = "parse_file" target_schema = schema.value.replace(".", "_") module_name = "safety_schemas.report.schemas." f"v{target_schema}.main" module = importlib.import_module(module_name) report_model = module.Report # This will raise a validation error if the content is wrong validated_report = getattr(report_model, parse)(raw_report) # TODO: Select the from from the version passed return ReportModel.from_v30(obj=validated_report)