{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Generació de classes Python des d'ontologies RDF i esquemes YAML\n", "\n", "Aquest tutorial mostra com generar fitxers Python amb classes entity a partir\n", "de dos tipus de font:\n", "\n", "1. **Ontologies RDF/OWL** (RiC-O) — descarrega, converteix i genera classes\n", "2. **Esquemes YAML** (datasets existents) — genera classes des del schema del backend\n", "\n", "## Pipeline general\n", "\n", "```text\n", "RDF/OWL ──→ rdf_to_yaml() ──→ YAML DRM ──→ generate_classes() ──→ .py\n", "YAML ──────────────────────────────────────→ generate_classes() ──→ .py\n", "```" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 1. Generació des d'una ontologia RDF/OWL\n", "\n", "### Pipeline complet en un pas\n", "\n", "La funció `download_ontology_and_convert()` fa tot el procés automàticament:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from drm.rdf_schema import download_ontology_and_convert\n", "\n", "# Descarrega, converteix i genera classes en un sol pas\n", "output_path = download_ontology_and_convert(\n", " \"https://raw.githubusercontent.com/ICA-EGAD/RiC-O/master/ontology/current-version/RiC-O_1-1.rdf\",\n", " \"rico\", # nom → genera rico_entities.py\n", " output_dir=\"drm/\"\n", ")\n", "print(f\"Classes generades a: {output_path}\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Pas a pas\n", "\n", "Si vols més control, pots fer cada pas per separat:\n", "\n", "#### 1. Descarregar l'ontologia" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "import os\nfrom drm.rdf_schema import download_ontology\n\nont_path = download_ontology(\n \"https://raw.githubusercontent.com/ICA-EGAD/RiC-O/master/ontology/current-version/RiC-O_1-1.rdf\",\n output_dir=\"ontologies/\",\n filename=\"RiC-O_1-1.rdf\"\n)\nprint(f\"Ontologia descarregada: {ont_path}\")\nprint(f\"Mida: {os.path.getsize(ont_path) / 1024 / 1024:.1f} MB\")" }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### 2. Convertir RDF → YAML DRM" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from drm.rdf_schema import rdf_to_yaml\n", "\n", "yaml_str = rdf_to_yaml(ont_path, \"rico\")\n", "print(\"Primeres línies del YAML generat:\")\n", "print(yaml_str[:600])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### 3. YAML → Classes Python" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from drm.schema_gen import generate_classes\n", "\n", "py_source = generate_classes(yaml_str)\n", "print(f\"Total línies generades: {len(py_source.splitlines())}\")\n", "print()\n", "print(\"Primera classe generada (Thing):\")\n", "for line in py_source.splitlines()[:15]:\n", " print(line)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### 4. Escriure el fitxer" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "output_file = \"drm/entities_rico.py\"\n", "with open(output_file, \"w\") as f:\n", " f.write(py_source)\n", "print(f\"Fitxer escrit: {output_file}\")\n", "print(f\"Mida: {os.path.getsize(output_file) / 1024:.0f} KB\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Mapeig RDF/OWL → DRM\n", "\n", "L'automata mapeja els constructs OWL de la següent manera:\n", "\n", "| OWL construct | DRM mapping | Exemple RiC-O |\n", "|---------------|-------------|---------------|\n", "| `owl:Class` | Nom de la classe | `Agent`, `Document` |\n", "| `rdfs:subClassOf` | `WeakNode` (herència) | `Agent` hereta de `Thing` |\n", "| `owl:DatatypeProperty` | Propietats del node | `Agent.authorizingMandate` |\n", "| `owl:ObjectProperty` | Relacions entre nodes | `accumulationRelation_role` |\n", "| `owl:hasKey` | Camps de la PK | *(RiC-O no en té)* |\n", "| `rdfs:comment` | Docstring de la classe | Descripcions de RiC-O |" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Anàlisi de l'ontologia RiC-O\n", "\n", "Resum de les classes generades a partir de RiC-O:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import yaml\n", "\n", "data = yaml.safe_load(yaml_str)\n", "\n", "node_count = sum(1 for v in data[\"labels\"].values() if v[\"base_class\"] == \"Node\")\n", "weaknode_count = sum(1 for v in data[\"labels\"].values() if v[\"base_class\"] == \"WeakNode\")\n", "rel_count = len(data.get(\"relationships\", {}))\n", "weakrel_count = len(data.get(\"weak_relations\", {}))\n", "\n", "print(f\"Node classes: {node_count}\")\n", "print(f\"WeakNode classes: {weaknode_count}\")\n", "print(f\"Relacions: {rel_count}\")\n", "print(f\"WeakRelacions: {weakrel_count}\")\n", "print(f\"Total classes: {node_count + weaknode_count + rel_count + weakrel_count}\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Jerarquia més profunda de RiC-O\n", "\n", "RiC-O té una jerarquia de profunditat 5:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Construir el mapa de pares\n", "parent_map = {}\n", "for label, info in data[\"labels\"].items():\n", " if info[\"base_class\"] == \"WeakNode\" and \"parent\" in info:\n", " parent_map[label] = info[\"parent\"]\n", "\n", "# Calcular profunditats\n", "def get_depth(label, memo={}):\n", " if label in memo:\n", " return memo[label]\n", " if label not in parent_map:\n", " memo[label] = 0\n", " return 0\n", " depth = 1 + get_depth(parent_map[label], memo)\n", " memo[label] = depth\n", " return depth\n", "\n", "depths = {l: get_depth(l) for l in parent_map}\n", "max_depth = max(depths.values())\n", "\n", "# Trobar la classe més profunda\n", "deepest = [l for l, d in depths.items() if d == max_depth]\n", "label = sorted(deepest)[0]\n", "\n", "# Traçar la cadena\n", "path = []\n", "current = label\n", "while current in parent_map:\n", " path.append(current)\n", " current = parent_map[current]\n", "path.append(current)\n", "path.reverse()\n", "\n", "print(f\"Classe més profunda: {label} (profunditat {max_depth})\")\n", "print(\"Jerarquia completa:\")\n", "for i, cls in enumerate(path):\n", " info = data[\"labels\"][cls]\n", " base = info[\"base_class\"]\n", " print(f\" {i}. {cls} ({base})\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Importar les classes generades" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from drm.rico_entities import Thing, Agent, Document, AuthorshipRelation\n", "\n", "# Thing és un Node (arrel de la jerarquia)\n", "print(f\"Thing: {Thing.__name__} → base: {Thing.__bases__[0].__name__}\")\n", "\n", "# Agent és un WeakNode (fill de Thing)\n", "print(f\"Agent: {Agent.__name__} → base: {Agent.__bases__[0].__name__}\")\n", "\n", "# AuthorshipRelation és un WeakNode (profunditat 5)\n", "print(f\"AuthorshipRelation: {AuthorshipRelation.__name__} → base: {AuthorshipRelation.__bases__[0].__name__}\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 2. Generació des d'un esquema YAML (datasets existents)\n", "\n", "Els datasets existents tenen un schema YAML que es pot generar amb\n", "`GraphStore.schema_yaml()`. Aquest schema es pot utilitzar per generar\n", "classes Python.\n", "\n", "### Exemple: Karate Club\n", "\n", "Primer carreguem el dataset i generem el schema YAML:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from drm import NetworkXGraph\n", "from drm.exemples import load_karate_club\n", "\n", "# Carregar el dataset\n", "g = NetworkXGraph()\n", "stats = load_karate_club(g)\n", "print(\"Estadístiques:\", stats)\n", "\n", "# Generar el schema YAML\n", "yaml_str = g.schema_yaml(\"karate\")\n", "print()\n", "print(\"Schema YAML generat:\")\n", "print(yaml_str)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Analitzem el schema:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import yaml\n", "\n", "data = yaml.safe_load(yaml_str)\n", "print(\"Labels:\")\n", "for label, info in data[\"labels\"].items():\n", " print(f\" {label}:\")\n", " print(f\" base_class: {info['base_class']}\")\n", " print(f\" primary_key: {info.get('primary_key', [])}\")\n", " print(f\" properties: {info.get('properties', {})}\")\n", " if 'parent' in info:\n", " print(f\" parent: {info['parent']}\")\n", "print()\n", "print(\"Relacions:\", list(data.get(\"relationships\", {}).keys()))\n", "print(\"WeakRelacions:\", list(data.get(\"weak_relations\", {}).keys()))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Generem les classes Python:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from drm.schema_gen import generate_classes\n", "\n", "py_source = generate_classes(yaml_str)\n", "print(py_source)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Exemple: Bibliografia (OpenAlex)\n", "\n", "El dataset bibliografia té classes `Paper` i `Author` amb relacions\n", "`AUTHORED` i `CITES`:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from drm import NetworkXGraph\n", "from drm.exemples import load_bibliografia_openalex\n", "\n", "g = NetworkXGraph()\n", "stats = load_bibliografia_openalex(g, query=\"graph database\", per_page=5)\n", "print(\"Estadístiques:\", stats)\n", "\n", "yaml_str = g.schema_yaml(\"bibliografia\")\n", "data = yaml.safe_load(yaml_str)\n", "\n", "print(\"\\nLabels:\")\n", "for label, info in data[\"labels\"].items():\n", " print(f\" {label}: base={info['base_class']}, pk={info.get('primary_key', [])}\")\n", "print(\"\\nRelacions:\", list(data.get(\"relationships\", {}).keys()))\n", "\n", "py_source = generate_classes(yaml_str)\n", "print(\"\\nClasses generades:\")\n", "for line in py_source.splitlines():\n", " if line.startswith(\"class \") or \"def __init__\" in line or \"super().__init__\" in line:\n", " print(line)\n", "\n", "g.close()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 3. Generació des d'un YAML manual\n", "\n", "També pots crear un YAML manual i generar les classes:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from drm.schema_gen import generate_classes\n", "\n", "yaml_str = \"\"\"\n", "labels:\n", " Document:\n", " class_name: Document\n", " base_class: Node\n", " properties:\n", " title: string\n", " year: integer\n", " primary_key: [\"id\"]\n", " doc: \"A cultural document.\"\n", " Page:\n", " class_name: Page\n", " base_class: WeakNode\n", " properties: {}\n", " primary_key: []\n", " parent: Document\n", " parent_relation: HAS_PAGE\n", " doc: \"A page within a document.\"\n", "relationships:\n", " references:\n", " class_name: References\n", " src: Document\n", " dst: Document\n", " properties: {}\n", "weak_relations:\n", " HAS_PAGE:\n", " class_name: HasPage\n", " base_class: WeakRelation\n", " propagate: true\n", " src: Document\n", " dst: Page\n", "\"\"\"\n", "\n", "py_source = generate_classes(yaml_str)\n", "print(py_source)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 4. PK optional vs mandatory\n", "\n", "El generador detecta automàticament si una classe té `primary_key` definit:\n", "\n", "| YAML `primary_key` | Codi generat |\n", "|--------------------|-------------|\n", "| `[]` (buit) | `pk: Optional[Dict[str, Any]] = None` |\n", "| `[\"id\"]` | `pk: Dict[str, Any]` (mandatory) |\n", "\n", "Això és important per a ontologies RDF que no fan servir `owl:hasKey`:\n", "el backend assigna un ID intern (`neo4j_id`) que es converteix en la PK." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# RiC-O no té owl:hasKey → totes les classes tenen primary_key: []\n", "no_pk = [l for l, i in data[\"labels\"].items() if not i.get(\"primary_key\")]\n", "with_pk = [l for l, i in data[\"labels\"].items() if i.get(\"primary_key\")]\n", "\n", "print(f\"Classes sense PK definida: {len(no_pk)}\")\n", "print(f\"Classes amb PK definida: {len(with_pk)}\")\n", "print()\n", "print(\"Exemples de classes sense PK (RiC-O):\")\n", "for cls in sorted(no_pk)[:5]:\n", " info = data[\"labels\"][cls]\n", " parent = info.get('parent', 'arrel')\n", " print(f\" - {cls} (parent: {parent})\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Resum\n", "\n", "| Font | Funció | Sortida |\n", "|------|--------|---------|\n", "| RDF/OWL URL | `download_ontology_and_convert()` | `drm/{db_name}_entities.py` |\n", "| RDF/OWL fitxer | `rdf_to_yaml()` + `generate_classes()` | Codi Python (string) |\n", "| Schema YAML | `generate_classes()` | Codi Python (string) |\n", "| YAML manual | `generate_classes()` | Codi Python (string) |" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "name": "python", "version": "3.10.0" } }, "nbformat": 4, "nbformat_minor": 4 }