Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions docs/reference/export.md
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,11 @@ export_lpg(graph, "import.cypher", method="cypher")
exporter = SemanticNetworkYAMLExporter()
exporter.export(graph, "graph.yaml")
```

The YAML exporters read `entities`/`relationships`/`triplets` (with
`nodes`/`edges` accepted as aliases, so `ContextGraph.to_dict()` exports
directly). A non-empty mapping supplying none of them raises
`ValidationError` rather than writing a file with every collection empty.
</Tab>
<Tab title="Graph DB Import">
**LPGExporter** writes Cypher `CREATE` statements for Neo4j and Memgraph:
Expand Down
18 changes: 6 additions & 12 deletions semantica/export/arango_aql_exporter.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
from pathlib import Path
from typing import Any, Dict, List, Optional, Union

from ..utils.helpers import ensure_directory
from ..utils.helpers import ensure_directory, normalize_graph_payload
from ..utils.logging import get_logger
from ..utils.progress_tracker import get_progress_tracker

Expand Down Expand Up @@ -204,17 +204,11 @@ def _generate_aql_statements(
self._generate_collection_creation(vertex_collection, edge_collection)
)

# Extract entities and relationships
entities = knowledge_graph.get("entities", [])
relationships = knowledge_graph.get("relationships", [])
nodes = knowledge_graph.get("nodes", entities)
edges = knowledge_graph.get("edges", relationships)

# Use nodes/edges if entities/relationships are empty
if not entities and nodes:
entities = nodes
if not relationships and edges:
relationships = edges
# Accept either vocabulary; resolution is centralized so every
# exporter agrees on what a given payload means.
normalized = normalize_graph_payload(knowledge_graph)
entities = normalized["entities"]
relationships = normalized["relationships"]

# Generate vertex INSERT statements
vertex_statements = self._generate_vertex_inserts(entities, vertex_collection)
Expand Down
39 changes: 39 additions & 0 deletions semantica/export/export_usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,45 @@ export_yaml(semantic_network, "network.yaml", method="semantic_network")
export_yaml(schema, "schema.yaml", method="schema")
```

### Accepted Input

Both YAML exporters read their payload by key, so the input must be a mapping;
anything else raises `ProcessingError`. A bare list is rejected rather than
wrapped, since these formats distinguish entities from relationships from
triplets and guessing which one a list holds would mislabel the records.

Each exporter then reads a fixed set of keys, and raises `ValidationError` on a
non-empty mapping that supplies none of them — such a payload would otherwise
serialize to a valid file with every collection empty. Naming a recognized key
is not enough on its own: `{"entities": [], "data": [...]}` also raises, since
nothing resolves while the records sit under a key the exporter never reads.

| Method | Recognized keys |
| :--- | :--- |
| `"semantic_network"` | `entities` (alias `nodes`), `relationships` (alias `edges`), `triplets` |
| `"schema"` | `classes`, `properties`, `namespaces`, `uri`, `title`, `description`, `version` |

`metadata` is carried through on both, but does not by itself make a payload
recognized — an `export_json` envelope (`{"data": [...], "count": N,
"metadata": {...}}`) carries one and is rejected.

```python
# ContextGraph.to_dict() exports directly via the nodes/edges aliases
export_yaml(context_graph.to_dict(), "graph.yaml")

# A bare list has no unambiguous meaning here
export_yaml(records, "out.yaml") # ProcessingError

# An export_json payload is refused rather than written out empty
export_yaml({"data": records}, "out.yaml") # ValidationError

# ...and so is one that names a recognized key but leaves it empty
export_yaml({"entities": [], "data": records}, "out.yaml") # ValidationError
```

An empty mapping is still accepted: an empty graph is a legitimate export and
has no records to lose.

## OWL Export

### OWL/XML Format
Expand Down
32 changes: 20 additions & 12 deletions semantica/export/lpg_exporter.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
from typing import Any, Dict, List, Optional, Union

from ..utils.exceptions import ProcessingError, ValidationError
from ..utils.helpers import ensure_directory
from ..utils.helpers import ensure_directory, normalize_graph_payload
from ..utils.logging import get_logger
from ..utils.progress_tracker import get_progress_tracker

