Source code for linkml.generators.panderagen.panderagen

import logging
import os
from dataclasses import dataclass
from enum import Enum
from pathlib import PurePosixPath
from types import ModuleType
from typing import Optional

import click
from jinja2 import Environment, PackageLoader
from linkml_runtime.linkml_model.meta import TypeDefinition
from linkml_runtime.utils.compile_python import compile_python
from linkml_runtime.utils.formatutils import camelcase
from linkml_runtime.utils.schemaview import SchemaView

from linkml._version import __version__
from linkml.generators.oocodegen import OOClass, OOCodeGenerator, OODocument

from .class_generator_mixin import ClassGeneratorMixin
from .enum_generator_mixin import EnumGeneratorMixin
from .slot_generator_mixin import SlotGeneratorMixin

logger = logging.getLogger(__name__)


# keys are template_path
TYPEMAP = {
    "panderagen_class_based": {
        "xsd:string": "str",
        "xsd:integer": "int",
        "xsd:float": "float",
        "xsd:double": "float",
        "xsd:boolean": "bool",
        "xsd:dateTime": "DateTime",
        "xsd:date": "Date",
        "xsd:time": "Time",
        "xsd:anyURI": "str",
        "xsd:decimal": "float",
    },
}


class TemplateEnum(Enum):
    CLASS_BASED = "panderagen_class_based"


[docs]@dataclass class PanderaGenerator(OOCodeGenerator, EnumGeneratorMixin, ClassGeneratorMixin, SlotGeneratorMixin): """ Generates Pandera python classes from a LinkML schema. Status: incompletely implemented One styles is supported: - panderagen_class_based """ DEFAULT_TEMPLATE_PATH = "panderagen_class_based" DEFAULT_TEMPLATE_FILE = "pandera.jinja2" # ClassVars generatorname = os.path.basename(__file__) generatorstem = PurePosixPath(generatorname).stem generatorversion = "0.0.1" valid_formats = ["python"] file_extension = "py" java_style = False # ObjectVars template_file: Optional[str] = None template_path: str = DEFAULT_TEMPLATE_PATH gen_classvars: bool = True gen_slots: bool = True genmeta: bool = False emit_metadata: bool = True coerce: bool = False
[docs] def default_value_for_type(self, typ: str) -> str: """Allow underlying framework to handle default if not specified.""" return None
@staticmethod def make_multivalued(range: str) -> str: return f"List[{range}]" def uri_type_map(self, xsd_uri: str, template: str = None): if template is None: template = self.template_path return TYPEMAP[template].get(xsd_uri) def map_type(self, t: TypeDefinition) -> str: if t.uri: typ = self.uri_type_map(t.uri) return typ elif t.typeof: typ = self.map_type(self.schemaview.get_type(t.typeof)) return typ else: raise ValueError(f"{t} cannot be mapped to a type") def load_template(self, template_filename): jinja_env = Environment(loader=PackageLoader("linkml.generators.panderagen", self.template_path)) return jinja_env.get_template(template_filename)
[docs] def compile_pandera(self) -> ModuleType: """ Generates and compiles Pandera model """ pandera_code = self.serialize() return compile_python(pandera_code)
[docs] def serialize(self, rendered_module: Optional[OODocument] = None) -> str: """ Serialize the schema to a Pandera module as a string """ if self.template_path is None: self.template_path = PanderaGenerator.DEFAULT_TEMPLATE_PATH if rendered_module is not None: module = rendered_module else: module = self.render() if self.template_file is None: self.template_file = PanderaGenerator.DEFAULT_TEMPLATE_FILE template_file = self.template_file template_obj = self.load_template(template_file) code = template_obj.render( doc=module, metamodel_version=self.schema.metamodel_version, model_version=self.schema.version, coerce=self.coerce, type_map=TYPEMAP, template_path=self.template_path, ) return code
[docs] def render(self) -> OODocument: """ Create a data structure ready to pass to the serialization templates. """ sv: SchemaView = self.schemaview module_name = camelcase(sv.schema.name) oodoc = OODocument(name=module_name, package=self.package, source_schema=sv.schema) classes = [] for c in self.ordered_classes(): cn = c.name safe_cn = camelcase(cn) ooclass = OOClass( name=safe_cn, description=c.description, package=self.package, fields=[], source_class=c, ) classes.append(ooclass) if c.mixin: ooclass.mixin = c.mixin if c.mixins: ooclass.mixins = [(x) for x in c.mixins] if c.is_a: ooclass.is_a = self.get_class_name(c.is_a) parent_slots = sv.class_slots(c.is_a) else: parent_slots = [] for sn in sv.class_slots(cn): oofield = self.handle_slot(cn, sn) if sn not in parent_slots: ooclass.fields.append(oofield) ooclass.all_fields.append(oofield) oodoc.classes = classes return oodoc
@click.option("--package", help="Package name where relevant for generated class files") @click.option("--template-path", help="Optional jinja2 template directory within module") @click.option("--template-file", help="Optional jinja2 template to use for class generation") @click.version_option(__version__, "-V", "--version") @click.argument("yamlfile") @click.command(name="gen-pandera") def cli( yamlfile, package=None, template_path=None, template_file=None, **args, ): if template_path is not None and template_path not in TYPEMAP: raise Exception(f"Template {template_path} not supported") """Generate Pandera classes to represent a LinkML model""" gen = PanderaGenerator( yamlfile, package=package, template_path=template_path, template_file=template_file, **args, ) print(gen.serialize()) if __name__ == "__main__": cli()