Expand Down Expand Up @@ -154,15 +154,18 @@ def _generate_cypher_queries(
"""
queries = []

# Generate indexes if requested
if self.include_indexes:
queries.extend(self._generate_indexes(knowledge_graph))
# Accept either vocabulary. Reading 'nodes' with 'entities' as the
# default dropped every entity when 'nodes' was present but empty --
# the shape JSONExporter emits -- so resolution is centralized.
normalized = normalize_graph_payload(knowledge_graph)
nodes = normalized["entities"]
edges = normalized["relationships"]

# Extract entities and relationships
entities = knowledge_graph.get("entities", [])
relationships = knowledge_graph.get("relationships", [])
nodes = knowledge_graph.get("nodes", entities)
edges = knowledge_graph.get("edges", relationships)
# Generate indexes if requested. Fed the normalized entities so index
# generation sees the same records as node generation; reading
# 'entities' directly here skipped indexes for nodes/edges payloads.
if self.include_indexes:
queries.extend(self._generate_indexes(nodes))

# Generate node creation queries
node_queries = self._generate_node_queries(nodes)
Expand All @@ -174,13 +177,18 @@ def _generate_cypher_queries(

return queries

def _generate_indexes(self, knowledge_graph: Dict[str, Any]) -> List[str]:
"""Generate Cypher index and constraint creation queries."""
def _generate_indexes(self, entities: List[Dict[str, Any]]) -> List[str]:
"""Generate Cypher index and constraint creation queries.

Args:
entities: Entity records, already resolved from whichever
vocabulary the caller supplied.
"""
indexes = []

# Get unique entity types for labels
entity_types = set()
for entity in knowledge_graph.get("entities", []):
for entity in entities:
entity_type = entity.get("type") or entity.get("entity_type")
if entity_type:
entity_types.add(entity_type)
Expand Down
22 changes: 20 additions & 2 deletions semantica/export/methods.py
Original file line number Diff line number Diff line change
Expand Up @@ -494,7 +494,7 @@ def export_graph(


def export_yaml(
data: Union[Dict[str, Any], List[Dict[str, Any]]],
data: Dict[str, Any],
Comment thread
qodo-free-for-open-source-projects[bot] marked this conversation as resolved.
file_path: Union[str, Path],
method: str = "semantic_network",
**kwargs,
Expand All @@ -504,14 +504,32 @@ def export_yaml(

This is a user-friendly wrapper that exports data to YAML format.

Unlike :func:`export_json` and :func:`export_csv`, which treat a list as
opaque records, both YAML methods are keyed formats: they distinguish
entities from relationships from triplets (and classes from properties
for ``method="schema"``). A bare list is therefore rejected rather than
guessed at, since inferring which collection it represents would silently
mislabel the records.

Args:
data: Data to export (semantic network, entities, relationships)
data: Data to export, as a mapping. For ``method="semantic_network"``,
keyed by 'entities'/'relationships'/'triplets'; for
``method="schema"``, by 'classes'/'properties'.
file_path: Output YAML file path
method: Export method (default: "semantic_network")
- "semantic_network": Semantic network YAML export
- "schema": Schema YAML export
**kwargs: Additional options passed to YAML exporters

Raises:
ProcessingError: if ``data`` is not a mapping, or if ``method`` is not
a known YAML export method.
ValidationError: if ``data`` is a mapping whose keys the selected
exporter does not read -- an ``export_json`` envelope
(``{"data": [...], "count": N, "metadata": {...}}``) is the
common case. Such a payload used to be written out as a valid
YAML file with every collection empty.

Examples:
>>> from semantica.export.methods import export_yaml
>>> export_yaml(semantic_network, "network.yaml", method="semantic_network")
Expand Down
10 changes: 7 additions & 3 deletions semantica/export/neo4j_csv_exporter.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
from typing import Any, Dict, Iterable, List, Optional, Sequence, Union

from ..utils.exceptions import ProcessingError, ValidationError
from ..utils.helpers import ensure_directory
from ..utils.helpers import ensure_directory, normalize_graph_payload
from ..utils.logging import get_logger
from ..utils.progress_tracker import get_progress_tracker

Expand Down Expand Up @@ -495,8 +495,12 @@ def _prepare_export(self, graph: Any, strict: bool) -> Dict[str, Any]:

def _normalize_graph(self, graph: Any) -> Dict[str, List[Dict[str, Any]]]:
if isinstance(graph, dict):
nodes = graph.get("nodes") or graph.get("entities") or []
relationships = graph.get("edges") or graph.get("relationships") or []
# Mapping payloads go through the shared resolver so this backend
# cannot drift from the others; the attribute path below stays
# local, since objects are not mappings.
resolved = normalize_graph_payload(graph, require_recognized=False)
nodes = resolved["entities"]
relationships = resolved["relationships"]
else:
nodes = getattr(graph, "nodes", None)
if nodes is None:
Expand Down
Loading
Loading