From 3ba3d0ebbb4afda4cdac55903a2f4988203dba51 Mon Sep 17 00:00:00 2001
From: Anthony Mahanna <43019056+aMahanna@users.noreply.github.com>
Date: Mon, 4 Dec 2023 17:08:16 -0500
Subject: [PATCH] ArangoRDF Overhaul: 0.1.0 (#15)
* new: test suite & test data
* update: repo config
* new: arango_rdf overhaul checkpoint
* temp: base ontology files
location TBD
* new: `flake8` & `mypy` workflows
* fix: black, flake, mypy
* cleanup
* temp: disable black worflow
* fix: add flake & mypy dependency
* fix: add `rich` dependency
* temp: disable `mypy` workflow
getting inconsistent `mypy` results between local environment & Github Actions environment
* enable: black, mypy
* cleanup: `arango_rdf`
formatting fixes, mypy fixes, docstring updates, general code cleanup
* black: test_main
* update: setup files
* update: test_pgt_case_3_2
addresses all **list_conversion** parameter cases
* update: tests
* misc: pragma no cover
* fix: test assertions
* update: test_rpt_basic_cases
* cleanup: main
* new: `rich` Live Group progress bars, `batch_size` parameter, code cleanup
* update: `rich` trackers in utils
* new: `RDFLists` typing
* new: ignore E266 flake8
* misc: line breaks
* update: `process_rpt_term`, pragma no cover
* new: case 7 prototype
* update 6.trig
* cleanup utils
* cleanup
* variable renaming, cleanup
* cleanup: test data
* rework: test suite
* remove: examples/data
* remove: arango_rdf/ontologies
* new: arango_rdf/meta
* checkpoint: arango_rdf
* fix: isort
* fix: compare_graphs
* temp fix: mypy
* new: fraud detection & imdb tests
* checkpoint: main.py
* fix: isort
* fix: isort (again)
* new: meta files
switching to `trig` format
* checkpoint: tests
* checkpoint: arango_rdf
working on adb mapping functionality
* checkpoint: tests
* checkpoint: arango_rdf
* cleanup: tests
* checkpoint: arango_rdf
* update: test cases
* cleanup: arango_rdf
* fix: rpt case 5
* cleanup: tests
* new: cityhash dependency
* cleanup & docstrings: arango_rdf
flake8 will fail
* fix: flake8
autopep8 & yapf did not work, manual fix was required
* fix: pgt case 6
* new: __build_subclass_tree() and __identify_best_class()
* update: Tree.show()
* cleanup main
* new: dc.trig & xsd.trig starter files
only adding the nodes that are referenced by the other ontologies (OWL, RDF, RDFS) for now
* update: tests
* cleanup: arango_rdf
new `__pgt_add_to_adb_mapping` helper method, add restriction to property type relationship creation if contextualize_graph = True
* fix: pgt case 2_4
* more cleanup: arango_rdf
* new: load RDF Predicates regardless of contextualize_graph value (PGT only)
* update: test_adb_native_graph_to_rdf
* attempt fix: missing coverage on L922
coveralls seems to think this line is not covered by tests...
* Update README.md
* update docstrings
* Update README.md
* Update README.md
* Update README.md
* Update README.md
* fix: flake8
* Update README.md
* new: notebook overhaul baseline
* fix: process_val_as_string
* remove: unused func
* fix: p_already_has_dr
* new: __get_literal_val
* update: __get_literal_val
* fix: subgraph names
* cp: adb_key_uri
* cleanup: arango_rdf
* update: meta trig files
* cleanup: arango_rdf
* update: tests
* more cleanup
* fix: flake8
* new: ArangoRDFController
* fix: isort
* new: use_async (rdf to arangodb)
* cleanup
* update test params
* update: test case 7
* cleanup: insert_adb_docs
* update: tests
* cleanup
* new: ArangoRDF.ipynb output file
* revert: d2277fa7f66a04d148b23ce04d9ad92db598f97c
* new: game of thrones dump
* update: tests
* cp: arango_rdf
* update notebook
* new: cases 8-15 in notebook
* new: rdf-star support for rpt
* Revert "new: rdf-star support for rpt"
This reverts commit 2a0ae04c445ba21f254de7927375a771b43abd65.
* checkpoint
rdf-star support prototyping,
* cleanup: adb to rdf
* new: rdf_statement_blacklist
* discard "List" collection for pgt
* new: __get_adb_edge_key
* cleanup
* checkpoint
* cleanup
* new: rdf star cases (8 to 15)
* new: individualize RPT tests
* Update ArangoRDF.ipynb
* cleanup
* new: hash adb edge ids
* update: rdf-star support workaround
* new: test cases 8-15 (pgt)
* update notebook
* cleanup
* actions: use ArangoDB 3.11
* fix notebook
* cleanup
* Update setup.py
* new: design doc
template used: https://github.com/arangodb/documents/blob/master/DesignDocuments/DesignDocumentTemplate.md
* new: simplify_reified_triples flag
* new: keyify_literals (rpt)
minor cleanup
* rework: batch_size (adb to rdf)
* use batch_size in tests
(adb to rdf & rdf to adb)
* new: adb_key URI test case
* cleanup based on feedback
* fix: mypy
* update build workflow
* update release workflow
* cleanup, todo comments
* swap python 3.7 for 3.12
* cleanup tests (case 1 & 6)
* cleanup
* migrate to `pyproject.toml`
* fix lint
* fix mypy
* flake8 extend ignore
trying to workaround 3.12 builds: https://github.com/ArangoDB-Community/ArangoRDF/actions/runs/6856708733/job/18644393745?pr=15
---
.github/workflows/build.yml | 13 +-
.github/workflows/release.yml | 66 +-
README.md | 159 +-
arango_rdf/abc.py | 95 +
arango_rdf/controller.py | 106 +
arango_rdf/main.py | 3116 +++++++++++--
arango_rdf/meta/adb.trig | 12 +
arango_rdf/meta/dc.trig | 28 +
arango_rdf/meta/owl.trig | 552 +++
arango_rdf/meta/rdf.trig | 151 +
arango_rdf/meta/rdfs.trig | 111 +
arango_rdf/meta/xsd.trig | 12 +
arango_rdf/typings.py | 30 +
arango_rdf/utils.py | 88 +
examples/ArangoRDF.ipynb | 1889 ++++++--
examples/data/airport-ontology.owl | 684 ---
examples/data/sfo-aircraft-partial.ttl | 35 -
pyproject.toml | 84 +-
setup.cfg | 2 -
setup.py | 45 +-
tests/conftest.py | 155 +-
...81329febf6efe22788e03ddeaf0af.data.json.gz | Bin 0 -> 196 bytes
...329febf6efe22788e03ddeaf0af.structure.json | 1 +
...97786af4bf30dc5b07809a950792c.data.json.gz | Bin 0 -> 275 bytes
...786af4bf30dc5b07809a950792c.structure.json | 1 +
.../data/adb/fraud_dump/Text_Search.view.json | 1 +
...c888a45b895a4783b6dbd338f0155.data.json.gz | Bin 0 -> 20 bytes
...88a45b895a4783b6dbd338f0155.structure.json | 1 +
...ca6a6a72935fd370f79f3a3e62b0e.data.json.gz | Bin 0 -> 20 bytes
...6a6a72935fd370f79f3a3e62b0e.structure.json | 1 +
...2c8489196d21e33f194f4bafb3f05.data.json.gz | Bin 0 -> 20 bytes
...8489196d21e33f194f4bafb3f05.structure.json | 1 +
...3af7a2caabc3098bc21db7ce2759d.data.json.gz | Bin 0 -> 20 bytes
...f7a2caabc3098bc21db7ce2759d.structure.json | 1 +
...7636f2b54efb49f1f02feeeacfb01.data.json.gz | Bin 0 -> 292 bytes
...36f2b54efb49f1f02feeeacfb01.structure.json | 1 +
...c8ba0d331b61fccfd1e88cfedce00.data.json.gz | Bin 0 -> 20 bytes
...ba0d331b61fccfd1e88cfedce00.structure.json | 1 +
...1953e2b3a86325411a027c406e65a.data.json.gz | Bin 0 -> 1076 bytes
...53e2b3a86325411a027c406e65a.structure.json | 1 +
...8443e43d93dab7ebef303bbe9642f.data.json.gz | Bin 0 -> 1696 bytes
...43e43d93dab7ebef303bbe9642f.structure.json | 1 +
...af1f610a12434c9128e4a399cef8a.data.json.gz | Bin 0 -> 183 bytes
...1f610a12434c9128e4a399cef8a.structure.json | 1 +
...3a224b40d7b67210b78f2e390d00f.data.json.gz | Bin 0 -> 465 bytes
...224b40d7b67210b78f2e390d00f.structure.json | 1 +
...c1f9324753048c0096d036a694f86.data.json.gz | Bin 0 -> 794 bytes
...f9324753048c0096d036a694f86.structure.json | 1 +
tests/data/adb/fraud_dump/dump.json | 1 +
...5b76a2418eba4baeabc1ed9142b54.data.json.gz | Bin 0 -> 2292 bytes
...76a2418eba4baeabc1ed9142b54.structure.json | 1 +
...d06622c761cbceb384fd10a0b53f3.data.json.gz | Bin 0 -> 1277 bytes
...6622c761cbceb384fd10a0b53f3.structure.json | 1 +
...3a0d163cf508838e6b038c7b4d6cf.data.json.gz | Bin 0 -> 399 bytes
...0d163cf508838e6b038c7b4d6cf.structure.json | 1 +
tests/data/adb/got_dump/ENCRYPTION | 1 +
...d338ddbd547e41e4a1296de82963a.data.json.gz | Bin 0 -> 361 bytes
...38ddbd547e41e4a1296de82963a.structure.json | 1 +
tests/data/adb/got_dump/Text_Search.view.json | 1 +
...ff6d9ccc56c155188778aff284f7c.data.json.gz | Bin 0 -> 444 bytes
...6d9ccc56c155188778aff284f7c.structure.json | 1 +
...c888a45b895a4783b6dbd338f0155.data.json.gz | Bin 0 -> 20 bytes
...88a45b895a4783b6dbd338f0155.structure.json | 1 +
...ca6a6a72935fd370f79f3a3e62b0e.data.json.gz | Bin 0 -> 20 bytes
...6a6a72935fd370f79f3a3e62b0e.structure.json | 1 +
...2c8489196d21e33f194f4bafb3f05.data.json.gz | Bin 0 -> 20 bytes
...8489196d21e33f194f4bafb3f05.structure.json | 1 +
...3af7a2caabc3098bc21db7ce2759d.data.json.gz | Bin 0 -> 20 bytes
...f7a2caabc3098bc21db7ce2759d.structure.json | 1 +
...96aecb1ced2b96e78e137824f5e39.data.json.gz | Bin 0 -> 3071 bytes
...aecb1ced2b96e78e137824f5e39.structure.json | 1 +
...4de9a99a6607c1ea19444d2424dee.data.json.gz | Bin 0 -> 20 bytes
...e9a99a6607c1ea19444d2424dee.structure.json | 1 +
...7636f2b54efb49f1f02feeeacfb01.data.json.gz | Bin 0 -> 160 bytes
...36f2b54efb49f1f02feeeacfb01.structure.json | 1 +
...912aeadc6797e32beffa88f49e6e8.data.json.gz | Bin 0 -> 20 bytes
...2aeadc6797e32beffa88f49e6e8.structure.json | 1 +
...1f94fb7e8e2cc4da51136cf39a20b.data.json.gz | Bin 0 -> 99 bytes
...94fb7e8e2cc4da51136cf39a20b.structure.json | 1 +
...e7f9c62af75701e427bd5324df754.data.json.gz | Bin 0 -> 475 bytes
...f9c62af75701e427bd5324df754.structure.json | 1 +
tests/data/adb/got_dump/dump.json | 1 +
tests/data/adb/imdb_dump/ENCRYPTION | 1 +
.../data/adb/imdb_dump/Movies.structure.json | 1 +
...62e1f485e79d07ef4973f6b1b9f88.data.json.gz | Bin 0 -> 68107 bytes
.../data/adb/imdb_dump/Ratings.structure.json | 1 +
...cd33ae274522f351c266f028eed7b.data.json.gz | Bin 0 -> 1407601 bytes
tests/data/adb/imdb_dump/Users.structure.json | 1 +
...ae5fda8d810a29f12d1e61b4ab25f.data.json.gz | Bin 0 -> 16717 bytes
tests/data/adb/imdb_dump/dump.json | 1 +
tests/data/rdf/beatles.ttl | 35 +
tests/data/rdf/bnode.ttl | 12 +
tests/data/rdf/books.ttl | 20 +
tests/data/rdf/cases/1.ttl | 5 +
tests/data/rdf/cases/10.ttl | 11 +
tests/data/rdf/cases/11_1.ttl | 10 +
tests/data/rdf/cases/11_2.ttl | 13 +
tests/data/rdf/cases/12_1.ttl | 10 +
tests/data/rdf/cases/12_2.ttl | 10 +
tests/data/rdf/cases/13.ttl | 18 +
tests/data/rdf/cases/14_1.ttl | 4 +
tests/data/rdf/cases/14_2.ttl | 18 +
tests/data/rdf/cases/15.ttl | 18 +
tests/data/rdf/cases/2_1.ttl | 9 +
tests/data/rdf/cases/2_2.ttl | 4 +
tests/data/rdf/cases/2_3.ttl | 8 +
tests/data/rdf/cases/2_4.ttl | 5 +
tests/data/rdf/cases/3_1.ttl | 7 +
tests/data/rdf/cases/3_2.ttl | 4 +
tests/data/rdf/cases/4.ttl | 3 +
tests/data/rdf/cases/5.ttl | 4 +
tests/data/rdf/cases/6.trig | 23 +
tests/data/rdf/cases/7.ttl | 30 +
tests/data/rdf/cases/8.ttl | 11 +
tests/data/rdf/cases/9.ttl | 12 +
tests/data/rdf/collection.ttl | 31 +
tests/data/rdf/container.ttl | 68 +
tests/data/rdf/disjoint.ttl | 12 +
tests/data/rdf/foaf.ttl | 15 +
tests/data/rdf/hierarchy.ttl | 14 +
tests/data/rdf/key.ttl | 6 +
tests/data/rdf/rdfowl.ttl | 208 +
tests/data/rdf/rdfschema.ttl | 109 +
tests/data/rdf/reification.ttl | 10 +
tests/testRdfsOwlBase.py | 18 -
tests/test_main.py | 3980 ++++++++++++++++-
tests/tools/arangorestore | Bin 0 -> 8213232 bytes
127 files changed, 10615 insertions(+), 1631 deletions(-)
create mode 100644 arango_rdf/abc.py
create mode 100644 arango_rdf/controller.py
create mode 100644 arango_rdf/meta/adb.trig
create mode 100644 arango_rdf/meta/dc.trig
create mode 100644 arango_rdf/meta/owl.trig
create mode 100644 arango_rdf/meta/rdf.trig
create mode 100644 arango_rdf/meta/rdfs.trig
create mode 100644 arango_rdf/meta/xsd.trig
create mode 100644 arango_rdf/typings.py
create mode 100644 arango_rdf/utils.py
delete mode 100644 examples/data/airport-ontology.owl
delete mode 100644 examples/data/sfo-aircraft-partial.ttl
delete mode 100644 setup.cfg
create mode 100644 tests/data/adb/fraud_dump/Class_9bd81329febf6efe22788e03ddeaf0af.data.json.gz
create mode 100644 tests/data/adb/fraud_dump/Class_9bd81329febf6efe22788e03ddeaf0af.structure.json
create mode 100644 tests/data/adb/fraud_dump/Relationship_fbc97786af4bf30dc5b07809a950792c.data.json.gz
create mode 100644 tests/data/adb/fraud_dump/Relationship_fbc97786af4bf30dc5b07809a950792c.structure.json
create mode 100644 tests/data/adb/fraud_dump/Text_Search.view.json
create mode 100644 tests/data/adb/fraud_dump/_analyzers_839c888a45b895a4783b6dbd338f0155.data.json.gz
create mode 100644 tests/data/adb/fraud_dump/_analyzers_839c888a45b895a4783b6dbd338f0155.structure.json
create mode 100644 tests/data/adb/fraud_dump/_appbundles_105ca6a6a72935fd370f79f3a3e62b0e.data.json.gz
create mode 100644 tests/data/adb/fraud_dump/_appbundles_105ca6a6a72935fd370f79f3a3e62b0e.structure.json
create mode 100644 tests/data/adb/fraud_dump/_apps_c3f2c8489196d21e33f194f4bafb3f05.data.json.gz
create mode 100644 tests/data/adb/fraud_dump/_apps_c3f2c8489196d21e33f194f4bafb3f05.structure.json
create mode 100644 tests/data/adb/fraud_dump/_aqlfunctions_8293af7a2caabc3098bc21db7ce2759d.data.json.gz
create mode 100644 tests/data/adb/fraud_dump/_aqlfunctions_8293af7a2caabc3098bc21db7ce2759d.structure.json
create mode 100644 tests/data/adb/fraud_dump/_graphs_c827636f2b54efb49f1f02feeeacfb01.data.json.gz
create mode 100644 tests/data/adb/fraud_dump/_graphs_c827636f2b54efb49f1f02feeeacfb01.structure.json
create mode 100644 tests/data/adb/fraud_dump/_modules_5a8c8ba0d331b61fccfd1e88cfedce00.data.json.gz
create mode 100644 tests/data/adb/fraud_dump/_modules_5a8c8ba0d331b61fccfd1e88cfedce00.structure.json
create mode 100644 tests/data/adb/fraud_dump/accountHolder_2e31953e2b3a86325411a027c406e65a.data.json.gz
create mode 100644 tests/data/adb/fraud_dump/accountHolder_2e31953e2b3a86325411a027c406e65a.structure.json
create mode 100644 tests/data/adb/fraud_dump/account_e268443e43d93dab7ebef303bbe9642f.data.json.gz
create mode 100644 tests/data/adb/fraud_dump/account_e268443e43d93dab7ebef303bbe9642f.structure.json
create mode 100644 tests/data/adb/fraud_dump/bank_bd5af1f610a12434c9128e4a399cef8a.data.json.gz
create mode 100644 tests/data/adb/fraud_dump/bank_bd5af1f610a12434c9128e4a399cef8a.structure.json
create mode 100644 tests/data/adb/fraud_dump/branch_9603a224b40d7b67210b78f2e390d00f.data.json.gz
create mode 100644 tests/data/adb/fraud_dump/branch_9603a224b40d7b67210b78f2e390d00f.structure.json
create mode 100644 tests/data/adb/fraud_dump/customer_91ec1f9324753048c0096d036a694f86.data.json.gz
create mode 100644 tests/data/adb/fraud_dump/customer_91ec1f9324753048c0096d036a694f86.structure.json
create mode 100644 tests/data/adb/fraud_dump/dump.json
create mode 100644 tests/data/adb/fraud_dump/transaction_f4d5b76a2418eba4baeabc1ed9142b54.data.json.gz
create mode 100644 tests/data/adb/fraud_dump/transaction_f4d5b76a2418eba4baeabc1ed9142b54.structure.json
create mode 100644 tests/data/adb/got_dump/Characters_994d06622c761cbceb384fd10a0b53f3.data.json.gz
create mode 100644 tests/data/adb/got_dump/Characters_994d06622c761cbceb384fd10a0b53f3.structure.json
create mode 100644 tests/data/adb/got_dump/ChildOf_cb33a0d163cf508838e6b038c7b4d6cf.data.json.gz
create mode 100644 tests/data/adb/got_dump/ChildOf_cb33a0d163cf508838e6b038c7b4d6cf.structure.json
create mode 100644 tests/data/adb/got_dump/ENCRYPTION
create mode 100644 tests/data/adb/got_dump/Locations_eebd338ddbd547e41e4a1296de82963a.data.json.gz
create mode 100644 tests/data/adb/got_dump/Locations_eebd338ddbd547e41e4a1296de82963a.structure.json
create mode 100644 tests/data/adb/got_dump/Text_Search.view.json
create mode 100644 tests/data/adb/got_dump/Traits_42dff6d9ccc56c155188778aff284f7c.data.json.gz
create mode 100644 tests/data/adb/got_dump/Traits_42dff6d9ccc56c155188778aff284f7c.structure.json
create mode 100644 tests/data/adb/got_dump/_analyzers_839c888a45b895a4783b6dbd338f0155.data.json.gz
create mode 100644 tests/data/adb/got_dump/_analyzers_839c888a45b895a4783b6dbd338f0155.structure.json
create mode 100644 tests/data/adb/got_dump/_appbundles_105ca6a6a72935fd370f79f3a3e62b0e.data.json.gz
create mode 100644 tests/data/adb/got_dump/_appbundles_105ca6a6a72935fd370f79f3a3e62b0e.structure.json
create mode 100644 tests/data/adb/got_dump/_apps_c3f2c8489196d21e33f194f4bafb3f05.data.json.gz
create mode 100644 tests/data/adb/got_dump/_apps_c3f2c8489196d21e33f194f4bafb3f05.structure.json
create mode 100644 tests/data/adb/got_dump/_aqlfunctions_8293af7a2caabc3098bc21db7ce2759d.data.json.gz
create mode 100644 tests/data/adb/got_dump/_aqlfunctions_8293af7a2caabc3098bc21db7ce2759d.structure.json
create mode 100644 tests/data/adb/got_dump/_fishbowl_f8796aecb1ced2b96e78e137824f5e39.data.json.gz
create mode 100644 tests/data/adb/got_dump/_fishbowl_f8796aecb1ced2b96e78e137824f5e39.structure.json
create mode 100644 tests/data/adb/got_dump/_frontend_33a4de9a99a6607c1ea19444d2424dee.data.json.gz
create mode 100644 tests/data/adb/got_dump/_frontend_33a4de9a99a6607c1ea19444d2424dee.structure.json
create mode 100644 tests/data/adb/got_dump/_graphs_c827636f2b54efb49f1f02feeeacfb01.data.json.gz
create mode 100644 tests/data/adb/got_dump/_graphs_c827636f2b54efb49f1f02feeeacfb01.structure.json
create mode 100644 tests/data/adb/got_dump/_jobs_cf2912aeadc6797e32beffa88f49e6e8.data.json.gz
create mode 100644 tests/data/adb/got_dump/_jobs_cf2912aeadc6797e32beffa88f49e6e8.structure.json
create mode 100644 tests/data/adb/got_dump/_queues_3a31f94fb7e8e2cc4da51136cf39a20b.data.json.gz
create mode 100644 tests/data/adb/got_dump/_queues_3a31f94fb7e8e2cc4da51136cf39a20b.structure.json
create mode 100644 tests/data/adb/got_dump/_users_54de7f9c62af75701e427bd5324df754.data.json.gz
create mode 100644 tests/data/adb/got_dump/_users_54de7f9c62af75701e427bd5324df754.structure.json
create mode 100644 tests/data/adb/got_dump/dump.json
create mode 100644 tests/data/adb/imdb_dump/ENCRYPTION
create mode 100644 tests/data/adb/imdb_dump/Movies.structure.json
create mode 100644 tests/data/adb/imdb_dump/Movies_80662e1f485e79d07ef4973f6b1b9f88.data.json.gz
create mode 100644 tests/data/adb/imdb_dump/Ratings.structure.json
create mode 100644 tests/data/adb/imdb_dump/Ratings_e8dcd33ae274522f351c266f028eed7b.data.json.gz
create mode 100644 tests/data/adb/imdb_dump/Users.structure.json
create mode 100644 tests/data/adb/imdb_dump/Users_f9aae5fda8d810a29f12d1e61b4ab25f.data.json.gz
create mode 100644 tests/data/adb/imdb_dump/dump.json
create mode 100644 tests/data/rdf/beatles.ttl
create mode 100644 tests/data/rdf/bnode.ttl
create mode 100644 tests/data/rdf/books.ttl
create mode 100644 tests/data/rdf/cases/1.ttl
create mode 100644 tests/data/rdf/cases/10.ttl
create mode 100644 tests/data/rdf/cases/11_1.ttl
create mode 100644 tests/data/rdf/cases/11_2.ttl
create mode 100644 tests/data/rdf/cases/12_1.ttl
create mode 100644 tests/data/rdf/cases/12_2.ttl
create mode 100644 tests/data/rdf/cases/13.ttl
create mode 100644 tests/data/rdf/cases/14_1.ttl
create mode 100644 tests/data/rdf/cases/14_2.ttl
create mode 100644 tests/data/rdf/cases/15.ttl
create mode 100644 tests/data/rdf/cases/2_1.ttl
create mode 100644 tests/data/rdf/cases/2_2.ttl
create mode 100644 tests/data/rdf/cases/2_3.ttl
create mode 100644 tests/data/rdf/cases/2_4.ttl
create mode 100644 tests/data/rdf/cases/3_1.ttl
create mode 100644 tests/data/rdf/cases/3_2.ttl
create mode 100644 tests/data/rdf/cases/4.ttl
create mode 100644 tests/data/rdf/cases/5.ttl
create mode 100644 tests/data/rdf/cases/6.trig
create mode 100644 tests/data/rdf/cases/7.ttl
create mode 100644 tests/data/rdf/cases/8.ttl
create mode 100644 tests/data/rdf/cases/9.ttl
create mode 100644 tests/data/rdf/collection.ttl
create mode 100644 tests/data/rdf/container.ttl
create mode 100644 tests/data/rdf/disjoint.ttl
create mode 100644 tests/data/rdf/foaf.ttl
create mode 100644 tests/data/rdf/hierarchy.ttl
create mode 100644 tests/data/rdf/key.ttl
create mode 100644 tests/data/rdf/rdfowl.ttl
create mode 100644 tests/data/rdf/rdfschema.ttl
create mode 100644 tests/data/rdf/reification.ttl
delete mode 100644 tests/testRdfsOwlBase.py
create mode 100755 tests/tools/arangorestore
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 1baa8cf6..053f74cf 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -1,9 +1,8 @@
name: build
on:
workflow_dispatch:
- push:
- branches: [ main ]
pull_request:
+ push:
branches: [ main ]
env:
PACKAGE_DIR: arango_rdf
@@ -13,7 +12,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
- python: ["3.7", "3.8", "3.9"]
+ python: ["3.8", "3.9", "3.10", "3.11", "3.12"]
name: Python ${{ matrix.python }}
steps:
- uses: actions/checkout@v2
@@ -22,17 +21,21 @@ jobs:
with:
python-version: ${{ matrix.python }}
- name: Set up ArangoDB Instance via Docker
- run: docker create --name adb -p 8529:8529 -e ARANGO_ROOT_PASSWORD= arangodb/arangodb:3.9.1
+ run: docker create --name adb -p 8529:8529 -e ARANGO_ROOT_PASSWORD= arangodb/arangodb
- name: Start ArangoDB Instance
run: docker start adb
- name: Setup pip
- run: python -m pip install --upgrade pip setuptools wheel
+ run: pip install --upgrade pip setuptools wheel
- name: Install packages
run: pip install .[dev]
- name: Run black
run: black --check --verbose --diff --color ${{env.PACKAGE_DIR}} ${{env.TESTS_DIR}}
+ - name: Run flake8
+ run: flake8 ${{env.PACKAGE_DIR}} ${{env.TESTS_DIR}}
- name: Run isort
run: isort --check --profile=black ${{env.PACKAGE_DIR}} ${{env.TESTS_DIR}}
+ - name: Run mypy
+ run: mypy ${{env.PACKAGE_DIR}} ${{env.TESTS_DIR}}
- name: Run pytest
run: pytest --cov=${{env.PACKAGE_DIR}} --cov-report xml --cov-report term-missing -v --color=yes --no-cov-on-fail --code-highlight=yes
- name: Publish to coveralls.io
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 8b6a6668..ac040fc1 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -3,72 +3,34 @@ on:
workflow_dispatch:
release:
types: [published]
-env:
- PACKAGE_DIR: arango_rdf
- TESTS_DIR: tests
jobs:
- build:
- runs-on: ubuntu-latest
- strategy:
- matrix:
- python: ["3.7", "3.8", "3.9"]
- name: Python ${{ matrix.python }}
- steps:
- - uses: actions/checkout@v2
- - name: Setup Python ${{ matrix.python }}
- uses: actions/setup-python@v2
- with:
- python-version: ${{ matrix.python }}
- - name: Set up ArangoDB Instance via Docker
- run: docker create --name adb -p 8529:8529 -e ARANGO_ROOT_PASSWORD= arangodb/arangodb:3.9.1
- - name: Start ArangoDB Instance
- run: docker start adb
- - name: Setup pip
- run: python -m pip install --upgrade pip setuptools wheel
- - name: Install packages
- run: pip install .[dev]
- - name: Run black
- run: black --check --verbose --diff --color ${{env.PACKAGE_DIR}} ${{env.TESTS_DIR}}
- - name: Run isort
- run: isort --check --profile=black ${{env.PACKAGE_DIR}} ${{env.TESTS_DIR}}
- - name: Run pytest
- run: pytest --cov=${{env.PACKAGE_DIR}} --cov-report xml --cov-report term-missing -v --color=yes --no-cov-on-fail --code-highlight=yes
- - name: Publish to coveralls.io
- if: matrix.python == '3.8'
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- run: coveralls --service=github
-
release:
- needs: build
runs-on: ubuntu-latest
name: Release package
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v4
- name: Fetch complete history for all tags and branches
run: git fetch --prune --unshallow
- - name: Setup python
- uses: actions/setup-python@v2
+ - name: Setup Python
+ uses: actions/setup-python@v4
with:
- python-version: "3.8"
+ python-version: "3.10"
- name: Install release packages
run: pip install setuptools wheel twine setuptools-scm[toml]
- - name: Install dependencies
- run: pip install .[dev]
-
- name: Build distribution
run: python setup.py sdist bdist_wheel
- - name: Publish to PyPI Test
+ - name: Publish to Test PyPi
env:
TWINE_USERNAME: __token__
TWINE_PASSWORD: ${{ secrets.TWINE_PASSWORD_TEST }}
run: twine upload --repository testpypi dist/* #--skip-existing
- - name: Publish to PyPI
+
+ - name: Publish to PyPi
env:
TWINE_USERNAME: __token__
TWINE_PASSWORD: ${{ secrets.TWINE_PASSWORD }}
@@ -79,7 +41,7 @@ jobs:
runs-on: ubuntu-latest
name: Update Changelog
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v4
with:
fetch-depth: 0
@@ -91,10 +53,10 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- - name: Setup python
- uses: actions/setup-python@v2
+ - name: Setup Python
+ uses: actions/setup-python@v4
with:
- python-version: "3.8"
+ python-version: "3.10"
- name: Install release packages
run: pip install wheel gitchangelog pystache
@@ -106,12 +68,12 @@ jobs:
run: gitchangelog ${{env.VERSION}} > CHANGELOG.md
- name: Make commit for auto-generated changelog
- uses: EndBug/add-and-commit@v7
+ uses: EndBug/add-and-commit@v9
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
add: "CHANGELOG.md"
- branch: actions/changelog
+ new_branch: actions/changelog
message: "!gitchangelog"
- name: Create pull request for the auto generated changelog
@@ -124,4 +86,4 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Alert developer of open PR
- run: echo "Changelog $PR_URL is ready to be merged by developer."
\ No newline at end of file
+ run: echo "Changelog $PR_URL is ready to be merged by developer."
diff --git a/README.md b/README.md
index 783de97c..633fa317 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,4 @@
-# DEVELOPMENT VERSION - WIP - EXPECT BREAKING CHANGES
-___
-
-# Arango-RDF
+# ArangoRDF
[![build](https://github.com/ArangoDB-Community/ArangoRDF/actions/workflows/build.yml/badge.svg?branch=main)](https://github.com/ArangoDB-Community/ArangoRDF/actions/workflows/build.yml)
[![CodeQL](https://github.com/ArangoDB-Community/ArangoRDF/actions/workflows/analyze.yml/badge.svg?branch=main)](https://github.com/ArangoDB-Community/ArangoRDF/actions/workflows/analyze.yml)
@@ -18,7 +15,7 @@ ___
-Import/Export RDF graphs with ArangoDB
+Convert RDF Graphs to ArangoDB, and vice-versa.
## About RDF
@@ -47,58 +44,66 @@ pip install git+https://github.com/ArangoDB-Community/ArangoRDF
Run the full version with Google Colab:
```py
+from rdflib import Graph
from arango import ArangoClient
from arango_rdf import ArangoRDF
-db = ArangoClient(hosts="http://localhost:8529").db(
- "rdf", username="root", password="openSesame"
-)
+db = ArangoClient(hosts="http://localhost:8529").db("_system_", username="root", password="")
+
+adbrdf = ArangoRDF(db)
-# Clean up existing data and collections
-if db.has_graph("default_graph"):
- db.delete_graph("default_graph", drop_collections=True, ignore_missing=True)
+g = Graph()
+g.parse("https://raw.githubusercontent.com/stardog-union/stardog-tutorials/master/music/beatles.ttl")
-# Initializes default_graph and sets RDF graph identifier (ArangoDB sub_graph)
-# Optional: sub_graph (stores graph name as the 'graph' attribute on all edges in Statement collection)
-# Optional: default_graph (name of ArangoDB Named Graph, defaults to 'default_graph',
-# is root graph that contains all collections/relations)
-adb_rdf = ArangoRDF(db, sub_graph="http://data.sfgov.org/ontology")
-config = {"normalize_literals": False} # default: False
+# RDF to ArangoDB
+###################################################################################
-# RDF Import
-adb_rdf.init_rdf_collections(bnode="Blank")
+# 1.1: RDF-Topology Preserving Transformation (RPT)
+adbrdf.rdf_to_arangodb_by_rpt("Beatles", g, overwrite_graph=True)
-# Start with importing the ontology
-adb_graph = adb_rdf.import_rdf("./examples/data/airport-ontology.owl", format="xml", config=config, save_config=True)
+# 1.2: Property Graph Transformation (PGT)
+adbrdf.rdf_to_arangodb_by_pgt("Beatles", g, overwrite_graph=True)
-# Next, let's import the actual graph data
-adb_graph = adb_rdf.import_rdf(f"./examples/data/sfo-aircraft-partial.ttl", format="ttl", config=config, save_config=True)
+g = adbrdf.load_meta_ontology(g)
+# 1.3: RPT w/ Graph Contextualization
+adbrdf.rdf_to_arangodb_by_rpt("Beatles", g, contextualize_graph=True, overwrite_graph=True)
-# RDF Export
-# WARNING:
-# Exports ALL collections of the database,
-# currently does not account for default_graph or sub_graph
-# Results may vary, minifying may occur
-rdf_graph = adb_rdf.export_rdf(f"./examples/data/rdfExport.xml", format="xml")
+# 1.4: PGT w/ Graph Contextualization
+adbrdf.rdf_to_arangodb_by_pgt("Beatles", g, contextualize_graph=True, overwrite_graph=True)
-# Drop graph and ALL documents and collections to test import from exported data
-if db.has_graph("default_graph"):
- db.delete_graph("default_graph", drop_collections=True, ignore_missing=True)
+# 1.5: PGT w/ ArangoDB Document-to-Collection Mapping Exposed
+adb_mapping = adbrdf.build_adb_mapping_for_pgt(g)
+print(adb_mapping.serialize())
+adbrdf.rdf_to_arangodb_by_pgt("Beatles", g, adb_mapping, contextualize_graph=True, overwrite_graph=True)
-# Re-initialize our RDF Graph
-# Initializes default_graph and sets RDF graph identifier (ArangoDB sub_graph)
-adb_rdf = ArangoRDF(db, sub_graph="http://data.sfgov.org/ontology")
+# ArangoDB to RDF
+###################################################################################
-adb_rdf.init_rdf_collections(bnode="Blank")
+# Start from scratch!
+g = Graph()
+g.parse("https://raw.githubusercontent.com/stardog-union/stardog-tutorials/master/music/beatles.ttl")
+adbrdf.rdf_to_arangodb_by_pgt("Beatles", g, overwrite_graph=True)
-config = adb_rdf.get_config_by_latest() # gets the last config saved
-# config = adb_rdf.get_config_by_key_value('graph', 'music')
-# config = adb_rdf.get_config_by_key_value('AnyKeySuppliedInConfig', 'SomeValue')
+# 2.1: Via Graph Name
+g2, adb_mapping_2 = adbrdf.arangodb_graph_to_rdf("Beatles", Graph())
-# Re-import Exported data
-adb_graph = adb_rdf.import_rdf(f"./examples/data/rdfExport.xml", format="xml", config=config)
+# 2.2: Via Collection Names
+g3, adb_mapping_3 = adbrdf.arangodb_collections_to_rdf(
+ "Beatles",
+ Graph(),
+ v_cols={"Album", "Band", "Class", "Property", "SoloArtist", "Song"},
+ e_cols={"artist", "member", "track", "type", "writer"},
+)
+
+print(len(g2), len(adb_mapping_2))
+print(len(g3), len(adb_mapping_3))
+print('--------------------')
+print(g2.serialize())
+print('--------------------')
+print(adb_mapping_2.serialize())
+print('--------------------')
```
## Development & Testing
@@ -119,3 +124,75 @@ def pytest_addoption(parser):
parser.addoption("--password", action="store", default="")
```
+## Additional Info: RDF to ArangoDB
+
+RDF-to-ArangoDB functionality has been implemented using concepts described in the paper *[Transforming RDF-star to Property Graphs: A Preliminary Analysis of Transformation Approaches](https://arxiv.org/abs/2210.05781)*.
+
+In other words, `ArangoRDF` offers 2 RDF-to-ArangoDB transformation methods:
+1. RDF-topology Preserving Transformation (RPT): `ArangoRDF.rdf_to_arangodb_by_rpt()`
+2. Property Graph Transformation (PGT): `ArangoRDF.rdf_to_arangodb_by_pgt()`
+
+RPT preserves the RDF Graph structure by transforming each RDF Statement into an ArangoDB Edge.
+
+PGT on the other hand ensures that Datatype Property Statements are mapped as ArangoDB Document Properties.
+
+```ttl
+@prefix ex: .
+@prefix xsd: .
+ex:book ex:publish_date "1963-03-22"^^xsd:date .
+ex:book ex:pages "100"^^xsd:integer .
+ex:book ex:cover 20 .
+ex:book ex:index 55 .
+```
+
+| RPT | PGT |
+|:-------------------------:|:-------------------------:|
+| ![image](https://user-images.githubusercontent.com/43019056/232347662-ab48ebfb-e215-4aff-af28-a5915414a8fd.png) | ![image](https://user-images.githubusercontent.com/43019056/232347681-c899ef09-53c7-44de-861e-6a98d448b473.png) |
+
+--------------------
+### RPT
+
+
+The `ArangoRDF.rdf_to_arangodb_by_rpt` method will store the RDF Resources of your RDF Graph under the following ArangoDB Collections:
+
+ - {graph_name}_URIRef: The Document collection for `rdflib.term.URIRef` resources.
+ - {graph_name}_BNode: The Document collection for`rdflib.term.BNode` resources.
+ - {graph_name}_Literal: The Document collection for `rdflib.term.Literal` resources.
+ - {graph_name}_Statement: The Edge collection for all triples/quads.
+
+--------------------
+### PGT
+
+In contrast to RPT, the `ArangoRDF.rdf_to_arangodb_by_pgt` method will rely on the nature of the RDF Resource/Statement to determine which ArangoDB Collection it belongs to. This is referred as the **ArangoDB Collection Mapping Process**. This process relies on 2 fundamental URIs:
+
+1) `` (adb:collection)
+ - Any RDF Statement of the form ` "Person"` will map the Subject to the ArangoDB "Person" document collection.
+
+2) `` (rdf:type)
+ - This strategy is divided into 3 cases:
+
+ 1. If an RDF Resource only has one `rdf:type` statement,
+ then the local name of the RDF Object is used as the ArangoDB
+ Document Collection name. For example,
+ ` `
+ would create an JSON Document for ``,
+ and place it under the `Person` Document Collection.
+ NOTE: The RDF Object will also have its own JSON Document
+ created, and will be placed under the "Class"
+ Document Collection.
+
+ 2. If an RDF Resource has multiple `rdf:type` statements,
+ with some (or all) of the RDF Objects of those statements
+ belonging in an `rdfs:subClassOf` Taxonomy, then the
+ local name of the "most specific" Class within the Taxonomy is
+ used (i.e the Class with the biggest depth). If there is a
+ tie between 2+ Classes, then the URIs are alphabetically
+ sorted & the first one is picked.
+
+ 3. If an RDF Resource has multiple `rdf:type` statements, with none
+ of the RDF Objects of those statements belonging in an
+ `rdfs:subClassOf` Taxonomy, then the URIs are
+ alphabetically sorted & the first one is picked. The local
+ name of the selected URI will be designated as the Document
+ collection for that Resource.
+--------------------
diff --git a/arango_rdf/abc.py b/arango_rdf/abc.py
new file mode 100644
index 00000000..3e3f20d5
--- /dev/null
+++ b/arango_rdf/abc.py
@@ -0,0 +1,95 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+from abc import ABC
+from typing import Any, Optional, Set, Tuple, Union
+
+from arango.graph import Graph as ADBGraph
+from rdflib import BNode
+from rdflib import Graph as RDFGraph
+from rdflib import URIRef
+
+from .typings import ADBMetagraph
+from .utils import Tree
+
+
+class AbstractArangoRDF(ABC):
+ def __init__(self) -> None:
+ raise NotImplementedError # pragma: no cover
+
+ def rdf_to_arangodb_by_rpt(
+ self,
+ name: str,
+ rdf_graph: RDFGraph,
+ contextualize_graph: bool,
+ overwrite_graph: bool,
+ use_async: bool,
+ batch_size: Optional[int],
+ **import_options: Any,
+ ) -> ADBGraph:
+ raise NotImplementedError # pragma: no cover
+
+ def rdf_to_arangodb_by_pgt(
+ self,
+ name: str,
+ rdf_graph: RDFGraph,
+ contextualize_graph: bool,
+ overwrite_graph: bool,
+ use_async: bool,
+ batch_size: Optional[int],
+ adb_mapping: Optional[RDFGraph],
+ **import_options: Any,
+ ) -> ADBGraph:
+ raise NotImplementedError # pragma: no cover
+
+ def arangodb_to_rdf(
+ self,
+ name: str,
+ rdf_graph: RDFGraph,
+ metagraph: ADBMetagraph,
+ list_conversion_mode: str,
+ infer_type_from_adb_v_col: bool,
+ include_adb_key_statements: bool,
+ **export_options: Any,
+ ) -> Tuple[RDFGraph, RDFGraph]:
+ raise NotImplementedError # pragma: no cover
+
+ def arangodb_collections_to_rdf(
+ self,
+ name: str,
+ rdf_graph: RDFGraph,
+ v_cols: Set[str],
+ e_cols: Set[str],
+ list_conversion_mode: str,
+ infer_type_from_adb_v_col: bool,
+ include_adb_key_statements: bool,
+ **export_options: Any,
+ ) -> Tuple[RDFGraph, RDFGraph]:
+ raise NotImplementedError # pragma: no cover
+
+ def arangodb_graph_to_rdf(
+ self,
+ name: str,
+ rdf_graph: RDFGraph,
+ list_conversion_mode: str,
+ infer_type_from_adb_v_col: bool,
+ include_adb_key_statements: bool,
+ **export_options: Any,
+ ) -> Tuple[RDFGraph, RDFGraph]:
+ raise NotImplementedError # pragma: no cover
+
+ def __fetch_adb_docs(self) -> None:
+ raise NotImplementedError # pragma: no cover
+
+ def __insert_adb_docs(self) -> None:
+ raise NotImplementedError # pragma: no cover
+
+
+class AbstractArangoRDFController(ABC):
+ def identify_best_class(
+ self,
+ rdf_resource: Union[URIRef, BNode],
+ class_set: Set[str],
+ subclass_tree: Tree,
+ ) -> str:
+ raise NotImplementedError # pragma: no cover
diff --git a/arango_rdf/controller.py b/arango_rdf/controller.py
new file mode 100644
index 00000000..b2c02a31
--- /dev/null
+++ b/arango_rdf/controller.py
@@ -0,0 +1,106 @@
+#!/usr/bin/env python3
+from typing import Set
+
+from arango.database import StandardDatabase
+from rdflib import Graph
+
+from .abc import AbstractArangoRDFController
+from .typings import RDFTerm
+from .utils import Tree
+
+
+class ArangoRDFController(AbstractArangoRDFController):
+ """ArangoDB-RDF controller.
+
+ You can derive your own custom ArangoRDFController.
+ """
+
+ def __init__(self) -> None:
+ self.db: StandardDatabase
+ self.rdf_graph: Graph
+
+ def identify_best_class(
+ self,
+ rdf_resource: RDFTerm,
+ class_set: Set[str],
+ subclass_tree: Tree,
+ ) -> str:
+ """Find the ideal RDFS Class among a selection of RDFS Classes. Essential
+ for the ArangoDB Collection Mapping Process used in RDF-to-ArangoDB (PGT).
+
+ The "ideal RDFS Class" is defined as an RDFS Class whose local name can be
+ used as the ArangoDB Document Collection that will store **rdf_resource**.
+
+ This system is a work-in-progress. Users are welcome to overwrite this
+ method via their own implementation of the `ArangoRDFController`
+ Python Class.
+
+ NOTE: Users are able to access the RDF Graph of the current
+ RDF-to-ArangoDB transformation via the `self.rdf_graph`
+ instance variable, and the database instance via the
+ `self.db` instance variable.
+
+ The current identification process goes as follows:
+ 1) If an RDF Resource only has one `rdf:type` statement
+ (either by explicit definition or by domain/range inference),
+ then the local name of the single RDFS Class is used as the ArangoDB
+ Document Collection name. For example,
+
+ would place the JSON Document for
+ under the ArangoDB "Person" Document Collection.
+
+ 2) If an RDF Resource has multiple `rdf:type` statements
+ (either by explicit definition or by domain/range inference),
+ with some (or all) of the RDFS Classes of those statements
+ belonging in an `rdfs:subClassOf` Taxonomy, then the
+ local name of the "most specific" Class within the Taxonomy is
+ used (i.e the Class with the biggest depth). If there is a
+ tie between 2+ Classes, then the URIs are alphabetically
+ sorted & the first one is picked. Relies on **subclass_tree**.
+
+ 3) If an RDF Resource has multiple `rdf:type` statements, with
+ none of the RDFS Classes of those statements belonging in an
+ `rdfs:subClassOf` Taxonomy, then the URIs are
+ alphabetically sorted & the first one is picked. The local
+ name of the selected URI will be designated as the Document
+ Collection for **rdf_resource**.
+
+ :param rdf_resource: The RDF Resource in question.
+ :type rdf_resource: URIRef | BNode
+ :param class_set: A set of RDFS Class URIs that
+ are associated to **rdf_resource** via the `RDF.Type`
+ relationship, either via explicit definition or via
+ domain/range inference.
+ :type class_set: Set[str]
+ :param subclass_tree: The Tree data structure representing
+ the RDFS subClassOf Taxonomy. See `ArangoRDF.__build_subclass_tree()`
+ for more info.
+ :type subclass_tree: arango_rdf.utils.Tree
+ :return: The most suitable RDFS Class URI among the set of RDFS Classes
+ to use as the ArangoDB Document Collection name associated to
+ **rdf_resource**.
+ :rtype: str
+ """
+ # These are accessible!
+ # print(self.db)
+ # print(self.rdf_graph)
+
+ best_class = ""
+
+ if len(class_set) == 1:
+ best_class = list(class_set)[0]
+
+ elif any([c in subclass_tree for c in class_set]):
+ best_depth = -1
+
+ for c in sorted(class_set):
+ depth = subclass_tree.get_node_depth(c)
+
+ if depth > best_depth:
+ best_depth = depth
+ best_class = c
+
+ else:
+ best_class = sorted(class_set)[0]
+
+ return best_class
diff --git a/arango_rdf/main.py b/arango_rdf/main.py
index 9e5dc907..c91973d0 100644
--- a/arango_rdf/main.py
+++ b/arango_rdf/main.py
@@ -1,398 +1,2904 @@
#!/usr/bin/env python3
-# -*- coding: utf-8 -*-
-
-# author @David Vidovich (Mission Solutions Group)
-# author @Arthur Keen (ArangoDB)
-
-import hashlib
-import sys
-import time
+import gc
+import logging
+import os
+import re
+from ast import literal_eval
from collections import defaultdict
-from typing import Any, Dict, List, Optional, Union
+from datetime import date, time
+from pathlib import Path
+from typing import Any, Callable, DefaultDict, Dict, List, Optional, Set, Tuple, Union
-# from rdflib.namespace import RDFS, OWL
+from arango.cursor import Cursor
from arango.database import StandardDatabase
-from arango.graph import Graph as ArangoGraph
-from rdflib import BNode
+from arango.graph import Graph as ADBGraph
+from arango.result import Result
+from farmhash import Fingerprint64 as FP64
+from isodate import Duration
+from rdflib import RDF, RDFS, XSD, BNode
+from rdflib import ConjunctiveGraph as RDFConjunctiveGraph
+from rdflib import Dataset as RDFDataset
from rdflib import Graph as RDFGraph
from rdflib import Literal, URIRef
-from tqdm import tqdm
-
+from rich.console import Group
+from rich.live import Live
+
+from .abc import AbstractArangoRDF
+from .controller import ArangoRDFController
+from .typings import (
+ ADBDocs,
+ ADBMetagraph,
+ Json,
+ PredicateScope,
+ RDFListData,
+ RDFListHeads,
+ RDFTerm,
+ RDFTermMeta,
+ TypeMap,
+)
+from .utils import Node, Tree, adb_track, empty_function, logger, rdf_track
+
+PROJECT_DIR = Path(__file__).parent
+
+
+class ArangoRDF(AbstractArangoRDF):
+ """ArangoRDF: Transform RDF Graphs into
+ ArangoDB Graphs & vice-versa.
+
+ Implemented using concepts referred in
+ https://arxiv.org/abs/2210.05781.
+
+ :param db: A python-arango database instance
+ :type db: arango.database.Database
+ :param logging_lvl: Defaults to logging.INFO. Other useful options are
+ logging.DEBUG (more verbose), and logging.WARNING (less verbose).
+ :type logging_lvl: str | int
+ :raise TypeError: On invalid parameter types
+ """
-class ArangoRDF:
def __init__(
self,
db: StandardDatabase,
- default_graph: str = "default_graph",
- sub_graph: Optional[str] = None,
+ controller: ArangoRDFController = ArangoRDFController(),
+ logging_lvl: Union[str, int] = logging.INFO,
+ ):
+ self.set_logging(logging_lvl)
+
+ if not isinstance(db, StandardDatabase):
+ msg = "**db** parameter must inherit from arango.database.StandardDatabase"
+ raise TypeError(msg)
+
+ if not isinstance(controller, ArangoRDFController):
+ msg = "**controller** parameter must inherit from ArangoRDFController"
+ raise TypeError(msg)
+
+ self.db = db
+ self.async_db = db.begin_async_execution(return_result=False)
+
+ self.controller = controller
+ self.controller.db = db
+
+ # `adb_docs`: An RDF to ArangoDB variable used as a buffer
+ # to store the to-be-inserted ArangoDB documents (RDF-to-ArangoDB).
+ self.adb_docs: ADBDocs
+
+ # `adb_col_uri`: An RDF predicate used to identify
+ # the ArangoDB Collection of an arbitrary RDF Resource.
+ # e.g ( "Person")
+ self.adb_col_uri = URIRef("http://www.arangodb.com/collection")
+
+ # `adb_key_uri`: An RDF predicate used to identify
+ # the ArangoDB Key of an arbitrary RDF Resource.
+ # e.g ( "4502")
+ self.adb_key_uri = URIRef("http://www.arangodb.com/key")
+
+ # Builds the ArangoDB Edge Definitions of the (soon to be) ArangoDB Graph
+ # Only used in RDF-to-ArangoDB methods.
+ self.__e_col_map: DefaultDict[str, DefaultDict[str, Set[str]]]
+
+ # `meta_graph`: An RDF Conjunctive Graph representing the
+ # Ontology files found under the `arango_rdf/meta/` directory.
+ # Essential for fully contextualizing an RDF Graph in ArangoDB.
+ self.meta_graph = RDFConjunctiveGraph()
+ for ns in os.listdir(f"{PROJECT_DIR}/meta"):
+ self.meta_graph.parse(f"{PROJECT_DIR}/meta/{ns}", format="trig")
+
+ # `rdf_graph`: An instance variable that serves as a shortcut of
+ # the current RDF Graph. Used in ArangoDB-to-RDF & RDF-to-ArangoDB methods.
+ self.rdf_graph = RDFGraph()
+ self.__adb_ns = "http://www.arangodb.com/"
+
+ # Commonly used URIs
+ self.__rdfs_resource_str = str(RDFS.Resource)
+ self.__rdfs_class_str = str(RDFS.Class)
+ self.__rdfs_literal_str = str(RDFS.Literal)
+ self.__rdfs_domain_str = str(RDFS.domain)
+ self.__rdfs_range_str = str(RDFS.range)
+ self.__rdf_type_str = str(RDF.type)
+ self.__rdf_property_str = str(RDF.Property)
+
+ # Commonly used ArangoDB Keys (derived from the commonly used URIs)
+ self.__rdf_type_key = self.rdf_id_to_adb_key(self.__rdf_type_str)
+ self.__rdf_property_key = self.rdf_id_to_adb_key(self.__rdf_property_str)
+ self.__rdfs_domain_key = self.rdf_id_to_adb_key(self.__rdfs_domain_str)
+ self.__rdfs_range_key = self.rdf_id_to_adb_key(self.__rdfs_range_str)
+
+ logger.info(f"Instantiated ArangoRDF with database '{db.name}'")
+
+ def set_logging(self, level: Union[int, str]) -> None:
+ logger.setLevel(level)
+
+ def __set_iterators(
+ self, rdf_iter_text: str, rdf_iter_color: str, adb_iter_text: str
) -> None:
+ self.__rdf_iterator = rdf_track(rdf_iter_text, rdf_iter_color)
+ self.__adb_iterator = adb_track(adb_iter_text)
+
+ ###################################################################################
+ # RDF to ArangoDB: RPT Methods
+ # * rdf_to_arangodb_by_rpt:
+ # * __rpt_process_term:
+ # * __rpt_process_statement:
+ # * __rpt_create_adb_graph
+ ###################################################################################
+
+ def rdf_to_arangodb_by_rpt(
+ self,
+ name: str,
+ rdf_graph: RDFGraph,
+ contextualize_graph: bool = False,
+ overwrite_graph: bool = False,
+ use_async: bool = True,
+ batch_size: Optional[int] = None,
+ keyify_literals: bool = True,
+ simplify_reified_triples: bool = True,
+ **import_options: Any,
+ ) -> ADBGraph:
+ """Create an ArangoDB Graph from an RDF Graph using
+ the RDF-topology-preserving transformation (RPT) Algorithm.
+
+ RPT tries is to preserve the RDF Graph structure by transforming
+ each RDF statement into an edge in the Property Graph. More info on
+ RPT can be foundin the package's README file, or in the following
+ paper: https://arxiv.org/pdf/2210.05781.pdf.
+
+ The `rdf_to_arangodb_by_rpt` method will store the RDF Resources of
+ **rdf_graph** under the following ArangoDB Collections:
+ - {name}_URIRef: The Document collection for `rdflib.term.URIRef` resources.
+ - {name}_BNode: The Document collection for`rdflib.term.BNode` resources.
+ - {name}_Literal: The Document collection for `rdflib.term.Literal` resources.
+ - {name}_Statement: The Edge collection for all triples/quads.
+
+ :param name: The name of the RDF Graph
+ :type name: str
+ :param rdf_graph: The RDF Graph object. NOTE: This method does not
+ currently support RDF graphs of type `rdflib.graph.Dataset`.
+ Apologies for the inconvenience.
+ :type: rdf_graph: rdflib.graph.Graph
+ :param contextualize_graph: A work-in-progress flag that seeks
+ to enhance the Terminology Box of **rdf_graph** by providing
+ the following features:
+ 1) Process RDF Predicates within **rdf_graph** as their own ArangoDB
+ Document, and cast a (predicate RDF.type RDF.Property) edge
+ relationship into the ArangoDB graph for every RDF predicate
+ used in the form (subject predicate object) within **rdf_graph**.
+ 2) Provide RDFS.Domain & RDFS.Range Inference on all
+ RDF Resources within the **rdf_graph**, so long that no
+ RDF.Type statement already exists in **rdf_graph**
+ for the given resource.
+ 3) Provide RDFS.Domain & RDFS.Range Introspection on all
+ RDF Predicates with the **rdf_graph**, so long that
+ no RDFS.Domain or RDFS.Range statement already exists
+ for the given predicate.
+ 4) TODO - What's next?
+ :type contextualize_graph: bool
+ :param overwrite_graph: Overwrites the ArangoDB graph identified
+ by **name** if it already exists, and drops its associated collections.
+ Defaults to False.
+ :type overwrite_graph: bool
+ :param use_async: Performs asynchronous ArangoDB ingestion if enabled.
+ Defaults to True.
+ :type use_async: bool
+ :param batch_size: If specified, runs the ArangoDB Data Ingestion
+ process for every **batch_size** RDF triples/quads within **rdf_graph**.
+ Defaults to `len(rdf_graph)`.
+ :type batch_size: int | None
+ :param keyify_literals: If set to False, will not use the hashed value of an
+ RDF Literal as its ArangoDB Document Key (i.e a randomly-generated
+ key will instead be used). If set to True, all RDF Literals with the same
+ value will be represented as one single ArangoDB Document. Defaults to True.
+ :type keyify_literals: bool
+ :param simplify_reified_triples: If set to False, will preserve the RDF
+ Structure of any reified triples. If set to True, will convert any reified
+ triples into regular ArangoDB edges. Defaults to True.
+ :type simplify_reified_triples: bool
+ :param import_options: Keyword arguments to specify additional
+ parameters for the ArangoDB Data Ingestion process.
+ The full parameter list is here:
+ https://docs.python-arango.com/en/main/specs.html#arango.collection.Collection.import_bulk
+ :type import_options: Any
+ :return: The ArangoDB Graph API wrapper.
+ :rtype: arango.graph.Graph
"""
- Parameters
- ----------
- db: StandardDatabase
- The python-arango database client
- default_graph: str
- The name of the ArangoDB graph that contains all collections
- sub_graph: str | None
- The identifier of the RDF graph that defines an ArangoDB sub-graph
- that only contains the nodes & edges of a specific graph
+ if isinstance(rdf_graph, RDFDataset):
+ raise TypeError( # pragma: no cover
+ """
+ Invalid type for **rdf_graph**: ArangoRDF does not yet
+ support RDF Graphs of type rdflib.graph.Dataset
+ """
+ )
+
+ self.rdf_graph = rdf_graph
+
+ # Reset the ArangoDB Config
+ self.adb_docs = defaultdict(lambda: defaultdict(dict))
+ self.__keyify_literals = keyify_literals
+ self.__simplify_reified_triples = simplify_reified_triples
+ self.__import_options = import_options
+ self.__import_options["on_duplicate"] = "update"
+
+ # Set the RPT ArangoDB Collection names
+ self.__URIREF_COL = f"{name}_URIRef"
+ self.__BNODE_COL = f"{name}_BNode"
+ self.__LITERAL_COL = f"{name}_Literal"
+ self.__STATEMENT_COL = f"{name}_Statement"
+
+ # Builds the ArangoDB Edge Definitions of the (soon to be) ArangoDB Graph
+ self.__e_col_map = defaultdict(lambda: defaultdict(set))
+ self.__e_col_map[self.__STATEMENT_COL] = defaultdict(set)
+
+ if overwrite_graph:
+ self.db.delete_graph(name, ignore_missing=True, drop_collections=True)
+
+ # NOTE: Graph Contextualization is an experimental work-in-progress
+ if contextualize_graph:
+ self.rdf_graph = self.load_base_ontology(rdf_graph)
+ explicit_type_map = self.__build_explicit_type_map()
+ predicate_scope = self.__build_predicate_scope()
+ domain_range_map = self.__build_domain_range_map(predicate_scope)
+ type_map = self.__combine_type_map_and_dr_map(
+ explicit_type_map, domain_range_map
+ )
+
+ size = len(self.rdf_graph)
+ if batch_size is None:
+ batch_size = size
+
+ s: RDFTerm # Subject
+ p: URIRef # Predicate
+ o: RDFTerm # Object
+ sg: Optional[RDFGraph] # Sub Graph
+
+ reified_triple_blacklist = set()
+ if simplify_reified_triples:
+ reified_triple_blacklist.update(
+ {
+ RDF.subject,
+ RDF.predicate,
+ RDF.object,
+ }
+ )
+
+ statements = (
+ self.rdf_graph.quads
+ if isinstance(self.rdf_graph, RDFConjunctiveGraph)
+ else self.rdf_graph.triples
+ )
+
+ self.__set_iterators("RDF → ADB (RPT)", "#08479E", " ")
+ with Live(Group(self.__rdf_iterator, self.__adb_iterator)):
+ self.__rdf_task = self.__rdf_iterator.add_task("", total=size)
+
+ t = (None, None, None)
+ for count, (s, p, o, *rest) in enumerate(statements(t), 1):
+ self.__rdf_iterator.update(self.__rdf_task, advance=1)
+
+ if p in reified_triple_blacklist:
+ continue
+
+ reified_triple_key = None
+ if simplify_reified_triples and (p, o) == (RDF.type, RDF.Statement):
+ s, p, o, reified_triple_key = self.__parse_reified_triple(s)
+
+ # Get the Sub Graph URI (if it exists)
+ sg = rest[0] if rest else None
+ sg_str = str(sg.identifier) if sg else ""
+
+ # Load the RDF Subject & Object as ArangoDB Documents
+ s_meta = self.__rpt_process_term(s)
+
+ if p == self.adb_key_uri:
+ continue
+
+ o_meta = self.__rpt_process_term(o)
+
+ self.__rpt_process_statement(
+ s_meta, p, o_meta, sg_str, reified_triple_key
+ )
+
+ # NOTE: Graph Contextualization is an experimental work-in-progress
+ if contextualize_graph:
+ # Load the RDF Predicate as an ArangoDB Document
+ p_meta = self.__rpt_process_term(p)
+ _, _, p_key, _ = p_meta
+
+ # Create the ArangoDB Edge
+ # p_has_no_type_statement = len(type_map[p]) == 0
+ p_has_no_type_statement = (p, RDF.type, None) not in self.rdf_graph
+ if p_has_no_type_statement:
+ key = f"{p_key}-{self.__rdf_type_key}-{self.__rdf_property_key}"
+ self.__add_adb_edge(
+ self.__STATEMENT_COL,
+ str(FP64(key)),
+ f"{self.__URIREF_COL}/{p_key}",
+ f"{self.__URIREF_COL}/{self.__rdf_property_key}",
+ self.__rdf_type_str,
+ "type",
+ sg_str,
+ )
+
+ # Run RDFS Domain/Range Inference & Introspection
+ dr_meta = [(*s_meta, "domain"), (*o_meta, "range")]
+ self.__infer_and_introspect_dr(
+ p,
+ p_key,
+ dr_meta,
+ type_map,
+ predicate_scope,
+ sg_str,
+ is_rpt=True,
+ )
+
+ # Empty `self.adb_docs` into ArangoDB once `batch_size` has been reached
+ if count % batch_size == 0:
+ self.__insert_adb_docs(use_async)
+
+ # Insert the remaining `self.adb_docs` into ArangoDB
+ self.__insert_adb_docs(use_async)
+
+ assert len(self.adb_docs) == 0
+ return self.__rpt_create_adb_graph(name)
+
+ def __rpt_process_term(self, t: RDFTerm) -> RDFTermMeta:
+ """Process an RDF Term as an ArangoDB document via RPT Standards. Returns the
+ ArangoDB Collection & Document Key associated to the RDF term along with
+ the string representation of the RDF term.
+
+ :param t: The RDF Term to process
+ :type t: URIRef | BNode | Literal
+ :return: The RDF Term object, along with its associated ArangoDB
+ Collection name, Document Key, and Document label.
+ :rtype: Tuple[URIRef | BNode | Literal, str, str, str]
"""
- self.db: StandardDatabase = db
- self.default_graph = default_graph
- self.sub_graph = sub_graph
+ t_str = str(t)
+ t_col = ""
+ t_key = self.rdf_id_to_adb_key(t_str, t)
+ t_label = ""
- # Create the graph
- if self.db.has_graph(default_graph):
- self.graph = self.db.graph(default_graph)
- else:
- self.graph = self.db.create_graph(default_graph)
+ if (
+ self.__simplify_reified_triples
+ and (t, RDF.type, RDF.Statement) in self.rdf_graph
+ ):
+ t_col = self.__STATEMENT_COL
+
+ elif type(t) is URIRef:
+ t_col = self.__URIREF_COL
+ t_label = self.rdf_id_to_adb_label(t_str)
+
+ self.adb_docs[t_col][t_key] = {
+ "_key": t_key,
+ "_uri": t_str,
+ "_label": t_label,
+ "_rdftype": "URIRef",
+ }
+
+ elif type(t) is BNode:
+ t_col = self.__BNODE_COL
+
+ self.adb_docs[t_col][t_key] = {
+ "_key": t_key,
+ "_label": "",
+ "_rdftype": "BNode",
+ }
+
+ elif type(t) is Literal:
+ t_col = self.__LITERAL_COL
+ t_value = self.__get_literal_val(t, t_str)
+ t_label = t_value
+
+ self.adb_docs[t_col][t_key] = {
+ "_value": t_value,
+ "_label": t_label, # TODO: REVISIT
+ "_rdftype": "Literal",
+ }
+
+ if self.__keyify_literals:
+ self.adb_docs[t_col][t_key]["_key"] = t_key
+
+ if t.language:
+ self.adb_docs[t_col][t_key]["_lang"] = t.language
+ elif t.datatype:
+ self.adb_docs[t_col][t_key]["_datatype"] = str(t.datatype)
- self.__set_sub_graph = sub_graph is not None
- self.rdf_graph = RDFGraph(identifier=sub_graph)
+ else:
+ raise ValueError() # pragma: no cover
- # Maps the default RDF collection names to the user-specified RDF collection names
- self.col_map: Dict[str, str] = {}
+ return t, t_col, t_key, t_label
- def init_rdf_collections(
+ def __rpt_process_statement(
self,
- iri: str = "IRI",
- bnode: str = "BNode",
- literal: str = "Literal",
- edge: str = "Statement",
+ s_meta: RDFTermMeta,
+ p: URIRef,
+ o_meta: RDFTermMeta,
+ sg_str: str,
+ reified_triple_key: Optional[str] = None,
) -> None:
+ """Processes the RDF Statement (s, p, o) as an ArangoDB edge for RPT.
+
+ :param s_meta: The RDF Term Metadata associated to the
+ RDF Subject of the statement containing the RDF Object.
+ :type s_meta: arango_rdf.typings.RDFTermMeta
+ :param p: The RDF Predicate URIRef of the statement (s, p, o).
+ :type p: URIRef
+ :param o_meta: The RDF Term Metadata associated to the RDF Object.
+ :type o_meta: arango_rdf.typings.RDFTermMeta
+ :param sg_str: The string representation of the sub-graph URIRef associated
+ to this statement (if any).
+ :type sg_str: str
"""
- Creates the node and edge collections for rdf import.
-
- Parameters
- ----------
- iri: str
- the name of the collection that will store the IRI nodes (default is "IRI")
- bnode: str
- the name of the collection that will store blank nodes (default is "BNode")
- literal: str
- the name of collection that will store literals (default is "Literal")
- edge: str
- the name of the edge collection that will connect the nodes (default is "Statement")
- """
- # init collections
- self.init_collection(iri, "iri")
- self.init_collection(bnode, "bnode")
- self.init_collection(literal, "literal")
- self.init_edge_collection(
- edge, [iri, bnode], [iri, literal, bnode], "statement"
+ _, s_col, s_key, _ = s_meta
+ _, o_col, o_key, _ = o_meta
+
+ p_str = str(p)
+ p_key = self.rdf_id_to_adb_key(p_str)
+ p_label = self.rdf_id_to_adb_label(p_str)
+
+ e_key = reified_triple_key or str(FP64(f"{s_key}-{p_key}-{o_key}"))
+
+ self.__add_adb_edge(
+ self.__STATEMENT_COL,
+ e_key,
+ f"{s_col}/{s_key}",
+ f"{o_col}/{o_key}",
+ p_str,
+ p_label,
+ sg_str,
)
- def init_collection(self, name: str, default_name: str) -> None:
- """
- Creates collection if it doesn't already exist
+ def __rpt_create_adb_graph(self, name: str) -> ADBGraph:
+ """Create an ArangoDB graph based on an RPT Transformation.
- parameters
- ----------
- name: str
- the name of the collection that will be created
- default_name: str
- the name that will be used to reference the collection in the code
+ :param name: The ArangoDB Graph name
+ :type name: str
+ :return: The ArangoDB Graph API wrapper.
+ :rtype: arango.graph.Graph
"""
- if self.db.has_collection(name) is False:
- self.db.create_collection(name)
- self.col_map[default_name] = name
+ if self.db.has_graph(name): # pragma: no cover
+ return self.db.graph(name)
+
+ return self.db.create_graph(
+ name,
+ edge_definitions=[
+ {
+ "edge_collection": self.__STATEMENT_COL,
+ "from_vertex_collections": [
+ self.__URIREF_COL,
+ self.__BNODE_COL,
+ ],
+ "to_vertex_collections": [
+ self.__URIREF_COL,
+ self.__BNODE_COL,
+ self.__LITERAL_COL,
+ ],
+ }
+ ],
+ )
- def init_edge_collection(
+ ###################################################################################
+ # RDF to ArangoDB: PGT Methods
+ # * rdf_to_arangodb_by_pgt:
+ # * build_adb_mapping_for_pgt:
+ # * __pgt_get_term_metadata:
+ # * __pgt_rdf_val_to_adb_val:
+ # * __pgt_process_rdf_term:
+ # * __pgt_process_object:
+ # * __pgt_process_statement:
+ # * __pgt_object_is_head_of_rdf_list:
+ # * __pgt_statement_is_part_of_rdf_list:
+ # * __pgt_process_rdf_lists:
+ # * __pgt_process_rdf_list_object:
+ # * __pgt_unpack_rdf_collection:
+ # * __pgt_unpack_rdf_container:
+ # * __pgt_create_adb_graph:
+ ###################################################################################
+
+ def rdf_to_arangodb_by_pgt(
self,
name: str,
- parent_collections: List[str],
- child_collections: List[str],
- default_name: str,
- ) -> None:
+ rdf_graph: RDFGraph,
+ contextualize_graph: bool = False,
+ overwrite_graph: bool = False,
+ use_async: bool = True,
+ batch_size: Optional[int] = None,
+ adb_mapping: Optional[RDFGraph] = None,
+ simplify_reified_triples: bool = True,
+ **import_options: Any,
+ ) -> ADBGraph:
+ """Create an ArangoDB Graph from an RDF Graph using
+ the Property Graph Transformation (PGT) Algorithm.
+
+ In contrast to RPT, PGT ensures that datatype property statements are
+ mapped to node properties in the PG. More info on PGT can be found
+ in the package's README file, or in the following
+ paper: https://arxiv.org/pdf/2210.05781.pdf.
+
+ In contrast to RPT, the `rdf_to_arangodb_by_pgt` method will rely on
+ the nature of the RDF Resource/Statement to determine which ArangoDB
+ Collection it belongs to. The ArangoDB Collection mapping process relies
+ on two fundamental URIs:
+
+ 1) (adb:collection)
+ - Any RDF Statement of the form
+ "Person"
+ will map the Subject to the ArangoDB
+ "Person" document collection.
+
+ 2) (rdf:type)
+ - This strategy is divided into 3 cases:
+ 2.1) If an RDF Resource only has one `rdf:type` statement,
+ then the local name of the RDF Object is used as the ArangoDB
+ Document Collection name. For example,
+
+ would create an JSON Document for ,
+ and place it under the "Person" Document Collection.
+ NOTE: The RDF Object will also have its own JSON Document
+ created, and will be placed under the "Class"
+ Document Collection.
+
+ 2.2) If an RDF Resource has multiple `rdf:type` statements,
+ with some (or all) of the RDF Objects of those statements
+ belonging in an `rdfs:subClassOf` Taxonomy, then the
+ local name of the "most specific" Class within the Taxonomy is
+ used (i.e the Class with the biggest depth). If there is a
+ tie between 2+ Classes, then the URIs are alphabetically
+ sorted & the first one is picked.
+
+ 2.3) If an RDF Resource has multiple `rdf:type` statements, with
+ none of the RDF Objects of those statements belonging in an
+ `rdfs:subClassOf` Taxonomy, then the URIs are
+ alphabetically sorted & the first one is picked. The local
+ name of the selected URI will be designated as the Document
+ collection for that Resource.
+
+ NOTE 1: If `contextualize_graph` is set to True, then additional `rdf:type`
+ statements may be generated via ArangoRDF's Domain & Range Inference
+ feature. These "synthetic" statements will be considered when mapping
+ RDF Resources to the correct ArangoDB Collections, but ONLY if there
+ were no "original" rdf:type statements to consider for
+ the given RDF Resource.
+
+ NOTE 2: The ArangoDB Collection Mapping algorithm is a Work in Progress,
+ and will most likely be subject to change for the time being.
+
+ In contrast to RPT, regardless of whether `contextualize_graph` is set to
+ True or not, all RDF Predicates within every RDF Statement in **rdf_graph**
+ will be processed as their own ArangoDB Document, and will be stored under
+ the "Property" Document Collection.
+
+ ===============================================================================
+ To demo the ArangoDB Collection Mapping process,
+ let us consider the following RDF Graph
+ --------------------------------------------------------------------
+ @prefix ex: .
+ @prefix adb: .
+ @prefix rdfs: .
+
+ ex:B rdfs:subClassOf ex:A .
+ ex:C rdfs:subClassOf ex:A .
+ ex:D rdfs:subClassOf ex:C .
+
+ ex:alex rdf:type ex:A .
+
+ ex:sam ex:age 25 .
+ ex:age rdfs:domain ex:A
+
+ ex:john rdf:type ex:B .
+ ex:john rdf:type ex:D .
+
+ ex:mike rdf:type ex:G
+ ex:mike rdf:type ex:F
+ ex:mike rdf:type ex:E
+
+ ex:frank adb:collection "Z" .
+ ex:frank rdf:type D .
+
+ ex:bob ex:name "Bob" .
+ --------------------------------------------------------------------
+ Given the RDF TTL Snippet above, we can derive the following
+ ArangoDB Collection mappings:
+
+ ex:alex --> "A"
+ - This RDF Resource only has one associated `rdf:type` statement.
+
+ ex:sam --> "A"
+ - Although this RDF Resource has no `rdf:type` associated statement,
+ we can infer from the domain of the property it uses (ex:age) that
+ it is of type ex:A.
+
+ ex:john --> "D"
+ - This RDF Resource has 2 `rdf:type` statements, but `ex:D` is "deeper"
+ than `ex:B` when considering the `rdfs:subClassOf` Taxonomy.
+
+ ex:mike --> "E"
+ - This RDF Resource has multiple `rdf:type` statements, with
+ none belonging to the `rdfs:subClassOf` Taxonomy.
+ Therefore, Alphabetical Sorting is used.
+
+ ex:frank --> "Z"
+ - This RDF Resource has an `adb:collection` statement associated
+ to it, which is prioritized over any other `rdf:type`
+ statement it may have.
+
+ ex:bob --> "UnknownResource"
+ - This RDF Resource has neither an `rdf:type` statement
+ nor an `adb:collection` statement associated to it. It
+ is therefore placed under the "UnknownResource"
+ Document Collection.
+ ===============================================================================
+
+ :param name: The name of the RDF Graph
+ :type name: str
+ :param rdf_graph: The RDF Graph object. NOTE: This method does not
+ currently support RDF graphs of type `rdflib.graph.Dataset`.
+ Apologies for the inconvenience.
+ :type: rdf_graph: rdflib.graph.Graph
+ :param contextualize_graph: A work-in-progress flag that seeks
+ to enhance the Terminology Box of **rdf_graph** by providing
+ the following features:
+ 1) Cast a (predicate RDF.type RDF.Property) edge
+ relationship into the ArangoDB graph for every RDF predicate
+ used in the form (subject predicate object) within **rdf_graph**.
+ 2) Provide RDFS.Domain & RDFS.Range Inference on all
+ RDF Resources within the **rdf_graph**, so long that no
+ RDF.Type statement already exists in **rdf_graph**
+ for the given resource.
+ 3) Provide RDFS.Domain & RDFS.Range Introspection on all
+ RDF Predicates with the **rdf_graph**, so long that
+ no RDFS.Domain or RDFS.Range statement already exists
+ for the given predicate.
+ 4) TODO - What's next?
+ :type contextualize_graph: bool
+ :param overwrite_graph: Overwrites the ArangoDB graph identified
+ by **name** if it already exists, and drops its associated collections.
+ Defaults to False.
+ :type overwrite_graph: bool
+ :param batch_size: If specified, runs the ArangoDB Data Ingestion
+ process for every **batch_size** RDF triples/quads within **rdf_graph**.
+ Defaults to `len(rdf_graph)`.
+ :param use_async: Performs asynchronous ArangoDB ingestion if enabled.
+ Defaults to True.
+ :type use_async: bool
+ :type batch_size: int | None
+ :param adb_mapping: An (optional) RDF Graph containing the ArangoDB
+ Collection Mapping statements of all identifiable Resources.
+ See `ArangoRDF.build_adb_mapping_for_pgt()` for more info.
+ :type adb_mapping: rdflib.graph.Graph | None
+ :param simplify_reified_triples: If set to False, will preserve the RDF
+ Structure of any reified triples. If set to True, will convert any reified
+ triples into regular ArangoDB edges. Defaults to True.
+ :type simplify_reified_triples: bool
+ :param import_options: Keyword arguments to specify additional
+ parameters for the ArangoDB Data Ingestion process.
+ The full parameter list is here:
+ https://docs.python-arango.com/en/main/specs.html#arango.collection.Collection.import_bulk
+ :return: The ArangoDB Graph API wrapper.
+ :rtype: arango.graph.Graph
"""
- Creates edge collection if it doesn't already exist. Appends to and from vertex collections if collection already exists.
-
- Parameters
- ----------
- name: str
- the name of the edge collection that will be created
- parent_collections: List[str]
- a list of collections that will be added to from_vertex_collections in the edge definition
- child_collections: List[str]
- a list of collections that will be added to to_vertex_collections in the edge definition
- default_name: str
- the name that will be used to reference the collection in the code
- """
-
- if self.graph.has_edge_collection(name):
- # check edge definition
- edge_defs = self.graph.edge_definitions()
- current_def = None
- for ed in edge_defs:
- if ed["edge_collection"] == name:
- current_def = ed
- break
-
- # check if existing definition includes the intedned collections
- new_from_vc = current_def["from_vertex_collections"]
- for col in parent_collections:
- if col not in current_def["from_vertex_collections"]:
- new_from_vc.append(col)
-
- new_to_vc = current_def["to_vertex_collections"]
- for col in child_collections:
- if col not in current_def["to_vertex_collections"]:
- new_to_vc.append(col)
-
- # replace def
- self.graph.replace_edge_definition(
- edge_collection=name,
- from_vertex_collections=new_from_vc,
- to_vertex_collections=new_to_vc,
+ if isinstance(rdf_graph, RDFDataset):
+ raise TypeError( # pragma: no cover
+ """
+ Invalid type for **rdf_graph**: ArangoRDF does not yet
+ support RDF Graphs of type rdflib.graph.Dataset
+ """
)
+ self.rdf_graph = rdf_graph
+
+ # Reset the ArangoDB Config
+ self.adb_docs = defaultdict(lambda: defaultdict(dict))
+ self.__simplify_reified_triples = simplify_reified_triples
+ self.__import_options = import_options
+ self.__import_options["on_duplicate"] = "update"
+
+ # A unique set of instance variables to
+ # convert RDF Lists into JSON Lists during the PGT Process
+ self.__rdf_list_heads: RDFListHeads = defaultdict(lambda: defaultdict(dict))
+ self.__rdf_list_data: RDFListData = defaultdict(lambda: defaultdict(dict))
+
+ # A set of ArangoDB Collections that will NOT imported via
+ # batch processing, as they contain documents whose properties
+ # are subject to change. For example, an RDF Resource may have
+ # multiple Literal statements associated to it.
+ self.__adb_col_blacklist: Set[str] = set()
+
+ # The ArangoDB Collection name of all unidentified RDF Resources
+ self.__UNKNOWN_RESOURCE = f"{name}_UnknownResource"
+
+ # Builds the ArangoDB Edge Definitions of the (soon to be) ArangoDB Graph
+ self.__e_col_map = defaultdict(lambda: defaultdict(set))
+
+ # NOTE: Graph Contextualization is an experimental work-in-progress
+ if not contextualize_graph:
+ self.adb_mapping = adb_mapping or RDFGraph()
+ self.build_adb_mapping_for_pgt(self.rdf_graph, self.adb_mapping)
else:
- self.graph.create_edge_definition(
- edge_collection=name,
- from_vertex_collections=parent_collections,
- to_vertex_collections=child_collections,
+ self.adb_mapping = adb_mapping or RDFGraph()
+ self.rdf_graph = self.load_base_ontology(rdf_graph)
+ explicit_type_map = self.__build_explicit_type_map(
+ self.__add_to_adb_mapping
+ )
+ subclass_tree = self.__build_subclass_tree(self.__add_to_adb_mapping)
+ predicate_scope = self.__build_predicate_scope(self.__add_to_adb_mapping)
+ domain_range_map = self.__build_domain_range_map(predicate_scope)
+ type_map = self.__combine_type_map_and_dr_map(
+ explicit_type_map, domain_range_map
+ )
+
+ self.build_adb_mapping_for_pgt(
+ self.rdf_graph,
+ self.adb_mapping,
+ explicit_type_map,
+ subclass_tree,
+ predicate_scope,
+ domain_range_map,
+ )
+
+ self.__e_col_map["type"]["from"].add("Property")
+ self.__e_col_map["type"]["from"].add("Class")
+ self.__e_col_map["type"]["to"].add("Class")
+ for label in ["domain", "range"]:
+ self.__e_col_map[label]["from"].add("Property")
+ self.__e_col_map[label]["to"].add("Class")
+
+ if overwrite_graph:
+ self.db.delete_graph(name, ignore_missing=True, drop_collections=True)
+
+ size = len(self.rdf_graph)
+ if batch_size is None:
+ batch_size = size
+
+ s: RDFTerm # Subject
+ p: URIRef # Predicate
+ o: RDFTerm # Object
+ sg: Optional[RDFGraph] # Sub Graph
+
+ reified_triple_blacklist = set()
+ if simplify_reified_triples:
+ reified_triple_blacklist.update(
+ {
+ RDF.subject,
+ RDF.predicate,
+ RDF.object,
+ }
)
- self.col_map[default_name] = name
+ rdf_statement_blacklist = {
+ (RDF.type, RDF.List),
+ (RDF.type, RDF.Bag),
+ (RDF.type, RDF.Seq),
+ }
+
+ statements = (
+ self.rdf_graph.quads
+ if isinstance(self.rdf_graph, RDFConjunctiveGraph)
+ else self.rdf_graph.triples
+ )
- def import_rdf(
+ ##################
+ # PGT Processing #
+ ##################
+ self.__set_iterators("RDF → ADB (PGT)", "#08479E", " ")
+ with Live(Group(self.__rdf_iterator, self.__adb_iterator)):
+ self.__rdf_task = self.__rdf_iterator.add_task("", total=size)
+
+ t = (None, None, None)
+ for count, (s, p, o, *rest) in enumerate(statements(t), 1):
+ self.__rdf_iterator.update(self.__rdf_task, advance=1)
+
+ if p in reified_triple_blacklist or (p, o) in rdf_statement_blacklist:
+ continue
+
+ reified_triple_key = None
+ if simplify_reified_triples and (p, o) == (RDF.type, RDF.Statement):
+ s, p, o, reified_triple_key = self.__parse_reified_triple(s)
+
+ # Address the possibility of (s, p, o) being a part of the
+ # structure of an RDF Collection or an RDF Container.
+ rdf_list_col = self.__pgt_statement_is_part_of_rdf_list(s, p)
+ if rdf_list_col:
+ key = self.rdf_id_to_adb_label(str(p))
+ doc = self.__rdf_list_data[rdf_list_col][s]
+ self.__pgt_rdf_val_to_adb_val(doc, key, o)
+ continue
+
+ # Process RDF Subject
+ s_meta = self.__pgt_get_term_metadata(s)
+ self.__pgt_process_rdf_term(s_meta)
+
+ if p in {self.adb_col_uri, self.adb_key_uri}:
+ continue
+
+ # Get the Sub Graph URI (if it exists)
+ sg = rest[0] if rest else None
+ sg_str = str(sg.identifier) if sg else ""
+
+ # Process RDF Predicate
+ p_meta = self.__pgt_get_term_metadata(p)
+ self.__pgt_process_rdf_term(p_meta)
+
+ # Process RDF Object
+ o_meta = self.__pgt_get_term_metadata(o)
+ self.__pgt_process_object(s_meta, p_meta, o_meta, sg_str)
+
+ # Load the RDF triple/quad as an ArangoDB Edge
+ self.__pgt_process_statement(
+ s_meta, p_meta, o_meta, sg_str, reified_triple_key
+ )
+
+ # NOTE: Graph Contextualization is an experimental work-in-progress
+ if contextualize_graph:
+ _, _, p_key, _ = p_meta
+
+ # Create the ArangoDB Edge
+ # p_has_no_type_statement = len(type_map[p]) == 0
+ # TODO: REVISIT - Should this even be here?
+ p_has_no_type_statement = (p, RDF.type, None) not in self.rdf_graph
+ if p_has_no_type_statement:
+ key = f"{p_key}-{self.__rdf_type_key}-{self.__rdf_property_key}"
+ self.__add_adb_edge(
+ "type",
+ str(FP64(key)),
+ f"Property/{p_key}",
+ f"Class/{self.__rdf_property_key}",
+ self.__rdf_type_str,
+ "type",
+ sg_str,
+ )
+
+ # Run RDFS Domain/Range Inference & Introspection
+ dr_meta = [(*s_meta, "domain"), (*o_meta, "range")]
+ self.__infer_and_introspect_dr(
+ p,
+ p_key,
+ dr_meta,
+ type_map,
+ predicate_scope,
+ sg_str,
+ is_rpt=False,
+ )
+
+ # Empty 'self.adb_docs' into ArangoDB once 'batch_size' has been reached
+ if count % batch_size == 0:
+ self.__insert_adb_docs(use_async, self.__adb_col_blacklist)
+
+ # Insert the remaining `self.adb_docs` into ArangoDB
+ self.__insert_adb_docs(use_async)
+
+ gc.collect()
+
+ ###################
+ # Post Processing #
+ ###################
+ self.__set_iterators("RDF → ADB (PGT Post-Process)", "#EF7D00", " ")
+ with Live(Group(self.__rdf_iterator, self.__adb_iterator)):
+ # Process `self.__rdf_list_heads` & `self.__rdf_list_data`
+ # into `self.adb_docs`
+ self.__pgt_process_rdf_lists()
+ self.__insert_adb_docs(use_async)
+
+ gc.collect()
+
+ assert len(self.adb_docs) == 0
+ return self.__pgt_create_adb_graph(name)
+
+ def build_adb_mapping_for_pgt(
self,
- data: str,
- format: str = "xml",
- config: dict = {},
- save_config: bool = False,
- **import_options: Any,
- ) -> ArangoGraph:
+ rdf_graph: RDFGraph,
+ adb_mapping: Optional[RDFGraph] = None,
+ explicit_type_map: Optional[TypeMap] = None,
+ subclass_tree: Optional[Tree] = None,
+ predicate_scope: Optional[PredicateScope] = None,
+ domain_range_map: Optional[TypeMap] = None,
+ ) -> RDFGraph:
+ """The PGT Algorithm relies on the ArangoDB Collection Mapping Process to
+ identify the ArangoDB Collection of every RDF Resource. Using this method prior
+ to running `ArangoRDF.rdf_to_arangodb_by_pgt()` allows users to see the
+ (RDF Resource)-to-(ArangoDB Collection) mapping of all of their (identifiable)
+ RDF Resources. See the `ArangoRDF.rdf_to_arangodb_by_pgt()` docstring
+ for an explanation on the ArangoDB Collection Mapping Process.
+
+ Should a user be interested in making changes to this mapping,
+ they are free to do so by modifying the returned RDF Graph.
+
+ Users can then pass the (modified) ADB Mapping back into the
+ `ArangoRDF.rdf_to_arangodb_by_pgt()` method to make sure the RDF Resources
+ of the RDF Graph are placed in the desired ArangoDB Collections.
+
+ A common use case would look like this:
+ ```
+ from arango_rdf import ArangoRDF
+ from arango import ArangoClient
+ from rdflib import Graph
+
+ db = ArangoClient(...)
+ adbrdf = ArangoRDF(db)
+
+ g = Graph()
+ g.parse('...')
+
+ adb_mapping = adbrdf.build_adb_mapping_for_pgt(g)
+ adb_mapping.remove(...)
+ adb_mapping.add(...)
+
+ adbrdf.rdf_to_arangodb_by_pgt(
+ 'PGTGraph', g, contextualize_graph=True, adb_mapping=adb_mapping
+ )
+ ```
+
+ NOTE: Running this method prior to `ArangoRDF.rdf_to_arangodb_by_pgt()`
+ is unnecessary if the user is not interested in
+ viewing/modifying the ADB Mapping.
+
+ For example, the `adb_mapping` may look like this:
+ -----------------------------------------
+ @prefix adb: .
+
+ adb:collection "Person" .
+ adb:collection "Person" .
+ adb:collection "Property" .
+ adb:collection "Class" .
+ adb:collection "Dog" .
+ -----------------------------------------
+
+ NOTE: There can only be 1 `adb:collection` statement
+ associated to each RDF Resource.
+
+ :param rdf_graph: The RDF Graph object.
+ :type rdf_graph: rdflib.graph.Graph
+ :param adb_mapping: An existing adb_mapping should a user not want to
+ see any previous `adb:collection` statements being overwritten by
+ the standard ArangoDB Collection Mapping Process.
+ :type adb_mapping: rdflib.graph.Graph
+ :param explicit_type_map: A dictionary mapping the "natural"
+ `RDF.Type` statements of every RDF Resource.
+ See `ArangoRDF.__build_explicit_type_map()` for more info.
+ NOTE: Users should not use this parameter (internal use only).
+ :type explicit_type_map: arango_rdf.typings.TypeMap
+ :param subclass_tree: The RDFS SubClassOf Taxonomy represented
+ as a Tree Data Structure. See
+ `ArangoRDF.__build_subclass_tree()` for more info.
+ NOTE: Users should not use this parameter (internal use only).
+ :type subclass_tree: arango_rdf.utils.Tree
+ :param predicate_scope: A dictionary mapping the Domain & Range values
+ of RDF Predicates. See `ArangoRDF.__build_predicate_scope()` for more info.
+ NOTE: Users should not use this parameter (internal use only).
+ :type predicate_scope: arango_rdf.typings.PredicateScope
+ :param domain_range_map: The Domain and Range Map produced by the
+ `ArangoRDF.__build_domain_range_map()` method.
+ NOTE: Users should not use this parameter (internal use only).
+ :type domain_range_map: arango_rdf.typings.TypeMap
+ :return: An RDF Graph containing the ArangoDB Collection Mapping
+ statements of all identifiable Resources. See the
+ `ArangoRDF.rdf_to_arangodb_by_pgt()` docstring for an explanation
+ on the ArangoDB Collection Mapping Process.
+ :rtype: rdflib.graph.Graph
"""
- Imports an rdf graph from a file into Arangodb
+ self.rdf_graph = rdf_graph
+ self.controller.rdf_graph = rdf_graph
+ self.adb_mapping = adb_mapping or RDFGraph()
+
+ self.adb_mapping.bind("adb", self.__adb_ns)
+
+ ############################################################
+ # 1) RDF.type statements
+ ############################################################
+ if explicit_type_map is None:
+ explicit_type_map = self.__build_explicit_type_map(
+ self.__add_to_adb_mapping
+ )
- Parameters
- ----------
- data: str
- path to rdf file
- format: str
- format of the rdf file (default is "xml")
- config: dict
- configuration options, which currently include:
- normalize_literals: bool
- normalize the RDF literals. Defaults to False
- save_config: bool
- save the specified configuration into the ArangoDB 'configurations' collection
+ ############################################################
+ # 2) RDF.subClassOf Statements
+ ############################################################
+ if subclass_tree is None:
+ subclass_tree = self.__build_subclass_tree(self.__add_to_adb_mapping)
+
+ ############################################################
+ # 3) Domain & Range Statements
+ ############################################################
+ if predicate_scope is None:
+ predicate_scope = self.__build_predicate_scope(self.__add_to_adb_mapping)
+
+ if domain_range_map is None:
+ domain_range_map = self.__build_domain_range_map(predicate_scope)
+
+ ############################################################
+ # 4) ADB.Collection Statements
+ ############################################################
+ for s, o, *_ in self.rdf_graph[: self.adb_col_uri :]:
+ if type(o) is not Literal:
+ raise ValueError(f"Object {o} must be Literal") # pragma: no cover
+
+ has_mapping = (s, None, None) in self.adb_mapping
+ new_mapping = (s, None, o) not in self.adb_mapping
+ if has_mapping and new_mapping:
+ # TODO: Create custom error
+ raise ValueError( # pragma: no cover
+ f"""
+ Subject '{s}' can only have 1 ArangoDB Collection association.
+ Found '{self.adb_mapping.value(s, self.adb_col_uri)}'
+ and '{str(o)}'.
+ """
+ )
+
+ self.__add_to_adb_mapping(s, str(o))
+
+ ############################################################
+ # 5) Finalize **adb_mapping**
+ ############################################################
+ for rdf_map in [explicit_type_map, domain_range_map]:
+ for rdf_resource, class_set in rdf_map.items():
+ has_mapping = (rdf_resource, None, None) in self.adb_mapping
+ if has_mapping or len(class_set) == 0:
+ continue # pragma: no cover # (false negative)
+
+ adb_col = self.rdf_id_to_adb_label(
+ self.controller.identify_best_class(
+ rdf_resource, class_set, subclass_tree
+ )
+ )
+
+ self.__add_to_adb_mapping(rdf_resource, adb_col)
+
+ return self.adb_mapping
+
+ def __pgt_get_term_metadata(
+ self, term: Union[URIRef, BNode, Literal]
+ ) -> RDFTermMeta:
+ """Return the following PGT-relevant metadata associated to the RDF Term:
+ 1. The RDF Term (**term**)
+ 2. The Arangodb Collection of **term**
+ 3. The Arangodb Key of **term**
+ 4. The ArangoDB "label" value of **term** (i.e its localname)
+
+ :param term: The RDF Term
+ :type term: URIRef | BNode | Literal
+ :return: The RDF Term object, along with its associated ArangoDB
+ Collection name, Document Key, and Document label.
+ :rtype: Tuple[URIRef | BNode | Literal, str, str, str]
"""
+ if type(term) is Literal:
+ return term, "", "", "" # No other metadata needed
- self.rdf_graph.parse(data, format=format)
+ t_str = str(term)
+ t_col = ""
+ t_key = self.rdf_id_to_adb_key(t_str, term)
+ t_label = self.rdf_id_to_adb_label(t_str)
- graph_id = self.rdf_graph.identifier.toPython()
+ if (
+ self.__simplify_reified_triples
+ and (term, RDF.type, RDF.Statement) in self.rdf_graph
+ ):
+ p = self.rdf_graph.value(term, RDF.predicate)
+ t_col = t_label = self.rdf_id_to_adb_label(str(p))
- normalize_literals = config.get("normalize_literals", False)
- if config and save_config:
- config["normalize_literals"] = normalize_literals
- config["default_graph"] = self.default_graph
- if self.__set_sub_graph:
- config["sub_graph"] = self.sub_graph
+ self.__adb_col_blacklist.add(t_col) # TODO: Revisit
- self.save_config(config)
+ else:
+ t_col = str(
+ self.adb_mapping.value(term, self.adb_col_uri)
+ or self.__UNKNOWN_RESOURCE
+ )
- adb_documents = defaultdict(list)
+ return term, t_col, t_key, t_label
- file_name = data.split("/")[-1]
- for s, p, o in tqdm(self.rdf_graph, desc=file_name, colour="#88a049"):
+ def __pgt_rdf_val_to_adb_val(
+ self, doc: Json, key: str, val: Any, process_val_as_string: bool = False
+ ) -> None:
+ """A helper function used to insert an arbitrary value
+ into an arbitrary document.
+
+ :param doc: An arbitrary document
+ :type doc: Dict[str, Any]
+ :param key: An arbitrary document property key.
+ :type key: str
+ :param val: The value associated to the document property **key**.
+ :type val: Any
+ :param process_val_as_string: If enabled, **val** is appended to
+ a string representation of the current value of the document
+ property. Defaults to False.
+ :type process_val_as_string: bool
+ """
- # build subject doc
- if isinstance(s, URIRef):
- s_collection = "iri"
- s_doc = self.build_iri_doc(s)
- elif isinstance(s, BNode):
- s_collection = "bnode"
- s_doc = self.build_bnode_doc(s)
- else:
- raise ValueError("Subject must be IRI or Blank Node")
-
- s_id = self.col_map[s_collection] + "/" + s_doc["_key"]
- adb_documents[s_collection].append(s_doc)
-
- # build object doc
- if isinstance(o, URIRef):
- o_collection = "iri"
- o_doc = self.build_iri_doc(o)
- elif isinstance(o, BNode):
- o_collection = "bnode"
- o_doc = self.build_bnode_doc(o)
- elif isinstance(o, Literal):
- o_collection = "literal"
- o_doc = self.build_literal_doc(o, normalize_literals)
- else:
- raise ValueError("Object must be IRI, Blank Node, or Literal")
+ # This flag is only active in ArangoRDF.__pgt_process_rdf_lists()
+ if process_val_as_string:
+ doc[key] += f"'{val}'," if type(val) is str else f"{val},"
+ return
- o_id = self.col_map[o_collection] + "/" + o_doc["_key"]
- adb_documents[o_collection].append(o_doc)
+ prev_val = doc.get(key)
- # build and insert edge
- edge = self.build_statement_edge(p, s_id, o_id, graph_id)
+ if prev_val is None:
+ doc[key] = val
+ elif isinstance(prev_val, list):
+ prev_val.append(val)
+ else:
+ doc[key] = [prev_val, val]
- # add RDF Graph id as edge property
- if self.__set_sub_graph:
- edge["_graph"] = graph_id
+ def __pgt_process_rdf_term(
+ self,
+ t_meta: RDFTermMeta,
+ s_col: str = "",
+ s_key: str = "",
+ p_label: str = "",
+ process_val_as_string: bool = False,
+ ) -> None:
+ """Process an RDF Term as an ArangoDB document by PGT.
+
+ :param t_meta: The RDF Term Metadata associated to the RDF Term.
+ :type t_meta: arango_rdf.typings.RDFTermMeta
+ :param s_col: The ArangoDB document collection of the Subject associated
+ to the RDF Term **t**. Only required if the RDF Term is of type Literal.
+ :type s_col: str
+ :param s_key: The ArangoDB document key of the Subject associated
+ to the RDF Term **t**. Only required if the RDF Term is of type Literal.
+ :type s_key: str
+ :param p_label: The RDF Predicate Label key of the Predicate associated
+ to the RDF Term **t**. Only required if the RDF Term is of type Literal.
+ :type p_label: str
+ :param process_val_as_string: If enabled, the value of **t** is appended to
+ a string representation of the current value of the document
+ property. Only considered if **t** is a Literal. Defaults to False.
+ :type process_val_as_string: bool
+ """
- adb_documents["statement"].append(edge)
+ t, t_col, t_key, t_label = t_meta
- # Set default ArangoDB `import_bulk` behavior to update/insert
- if "on_duplicate" not in import_options:
- import_options["on_duplicate"] = "update"
+ if t_key in self.adb_docs.get(t_col, {}):
+ return
- for collection, doc_list in tqdm(
- adb_documents.items(), colour="#5e3108", desc="/_api/import"
- ):
- self.db.collection(self.col_map[collection]).import_bulk(
- doc_list, **import_options
- )
+ if type(t) is URIRef:
+ self.adb_docs[t_col][t_key] = {
+ "_key": t_key,
+ "_uri": str(t),
+ "_label": t_label,
+ "_rdftype": "URIRef",
+ }
+
+ elif type(t) is BNode:
+ self.adb_docs[t_col][t_key] = {
+ "_key": t_key,
+ "_label": "",
+ "_rdftype": "BNode",
+ }
+
+ elif type(t) is Literal and all([s_col, s_key, p_label]):
+ doc = self.adb_docs[s_col][s_key]
+ t_value = self.__get_literal_val(t, str(t))
+ self.__pgt_rdf_val_to_adb_val(doc, p_label, t_value, process_val_as_string)
- return self.graph
+ self.__adb_col_blacklist.add(s_col) # TODO: REVISIT
+
+ else:
+ raise ValueError() # pragma: no cover
- def export_rdf(
+ def __pgt_process_object(
self,
- file_name: Optional[str] = None,
- format: Optional[str] = None,
- **query_options: Any,
- ) -> RDFGraph:
+ s_meta: RDFTermMeta,
+ p_meta: RDFTermMeta,
+ o_meta: RDFTermMeta,
+ sg_str: str,
+ ) -> None:
+ """Processes the RDF Object into ArangoDB. Given the possibily of
+ the RDF Object being used as the "root" of an RDF Collection or
+ an RDF Container (i.e an RDF List), this wrapper function is used
+ to prevent calling `__pgt_process_rdf_term` if it is not required.
+
+ :param s_meta: The RDF Term Metadata associated to the
+ RDF Subject of the statement containing the RDF Object.
+ :type s_meta: arango_rdf.typings.RDFTermMeta
+ :param p_meta: The RDF Term Metadata associated to the
+ RDF Predicate of the statement containing the RDF Object.
+ :type p_meta: arango_rdf.typings.RDFTermMeta
+ :param o_meta: The RDF Term Metadata associated to the RDF Object.
+ :type o_meta: arango_rdf.typings.RDFTermMeta
+ :param sg_str: The string representation of the sub-graph URIRef associated
+ to this statement (if any).
+ :type sg_str: str
"""
- Builds a rdf graph from the database graph and exports to a file
- Parameters
- ----------
- file_name: str | none
- path to where file will be exported
- format: str | none
- format of the rdf file
- """
- # init rdf graph
- g = RDFGraph()
+ s, s_col, s_key, _ = s_meta
+ p, _, _, p_label = p_meta
+ o, _, _, _ = o_meta
- aql = """
- FOR edge IN @@col
- RETURN {
- iri: edge["_iri"],
- from: DOCUMENT(edge._from),
- to: DOCUMENT(edge._to)
- }
+ if self.__pgt_object_is_head_of_rdf_list(o):
+ head = {"root": o, "sub_graph": sg_str}
+ self.__rdf_list_heads[s][p] = head
+
+ else:
+ self.__pgt_process_rdf_term(o_meta, s_col, s_key, p_label)
+
+ def __pgt_process_statement(
+ self,
+ s_meta: RDFTermMeta,
+ p_meta: RDFTermMeta,
+ o_meta: RDFTermMeta,
+ sg_str: str,
+ reified_triple_key: Optional[str] = None,
+ ) -> None:
+ """Processes the RDF Statement (s, p, o) as an ArangoDB Edge for PGT.
+
+ An edge is only created if:
+ 1) The RDF Object within the RDF Statement is not a Literal
+ 2) The RDF Object is not the "root" node of an RDF List structure
+
+ :param s_meta: The RDF Term Metadata associated to the
+ RDF Subject of the statement containing the RDF Object.
+ :type s_meta: arango_rdf.typings.RDFTermMeta
+ :param p_meta: The RDF Term Metadata associated to the
+ RDF Predicate of the statement containing the RDF Object.
+ :type p_meta: arango_rdf.typings.RDFTermMeta
+ :param o_meta: The RDF Term Metadata associated to the RDF Object.
+ :type o_meta: arango_rdf.typings.RDFTermMeta
+ :param sg_str: The string representation of the sub-graph URIRef associated
+ to this statement (if any).
+ :type sg_str: str
"""
+ o, o_col, o_key, _ = o_meta
- data_cursor = self.db.aql.execute(
- aql,
- bind_vars={"@col": self.col_map["statement"]},
- count=True,
- **query_options,
+ if type(o) is Literal or self.__pgt_object_is_head_of_rdf_list(o):
+ return
+
+ _, s_col, s_key, _ = s_meta
+ p, _, p_key, p_label = p_meta
+
+ e_key = reified_triple_key or str(FP64(f"{s_key}-{p_key}-{o_key}"))
+
+ self.__add_adb_edge(
+ p_label, # local name of predicate URI is used as the collection name
+ e_key,
+ f"{s_col}/{s_key}",
+ f"{o_col}/{o_key}",
+ str(p),
+ p_label,
+ sg_str,
)
- for data in tqdm(data_cursor, total=data_cursor.count(), colour="CYAN"):
- from_node = self.adb_doc_to_rdf_node(data["from"])
- to_node = self.adb_doc_to_rdf_node(data["to"])
- _iri = data["iri"]
+ self.__e_col_map[p_label]["from"].add(s_col)
+ self.__e_col_map[p_label]["to"].add(o_col)
- # add triple to graph
- g.add((from_node, URIRef(_iri), to_node))
+ if reified_triple_key:
+ self.__adb_col_blacklist.add(p_label)
- # output graph
- if file_name:
- g.serialize(destination=file_name, format=format)
+ def __pgt_object_is_head_of_rdf_list(self, o: RDFTerm) -> bool:
+ """Return True if the RDF Object *o* is either the "root" node
+ of some RDF Collection or RDF Container within the RDF Graph.
+ Essential for unpacking the complicated data structure of
+ RDF Lists and re-building them as a JSON List for ArangoDB insertion.
- return g
+ :param o: The RDF Object.
+ :type o: URIRef | BNode | Literal
+ :return: Whether the object points to an RDF List or not.
+ :rtype: bool
+ """
+ # TODO: Discuss repurcussions of this assumption
+ if type(o) is not BNode:
+ return False
+
+ first = (o, RDF.first, None)
+ rest = (o, RDF.rest, None)
+
+ if first in self.rdf_graph or rest in self.rdf_graph:
+ return True
+
+ _n = (o, URIRef(f"{RDF}_1"), None)
+ li = (o, URIRef(f"{RDF}li"), None)
+
+ if _n in self.rdf_graph or li in self.rdf_graph:
+ return True
+
+ return False
+
+ def __pgt_statement_is_part_of_rdf_list(self, s: RDFTerm, p: URIRef) -> str:
+ """Return the associated "Document Buffer" key if the RDF Statement
+ (s, p, _) is part of an RDF Collection or RDF Container within the RDF Graph.
+ Essential for unpacking the complicated data structure of
+ RDF Lists and re-building them as an ArangoDB Document Property.
+
+ :param s: The RDF Subject.
+ :type s: URIRef | BNode
+ :param p: The RDF Predicate.
+ :type p: URIRef
+ :return: The **self.adb_docs** "Document Buffer" key associated
+ to the RDF Statement. If the statement is not part of an RDF
+ List, return an empty string.
+ :rtype: str
+ """
+ # TODO: Discuss repurcussions of this assumption
+ if type(s) is not BNode:
+ return ""
- def build_iri_doc(self, iri: URIRef) -> dict:
- return {
- "_key": hashlib.md5(str(iri).encode("utf-8")).hexdigest(),
- "_iri": iri.toPython(),
- }
+ if p in {RDF.first, RDF.rest}:
+ return "_COLLECTION_BNODE"
+
+ p_str = str(p)
+ _n = r"^http://www.w3.org/1999/02/22-rdf-syntax-ns#_[0-9]{1,}$"
+ li = r"^http://www.w3.org/1999/02/22-rdf-syntax-ns#li$"
+
+ if re.match(_n, p_str) or re.match(li, p_str):
+ return "_CONTAINER_BNODE"
+
+ return ""
+
+ def __pgt_process_rdf_lists(self) -> None:
+ """A helper function to help process all RDF Collections & Containers
+ within the RDF Graph prior to inserting the documents into ArangoDB.
+
+ # TODO: Rework the following paragraph to address `_rdf_list_head` and
+ `_rdf_list_data` usage instead
+ This function relies on a Dictionary/Linked-List representation of the
+ RDF Lists. This representation is stored via the "_LIST_HEAD",
+ "_CONTAINER_BNODE", and "_COLLECTION_BNODE" keys within `self.adb_docs`.
+
+ Given the "linked-list" nature of these RDF Lists, we rely on
+ recursion via the `__pgt_process_rdf_list_object`,
+ `__pgt_unpack_rdf_collection`, and `__pgt_unpack_rdf_container` functions.
+
+ NOTE: A form of string manipulation is used if Literals are
+ present within the RDF List. For example, given the RDF Statement
+ ```ex:Doc ex:numbers (1 (2 3)) .```, the equivalent ArangoDB List is
+ constructed via a string-based solution:
+ "[" → "[1" → "[1, [" → "[1, [2," → "[1, [2, 3" → "[1, [2, 3]" → "[1, [2, 3]]"
+ """
+ list_heads = self.__rdf_list_heads.items()
- def build_bnode_doc(self, bnode: BNode) -> dict:
- return {"_key": bnode.toPython()}
+ self.__rdf_task = self.__rdf_iterator.add_task("", total=len(list_heads))
+ for s, s_dict in list_heads:
+ self.__rdf_iterator.update(self.__rdf_task, advance=1)
- def build_literal_doc(self, literal: Literal, normalize: bool) -> dict:
+ s_meta = self.__pgt_get_term_metadata(s)
+ _, s_col, s_key, _ = s_meta
+
+ doc = self.adb_docs[s_col][s_key]
+ doc["_key"] = s_key
+
+ for p, p_dict in s_dict.items():
+ p_meta = self.__pgt_get_term_metadata(p)
+ p_label = p_meta[-1]
+
+ root: RDFTerm = p_dict["root"]
+ sg: str = p_dict["sub_graph"]
+
+ doc[p_label] = ""
+ self.__pgt_process_rdf_list_object(doc, s_meta, p_meta, root, sg)
+ doc[p_label] = doc[p_label].rstrip(",")
+
+ # Delete doc[p_key] if there are no Literals within the RDF List
+ # TODO: Revisit the possibility of empty collections or containers...
+ if set(doc[p_label]) == {"[", "]"}:
+ del doc[p_label]
+ else:
+ doc[p_label] = literal_eval(doc[p_label])
+
+ def __pgt_process_rdf_list_object(
+ self,
+ doc: Json,
+ s_meta: RDFTermMeta,
+ p_meta: RDFTermMeta,
+ o: RDFTerm,
+ sg: str,
+ ) -> None:
+ """Given an ArangoDB Document, and the RDF List Statement represented
+ by `s_meta, p_meta, o`, process the value of the object **o**
+ into the ArangoDB Document.
+
+ If the Object is part of an RDF Collection Data Structure,
+ rely on the recursive `__pgt_unpack_rdf_collection` function.
+
+ If the Object is part of an RDF Container Data Structure,
+ rely on the recursive `__pgt_unpack_rdf_container` function.
+
+ If the Object is none of the above, then it is considered
+ as a processable entity.
+
+ :param doc: The ArangoDB Document associated to the RDF List.
+ :type doc: Dict[str, Any]
+ :param s_meta: The RDF Term Metadata associated to the RDF Subject.
+ :type s_meta: arango_rdf.typings.RDFTermMeta
+ :param p_meta: The RDF Term Metadata associated to the RDF Predicate.
+ :type p_meta: arango_rdf.typings.RDFTermMeta
+ :param o: The RDF List Object to process into ArangoDB.
+ :type o: URIRef | BNode | Literal
+ :param sg: The string representation of the sub-graph URIRef associated
+ to the RDF List Statement (if any).s
+ :type sg: str
+ """
+ p_label = p_meta[-1]
- lang = str(literal.language)
- type = str(literal.datatype)
- value = str(literal.value)
+ if o in self.__rdf_list_data["_COLLECTION_BNODE"]:
+ doc[p_label] += "["
- if type == "None":
- type = "http://www.w3.org/2001/XMLSchema#string"
+ next_bnode_dict = self.__rdf_list_data["_COLLECTION_BNODE"][o]
+ self.__pgt_unpack_rdf_collection(doc, s_meta, p_meta, next_bnode_dict, sg)
+
+ doc[p_label] = doc[p_label].rstrip(",") + "],"
+
+ elif o in self.__rdf_list_data["_CONTAINER_BNODE"]:
+ doc[p_label] += "["
+
+ next_bnode_dict = self.__rdf_list_data["_CONTAINER_BNODE"][o]
+ self.__pgt_unpack_rdf_container(doc, s_meta, p_meta, next_bnode_dict, sg)
+
+ doc[p_label] = doc[p_label].rstrip(",") + "],"
- # rdf strings are the only type allowed to not have a type. Coerce strings without type to xsd:String
- doc = {"_value": value, "_type": type, "_lang": lang}
- if normalize:
- key_string = value + type + lang
else:
- key_string = str(time.time())
+ _, s_col, s_key, _ = s_meta
+ o_meta = self.__pgt_get_term_metadata(o)
+
+ # Process the RDF Object as an ArangoDB Document
+ self.__pgt_process_rdf_term(o_meta, s_col, s_key, p_label, True)
+ # Process the RDF Statement as an ArangoDB Edge
+ self.__pgt_process_statement(s_meta, p_meta, o_meta, sg)
+
+ def __pgt_unpack_rdf_collection(
+ self,
+ doc: Json,
+ s_meta: RDFTermMeta,
+ p_meta: RDFTermMeta,
+ bnode_dict: Dict[str, RDFTerm],
+ sg: str,
+ ) -> None:
+ """A recursive function that disassembles the structure of the
+ RDF Collection, most notably known for its "first" & "rest" structure.
+
+ :param doc: The ArangoDB Document associated to the RDF Collection.
+ :type doc: Dict[str, Any]
+ :param s_meta: The RDF Term Metadata associated to the RDF Subject.
+ :type s_meta: arango_rdf.typings.RDFTermMeta
+ :param p_meta: The RDF Term Metadata associated to the RDF Predicate.
+ :type p_meta: arango_rdf.typings.RDFTermMeta
+ :param bnode_dict: A dictionary mapping the RDF.First and RDF.Rest
+ values associated to the current BNode of the RDF Collection.
+ :type bnode_dict: Dict[str, URIRef | BNode | Literal]
+ :param sg: The string representation of the sub-graph URIRef associated
+ to the RDF List Statement (if any).
+ :type sg: str
+ """
+
+ first: RDFTerm = bnode_dict["first"]
+ self.__pgt_process_rdf_list_object(doc, s_meta, p_meta, first, sg)
+
+ if "rest" in bnode_dict and bnode_dict["rest"] != RDF.nil:
+ rest = bnode_dict["rest"]
+
+ next_bnode_dict = self.__rdf_list_data["_COLLECTION_BNODE"][rest]
+ self.__pgt_unpack_rdf_collection(doc, s_meta, p_meta, next_bnode_dict, sg)
+
+ def __pgt_unpack_rdf_container(
+ self,
+ doc: Json,
+ s_meta: RDFTermMeta,
+ p_meta: RDFTermMeta,
+ bnode_dict: Dict[str, Union[RDFTerm, List[RDFTerm]]],
+ sg: str,
+ ) -> None:
+ """A recursive function that disassembles the structure of the
+ RDF Container, most notably known for its linear structure
+ (i.e rdf:li & rdf:_n properties)
+
+ :param doc: The ArangoDB Document associated to the RDF Collection.
+ :type doc: Dict[str, Any]
+ :param s_meta: The RDF Term Metadata associated to the RDF Subject.
+ :type s_meta: arango_rdf.typings.RDFTermMeta
+ :param p_meta: The RDF Term Metadata associated to the RDF Predicate.
+ :type p_meta: arango_rdf.typings.RDFTermMeta
+ :param bnode_dict: A dictionary mapping the values associated
+ associated to the current BNode of the RDF Container.
+ :type bnode_dict: Dict[str, URIRef | BNode | Literal]
+ :param sg: The string representation of the sub-graph URIRef associated
+ to the RDF List Statement (if any).
+ :type sg: str
+ """
+ # Sort based on the keys within bnode_dict
+ for data in sorted(bnode_dict.items()):
+ _, value = data # Fetch the value associated to the current key
+
+ # It is possible for the Container Membership Property
+ # to be re-used in multiple statements (e.g rdf:li),
+ # hence the reason why `value` can be a list or a single element.
+ value_as_list = value if isinstance(value, list) else [value]
+ for o in value_as_list:
+ self.__pgt_process_rdf_list_object(doc, s_meta, p_meta, o, sg)
+
+ def __pgt_create_adb_graph(self, name: str) -> ADBGraph:
+ """Create an ArangoDB graph based on a PGT Transformation.
+
+ :param name: The ArangoDB Graph name
+ :type name: str
+ :return: The ArangoDB Graph API wrapper.
+ :rtype: arango.graph.Graph
+ """
+ if self.db.has_graph(name): # pragma: no cover
+ return self.db.graph(name)
+
+ edge_definitions: List[Dict[str, Union[str, List[str]]]] = []
- doc["_key"] = hashlib.md5(key_string.encode("utf-8")).hexdigest()
+ all_v_cols: Set[str] = set()
+ non_orphan_v_cols: Set[str] = set()
- return doc
+ for col in self.adb_mapping.objects(None, self.adb_col_uri, True):
+ all_v_cols.add(str(col))
- def build_statement_edge(
- self, predicate: URIRef, subject_id: str, object_id: str, graph: str
- ) -> dict:
- _iri = predicate.toPython()
- _from = subject_id
- _predicate = hashlib.md5(_iri.encode("utf-8")).hexdigest()
- _to = object_id
- key_string = str(_from + _predicate + _to + graph)
- _key = hashlib.md5(key_string.encode("utf-8")).hexdigest()
+ adb_col_colblacklist = ["Statement", "List"] # TODO: REVISIT
+ for adb_col in adb_col_colblacklist:
+ all_v_cols.discard(adb_col)
- doc = {
- "_key": _key,
- "_iri": _iri,
+ for e_col, v_cols in self.__e_col_map.items():
+ edge_definitions.append(
+ {
+ "from_vertex_collections": list(v_cols["from"]),
+ "edge_collection": e_col,
+ "to_vertex_collections": list(v_cols["to"]),
+ }
+ )
+
+ non_orphan_v_cols |= {
+ c for c in v_cols["from"] | v_cols["to"] if c not in self.__e_col_map
+ }
+
+ orphan_v_cols = list(all_v_cols ^ non_orphan_v_cols ^ {self.__UNKNOWN_RESOURCE})
+
+ return self.db.create_graph(name, edge_definitions, orphan_v_cols)
+
+ ###################################################################################
+ # RDF to ArangoDB: RPT & PGT Shared Methods
+ # * load_meta_ontology
+ # * load_base_ontology
+ # * rdf_id_to_adb_key
+ # * rdf_id_to_adb_label
+ # * __parse_reified_triple
+ # * __add_adb_edge:
+ # * __infer_and_introspect_dr:
+ # * __build_explicit_type_map:
+ # * __build_subclass_tree:
+ # * __build_predicate_scope
+ # * __build_domain_range_map:
+ # * __combine_type_map_and_dr_map:
+ # * __get_literal_val:
+ # * __insert_adb_docs:
+ ###################################################################################
+
+ def load_meta_ontology(self, rdf_graph: RDFGraph) -> RDFConjunctiveGraph:
+ """An RDF-to-ArangoDB helper method that loads the RDF, RDFS, and OWL
+ Ontologies into **rdf_graph** as 3 sub-graphs. This method returns
+ an RDF Graph of type rdflib.graph.ConjunctiveGraph in order to support
+ sub-graph functionality.
+
+ This method is useful for users who seek to help contextualize their
+ RDF Graph within ArangoDB. A common use case would look like this:
+
+ ```
+ from arango_rdf import ArangoRDF
+ from arango import ArangoClient
+ from rdflib import Graph
+
+ db = ArangoClient(...)
+ adbrdf = ArangoRDF(db)
+
+ g = Graph()
+ g.parse('...')
+
+ cg = adbrdf.load_meta_ontology(g) # Returns a `ConjunctiveGraph`
+ adbrdf.rdf_to_arangodb_by_rpt('RPTGraph', cg, contextualize_graph=True)
+ adbrdf.rdf_to_arangodb_by_pgt('PGTGraph', cg, contextualize_graph=True)
+ ```
+
+ NOTE: If **rdf_graph** is already of type rdflib.graph.ConjunctiveGraph,
+ then the **same** graph is returned (pass by reference).
+
+ :param rdf_graph: The RDF Graph, soon to be converted into an ArangoDB Graph.
+ :type rdf_graph: rdflib.graph.Graph
+ :return: A ConjunctiveGraph equivalent of **rdf_graph** containing 3
+ additional subgraphs (RDF, RDFS, OWL)
+ :rtype: rdflib.graph.ConjunctiveGraph
+ """
+
+ graph: RDFConjunctiveGraph = (
+ rdf_graph
+ if isinstance(rdf_graph, RDFConjunctiveGraph)
+ else RDFConjunctiveGraph() + rdf_graph
+ )
+
+ for ns in os.listdir(f"{PROJECT_DIR}/meta"):
+ graph.parse(f"{PROJECT_DIR}/meta/{ns}", format="trig")
+
+ return graph
+
+ def load_base_ontology(self, rdf_graph: RDFGraph) -> RDFGraph:
+ """An RDF-to-ArangoDB helper method that loads a minimialistic
+ t-box into **rdf_graph**.
+
+ This method is called when users choose to set the
+ `contextualize_graph` flag to True via any of the two
+ `rdf_to_arangodb` methods.
+
+ The "base" t-box triples are:
+ 1)
+ 2)
+ 3)
+ 4)
+ 5)
+
+ :param rdf_graph: The RDF Graph, soon to be converted into an ArangoDB Graph.
+ :type rdf_graph: rdflib.graph.Graph
+ :return: The same **rdf_graph** with an addition of 5 statements
+ (at maximum) that make up the "base" t-box required for contextualizing
+ an RDF graph into ArangoDB.
+ :rtype: rdflib.graph.Graph
+ """
+
+ base_ontology = [
+ (RDFS.Class, RDF.type, RDFS.Class),
+ (RDF.Property, RDF.type, RDFS.Class),
+ (RDF.type, RDF.type, RDF.Property),
+ (RDFS.domain, RDF.type, RDF.Property),
+ (RDFS.range, RDF.type, RDF.Property),
+ (self.adb_col_uri, RDF.type, RDF.Property),
+ (self.adb_key_uri, RDF.type, RDF.Property),
+ ]
+
+ for t in base_ontology:
+ # We must make sure that we are not overwriting any quad statements
+ if t not in rdf_graph:
+ rdf_graph.add(t)
+
+ return rdf_graph
+
+ def rdf_id_to_adb_key(self, rdf_id: str, rdf_term: Optional[RDFTerm] = None) -> str:
+ """Convert an RDF Resource ID string into an ArangoDB Key via
+ some hashing function. If **rdf_term** is provided, then the value of
+ the statement (rdf_term adb:key "") will be used
+ as the ArangoDB Key (assuming that said statement exists).
+
+ Current hashing function used: FarmHash
+
+ List of hashing functions tested & benchmarked:
+ - Built-in hash() function
+ - Hashlib MD5
+ - xxHash
+ - MurmurHash
+ - CityHash
+ - FarmHash
+
+ :param rdf_id: The string representation of an RDF Resource
+ :type rdf_id: str
+ :param rdf_term: The optional RDF Term to check if it has an
+ adb:key statement associated to it.
+ :type rdf_term: Optional[URIRef | BNode | Literal]
+ :return: The ArangoDB _key equivalent of **rdf_id**
+ :rtype: str
+ """
+ # hash(rdf_id) # NOTE: not platform/session independent!
+ # hashlib.md5(rdf_id.encode()).hexdigest()
+ # xxhash.xxh64(rdf_id.encode()).hexdigest()
+ # mmh3.hash64(rdf_id, signed=False)[0]
+ # cityhash.CityHash64(item)
+ # FP64(rdf_id)
+
+ adb_key = self.rdf_graph.value(rdf_term, self.adb_key_uri)
+ return str(adb_key or FP64(rdf_id))
+
+ def rdf_id_to_adb_label(self, rdf_id: str) -> str:
+ """Return the suffix of an RDF URI. The suffix can (1)
+ be used as an ArangoDB Collection name, or (2) be used as
+ the `_label` property value for an ArangoDB Document.
+ For example, rdf_id_to_adb_label("http://example.com/Person")
+ returns "Person".
+
+ :param rdf_id: The string representation of a URIRef
+ :type rdf_id: str
+ :return: The suffix of the RDF URI string
+ :rtype: str
+ """
+ return re.split("/|#|:", rdf_id)[-1] or rdf_id
+
+ def __parse_reified_triple(
+ self, reified_subject: RDFTerm
+ ) -> Tuple[RDFTerm, URIRef, RDFTerm, str]:
+ """Helper method to extract the subject, predicate, object
+ values of a reified triple. Used if **simplify_reified_triples**
+ parameter is set to True.
+
+ :param reified_subject: The 'main' subject of the reified triple.
+ :type reified_subject: URIRef | BNode
+ :return: A tuple containing the reified triple's subject, predicate,
+ and object values, along with the ArangoDB Key of the reified triple.
+ :rtype: Tuple[RDFTerm, URIRef, RDFTerm, str]
+ """
+ s: RDFTerm = self.rdf_graph.value(reified_subject, RDF.subject)
+ p: URIRef = self.rdf_graph.value(reified_subject, RDF.predicate)
+ o: RDFTerm = self.rdf_graph.value(reified_subject, RDF.object)
+
+ reified_triple_key = self.rdf_id_to_adb_key(
+ str(reified_subject), reified_subject
+ )
+
+ return s, p, o, reified_triple_key
+
+ def __add_adb_edge(
+ self,
+ col: str,
+ key: str,
+ _from: str,
+ _to: str,
+ _uri: str,
+ _label: str,
+ _sg: str,
+ ) -> None:
+ """Insert the JSON-equivalent of an ArangoDB Edge
+ into `self.adb_docs` for temporary storage, until it gets
+ ingested into the **col** ArangoDB Collection.
+
+ :param col: The name of the ArangoDB Edge Collection.
+ :type col: str
+ :param key: The ArangoDB Key of the Edge.
+ :type key: str
+ :param _from: The _id of the ArangoDB _from document.
+ :type _from: str.
+ :param _to: The _id of the ArangoDB _to document.
+ :type _to: str.
+ :param _uri: The URI string of the RDF Predicate (i.e this edge).
+ :type _uri: str
+ :param _label: The "local name" of the RDF Predicate.
+ :type _label: str
+ :param _sg: The URI string of the Sub Graph associated to this edge (if any).
+ :type _sg: str
+ """
+
+ self.adb_docs[col][key] = {
+ **self.adb_docs[col][key],
+ "_key": key,
"_from": _from,
- "_predicate": _predicate,
"_to": _to,
+ "_uri": _uri,
+ "_label": _label,
+ "_rdftype": "URIRef",
}
- return doc
+ if _sg:
+ self.adb_docs[col][key]["_sub_graph_uri"] = _sg
+
+ def __infer_and_introspect_dr(
+ self,
+ p: URIRef,
+ p_key: str,
+ dr_meta: List[Tuple[RDFTerm, str, str, str, str]],
+ type_map: TypeMap,
+ predicate_scope: PredicateScope,
+ sg_str: str,
+ is_rpt: bool,
+ ) -> None:
+ """A helper method shared accross RDF to ArangoDB RPT & PGT to provide
+ Domain/Range (DR) Inference & Introspection.
+
+ DR Inference: Generate `RDF:type` statements for RDF Resources via the
+ `RDFS:Domain` & `RDFS:Range` statements of RDF Predicates.
+
+ DR Introspection: Generate `RDFS:Domain` & `RDFS:Range` statements for
+ RDF Predicates via the `RDF:type` statements of RDF Resources.
+
+ :param p: The RDF Predicate Object.
+ :type p: URIRef
+ :param p_key: The ArangoDB Key of the RDF Predicate Object.
+ :type p_key: str
+ :param dr_meta: The Domain & Range Metadata associated to the
+ current (s,p,o) statement.
+ :type dr_meta: List[Tuple[URIRef | BNode | Literal, str, str, str]]
+ :param type_map: A dictionary mapping the "natural" & "synthetic"
+ `RDF.Type` statements of every RDF Resource.
+ See `ArangoRDF.__combine_type_map_and_dr_map()` for more info.
+ :type type_map: arango_rdf.typings.TypeMap
+ :param predicate_scope: A dictionary mapping the Domain & Range
+ values of RDF Predicates. See `ArangoRDF.__build_predicate_scope()`
+ for more info.
+ :type predicate_scope: arango_rdf.typings.PredicateScope
+ :param sg_str: The string representation of the Sub Graph URI
+ of the statement associated to the current predicate **p**.
+ :type sg_str: str
+ :param is_rpt: A flag to identify if this method call originates
+ from an RPT process or not.
+ :type is_rpt: bool
+ """
+ if is_rpt:
+ TYPE_COL = self.__STATEMENT_COL
+ CLASS_COL = P_COL = self.__URIREF_COL
+ else:
+ TYPE_COL = "type"
+ CLASS_COL = "Class"
+ P_COL = "Property"
+
+ dr_map = {
+ "domain": (self.__rdfs_domain_str, self.__rdfs_domain_key),
+ "range": (self.__rdfs_range_str, self.__rdfs_range_key),
+ }
+
+ for t, t_col, t_key, t_label, dr_label in dr_meta:
+ if isinstance(t, Literal):
+ continue
+
+ DR_COL = self.__STATEMENT_COL if is_rpt else dr_label
+
+ # Domain/Range Inference
+ # TODO: REVISIT CONDITIONS FOR INFERENCE
+ # t_has_no_type_statement = len(type_map[t]) == 0
+ t_has_no_type_statement = (t, RDF.type, None) not in self.rdf_graph
+ if t_has_no_type_statement:
+ for _, class_key in predicate_scope[p][dr_label]:
+ self.__add_adb_edge(
+ TYPE_COL,
+ str(FP64(f"{t_key}-{self.__rdf_type_key}-{class_key}")),
+ f"{t_col}/{t_key}",
+ f"{CLASS_COL}/{class_key}",
+ self.__rdf_type_str,
+ "type",
+ sg_str,
+ )
+
+ if not is_rpt:
+ self.__e_col_map["type"]["from"].add(t_col)
+ self.__e_col_map["type"]["to"].add("Class")
+
+ # Domain/Range Introspection
+ # TODO: REVISIT CONDITIONS FOR INTROSPECTION
+ # p_dr_not_in_graph = (p, RDFS[dr_label], None) not in self.rdf_graph
+ # p_dr_not_in_meta_graph = (p, RDFS[dr_label], None) not in self.meta_graph
+ p_already_has_dr = dr_label in predicate_scope[p]
+ p_used_in_meta_graph = (None, p, None) in self.meta_graph
+ if type_map[t] and not p_already_has_dr and not p_used_in_meta_graph:
+ dr_str, dr_key = dr_map[dr_label]
+
+ for class_str in type_map[t]:
+ # TODO: optimize class_key
+ class_key = self.rdf_id_to_adb_key(class_str)
+ self.__add_adb_edge(
+ DR_COL,
+ str(FP64(f"{p_key}-{dr_key}-{class_key}")),
+ f"{P_COL}/{p_key}",
+ f"{CLASS_COL}/{class_key}",
+ dr_str,
+ dr_label,
+ sg_str,
+ )
+
+ def __build_explicit_type_map(
+ self, add_to_adb_mapping: Callable[[RDFTerm, str, bool], None] = empty_function
+ ) -> TypeMap:
+ """An RPT/PGT helper method used to build a dictionary mapping
+ the (subject rdf:type object) relationships within the RDF Graph.
+
+ Essential for providing Domain & Range Introspection, and essential for
+ completing the ArangoDB Collection Mapping Process.
+
+ For example, given the following snippet:
+ -----------------------------
+ @prefix ex: .
+
+ ex:bob rdf:type ex:Person .
+ ex:bob rdf:type ex:Parent .
+ ex:bob ex:son ex:alex .
+ -----------------------------
+ The `explicit_type_map` would look like:
+ ```
+ {
+ URIRef("ex:bob"): {"ex:Person", "ex:Parent"},
+ URIRef("ex:son"): {"rdf:Property"},
+ URIRef("ex:alex"): {}
+ }
+ ```
+
+ :param adb_mapping: The ADB Mapping of the current (RDF to
+ ArangoDB) PGT Process. If not specified, then it is implied that
+ this method was called from an RPT context.
+ :type adb_mapping: rdflib.graph.Graph | None
+ :return: The explicit_type_map dictionary mapping all RDF Statements of
+ the form (subject rdf:type object).
+ :rtype: DefaultDict[RDFTerm, Set[str]]
+ """
+ explicit_type_map: TypeMap = defaultdict(set)
+
+ s: URIRef
+ p: URIRef
+ o: URIRef
+
+ # RDF Type Statements
+ for s, o, *_ in self.rdf_graph[: RDF.type :]:
+ explicit_type_map[s].add(str(o))
+ add_to_adb_mapping(o, "Class", True)
+
+ # RDF Predicates
+ for p in self.rdf_graph.predicates(unique=True):
+ explicit_type_map[p].add(self.__rdf_property_str)
+ add_to_adb_mapping(p, "Property", True)
+
+ # RDF Type Statements (Reified)
+ for s in self.rdf_graph[: RDF.predicate : RDF.type]:
+ reified_s: URIRef = self.rdf_graph.value(s, RDF.subject)
+ reified_o: URIRef = self.rdf_graph.value(s, RDF.object)
+
+ explicit_type_map[reified_s].add(str(reified_o))
+ add_to_adb_mapping(reified_o, "Class", True)
+
+ # RDF Predicates (Reified)
+ for s, o, *_ in self.rdf_graph[: RDF.predicate :]:
+ explicit_type_map[o].add(self.__rdf_property_str)
+ add_to_adb_mapping(o, "Property", True)
+
+ return explicit_type_map
+
+ def __build_subclass_tree(
+ self, add_to_adb_mapping: Callable[[RDFTerm, str, bool], None] = empty_function
+ ) -> Tree:
+ """An RPT/PGT helper method used to build a Tree Data Structure
+ representing the `rdfs:subClassOf` Taxonomy of the RDF Graph.
+
+ Essential for providing Domain & Range Introspection, and essential for
+ completing the ArangoDB Collection Mapping Process.
+
+ For example, given the following snippet:
+ -----------------------------
+ @prefix ex: .
+
+ ex:Zenkey rdfs:subClassOf :Zebra .
+ ex:Zenkey rdfs:subClassOf :Donkey .
+ ex:Donkey rdfs:subClassOf :Animal .
+ ex:Zebra rdfs:subClassOf :Animal .
+ ex:Human rdfs:subClassOf :Animal .
+ ex:Animal rdfs:subClassOf :LivingThing .
+ ex:LivingThing rdfs:subClassOf :Thing .
+ -----------------------------
+ The `subclass_tree` would look like:
+ ```
+ ==================
+ |http://www.w3.org/2000/01/rdf-schema#Resource
+ |-...
+ |-http://www.w3.org/2000/01/rdf-schema#Class
+ |-...
+ |--...
+ |--http://example.com/Thing
+ |---http://example.com/LivingThing
+ |----http://example.com/Animal
+ |-----http://example.com/Donkey
+ |------http://example.com/Zenkey
+ |-----http://example.com/Human
+ |-----http://example.com/Zebra
+ |------http://example.com/Zenkey
+ ==================
+ ```
+
+ :param adb_mapping: The ADB Mapping of the current (RDF to
+ ArangoDB) PGT Process. If not specified, then it is implied that
+ this method was called from an RPT context.
+ :type adb_mapping: rdflib.graph.Graph | None
+ :return: The subclass_tree containing the RDFS SubClassOf Taxonomy.
+ :rtype: arango_rdf.utils.Tree
+ """
+ subclass_map: DefaultDict[str, Set[str]] = defaultdict(set)
+ subclass_graph = self.meta_graph + self.rdf_graph
+
+ # RDFS SubClassOf Statements
+ for s, o, *_ in subclass_graph[: RDFS.subClassOf :]:
+ subclass_map[str(o)].add(str(s))
+
+ add_to_adb_mapping(s, "Class", True)
+ add_to_adb_mapping(o, "Class", True)
+
+ # RDF SubClassOf Statements (Reified)
+ for s in subclass_graph[: RDF.predicate : RDFS.subClassOf]:
+ reified_s: URIRef = self.rdf_graph.value(s, RDF.subject)
+ reified_o: URIRef = self.rdf_graph.value(s, RDF.object)
+
+ subclass_map[str(reified_o)].add(str(reified_s))
+ add_to_adb_mapping(reified_s, "Class", True)
+ add_to_adb_mapping(reified_o, "Class", True)
+
+ # Connect any 'parent' URIs (i.e URIs that aren't a subclass of another URI)
+ # to the RDFS Class URI (prevents having multiple subClassOf taxonomies)
+ # Excludes the RDFS Resource URI
+ for key in set(subclass_map) - {self.__rdfs_resource_str}:
+ if (URIRef(key), RDFS.subClassOf, None) not in subclass_graph:
+ # TODO: Consider using OWL:Thing instead of RDFS:Class
+ subclass_map[self.__rdfs_class_str].add(key)
+
+ return Tree(root=Node(self.__rdfs_resource_str), submap=subclass_map)
+
+ def __build_predicate_scope(
+ self, add_to_adb_mapping: Callable[[RDFTerm, str, bool], None] = empty_function
+ ) -> PredicateScope:
+ """An RPT/PGT helper method used to build a dictionary mapping
+ the Domain & Range values of RDF Predicates within `self.rdf_graph`.
+
+ Essential for providing Domain & Range Inference, and essential for
+ completing the ArangoDB Collection Mapping Process.
+
+ For example, given the following snippet:
+ --------------------------------
+ @prefix ex: .
+
+ ex:name rdfs:domain ex:Person .
+ ex:son rdfs:domain ex:Parent .
+ ex:son rdfs:range ex:Person .
+ --------------------------------
+ The `predicate_scope` would look like:
+ ```
+ {
+ URIRef("ex:name"): {
+ "domain": {("ex:Person", hash("ex:Person")),}
+ "range": {}
+ },
+ URIRef("ex:son"): {
+ "domain": {("ex:Parent", hash("ex:Parent)),}
+ "range": {("ex:Person", hash("ex:Person")),}
+ }
+ }
+ ```
+
+ :param adb_mapping: The ADB Mapping of the current (RDF to
+ ArangoDB) PGT Process. If not specified, then it is implied that
+ this method was called from an RPT context.
+ :type adb_mapping: rdflib.graph.Graph | None
+ :return: The predicate_scope dictionary mapping all predicates within the
+ RDF Graph to their respective Domain & Range values..
+ :rtype: arango_rdf.typings.PredicateScope
+ """
+ class_blacklist = [self.__rdfs_literal_str, self.__rdfs_resource_str]
+
+ predicate_scope: PredicateScope = defaultdict(lambda: defaultdict(set))
+ predicate_scope_graph = self.meta_graph + self.rdf_graph
+
+ # RDFS Domain & Range
+ for label in ["domain", "range"]:
+ for p, c in predicate_scope_graph[: RDFS[label] :]:
+ class_str = str(c)
+
+ if class_str not in class_blacklist:
+ class_key = self.rdf_id_to_adb_key(class_str)
+ predicate_scope[p][label].add((class_str, class_key))
+
+ add_to_adb_mapping(p, "Property", True)
+ add_to_adb_mapping(c, "Class", True)
+
+ # RDFS Domain & Range (Reified)
+ for label in ["domain", "range"]:
+ t = predicate_scope_graph[: RDF.predicate : RDFS[label]]
+ for s in t:
+ reified_s: URIRef = self.rdf_graph.value(s, RDF.subject)
+ reified_o: URIRef = self.rdf_graph.value(s, RDF.object)
+
+ class_str = str(reified_o)
+
+ if class_str not in class_blacklist:
+ class_key = self.rdf_id_to_adb_key(class_str)
+ predicate_scope[reified_s][label].add((class_str, class_key))
+
+ add_to_adb_mapping(reified_s, "Property", True)
+ add_to_adb_mapping(reified_o, "Class", True)
+
+ return predicate_scope
+
+ def __build_domain_range_map(self, predicate_scope: PredicateScope) -> TypeMap:
+ """An RPT/PGT helper method used to build a dictionary mapping
+ the Domain/Range inference results of all RDF Subjects/Objects
+ that are found in an RDF Statement containing a Predicate with a
+ defined Domain or Range.
+
+ Essential for completing the ArangoDB Collection Mapping Process.
+
+ For example, given the following snippet:
+ ----------------------------------
+ @prefix ex: .
+
+ ex:bob ex:address "123 Main st" .
+ ex:bob ex:son ex:alex .
+
+ ex:address rdfs:domain ex:Entity .
+ ex:son rdfs:domain ex:Parent .
+ ex:son rdfs:range ex:Person .
+ ----------------------------------
+ The `domain_range_map` would look like:
+ ```
+ {
+ URIRef("ex:bob"): {"ex:Entity", "ex:Parent"},
+ URIRef("ex:alex"): {"ex:Person"}
+ }
+ ```
+
+ :param predicate_scope: The mapping of RDF Predicates to their
+ respective domain/range values.
+ :type predicate_scope: arango_rdf.typings.PredicateScope
+ :return: The Domain and Range Mapping
+ :rtype: arango_rdf.typings.TypeMap
+ """
+ domain_range_map: TypeMap = defaultdict(set)
+
+ s: URIRef
+ o: URIRef
+ for p, scope in predicate_scope.items():
+ # RDF Triples
+ for s, o, *_ in self.rdf_graph[:p:]:
+ for class_str, _ in scope["domain"]:
+ domain_range_map[s].add(class_str)
+
+ for class_str, _ in scope["range"]:
+ domain_range_map[o].add(class_str)
+
+ # RDF Triples (Reified)
+ for s in self.rdf_graph[: RDF.predicate : p]:
+ reified_s: URIRef = self.rdf_graph.value(s, RDF.subject)
+ reified_o: URIRef = self.rdf_graph.value(s, RDF.object)
+
+ for class_str, _ in scope["domain"]:
+ domain_range_map[reified_s].add(class_str)
+
+ for class_str, _ in scope["range"]:
+ domain_range_map[reified_o].add(class_str)
+
+ return domain_range_map
+
+ def __combine_type_map_and_dr_map(
+ self,
+ explicit_type_map: TypeMap,
+ domain_range_map: TypeMap,
+ ) -> TypeMap:
+ """An RPT/PGT helper method used to combine the results of the
+ `__build_explicit_type_map()` & `__build_domain_range_map()` methods.
+
+ Essential for providing Domain & Range Introspection.
+
+ :param explicit_type_map: The Explicit Type Map produced by the
+ `ArangoRDF.__build_explicit_type_map()` method.
+ :type explicit_type_map: arango_rdf.typings.TypeMap
+ :param domain_range_map: The Domain and Range Map produced by the
+ `ArangoRDF.__build_domain_range_map()` method.
+ :type domain_range_map: arango_rdf.typings.TypeMap
+ :return: The combined mapping (union) of the two dictionaries provided.
+ :rtype: arango_rdf.typings.TypeMap
+ """
+ type_map: TypeMap = defaultdict(set)
+
+ for key in explicit_type_map.keys() | domain_range_map.keys():
+ type_map[key] = explicit_type_map[key] | domain_range_map[key]
+
+ return type_map
+
+ def __get_literal_val(self, t: Literal, t_str: str) -> Any:
+ """Extracts a JSON-serializable representation
+ of a Literal's value based on its datatype.
+
+ :param t: The RDF Literal object.
+ :type t: Literal
+ :param t_str: The string representation of the RDF Literal
+ :type t_str: str
+ :return: A JSON-serializable value representing the Literal
+ :rtype: Any
+ """
+ if isinstance(t.value, (date, time, Duration)):
+ return t_str
+
+ if t.datatype == XSD.decimal:
+ return float(t.value)
+
+ return t.value if t.value is not None else t_str
+
+ def __insert_adb_docs(
+ self, use_async: bool, adb_col_blacklist: Set[str] = set()
+ ) -> None:
+ """Insert ArangoDB documents into their ArangoDB collection.
+
+ :param use_async: Performs asynchronous ingestion if enabled.
+ :type use_async: bool
+ :param adb_col_blacklist: A list of ArangoDB Collections that will not be
+ populated on this call of `__insert_adb_docs()`. Essential for allowing List
+ construction of RDF Literals (PGT Only).
+ :type adb_col_blacklist: Set[str]
+ """
+ if len(self.adb_docs) == 0:
+ return
+
+ db = self.async_db if use_async else self.db
+
+ # Avoiding "RuntimeError: dictionary changed size during iteration"
+ adb_cols = list(self.adb_docs.keys())
+
+ for adb_col in adb_cols:
+ if adb_col in adb_col_blacklist:
+ continue
+
+ action = f"ArangoDB Import: {adb_col}"
+ adb_task = self.__adb_iterator.add_task("", action=action)
+
+ if not self.db.has_collection(adb_col):
+ is_edge = adb_col in self.__e_col_map
+ self.db.create_collection(adb_col, edge=is_edge)
+
+ col = db.collection(adb_col)
+ docs = self.adb_docs[adb_col].values()
+ col.import_bulk(docs, **self.__import_options)
+
+ del self.adb_docs[adb_col] # Clear buffer
+
+ self.__adb_iterator.stop_task(adb_task)
+ self.__adb_iterator.update(adb_task, visible=False)
+
+ gc.collect()
+
+ ###################################################################################
+ # ArangoDB to RDF Methods
+ # * arangodb_to_rdf:
+ # * arangodb_collections_to_rdf:
+ # * arangodb_graph_to_rdf:
+ # * __process_adb_doc:
+ # * __add_to_rdf_graph:
+ # * __adb_val_to_rdf_val:
+ # * __fetch_adb_docs:
+ ###################################################################################
+
+ def arangodb_to_rdf(
+ self,
+ name: str,
+ rdf_graph: RDFGraph,
+ metagraph: ADBMetagraph,
+ list_conversion_mode: str = "static",
+ infer_type_from_adb_v_col: bool = False,
+ include_adb_key_statements: bool = False,
+ **export_options: Any,
+ ) -> Tuple[RDFGraph, RDFGraph]:
+ """Create an RDF Graph from an ArangoDB Graph via its Metagraph.
+
+ :param name: The name of the ArangoDB Graph
+ :type name: str
+ :param rdf_graph: The target RDF Graph to insert into.
+ :type rdf_graph: rdflib.graph.Graph
+ :param metagraph: An dictionary of dictionaries defining the ArangoDB Vertex
+ & Edge Collections whose entries will be inserted into the RDF Graph.
+ :type metagraph: arango_rdf.typings.ADBMetagraph
+ :param list_conversion_mode: Specify how ArangoDB JSON lists
+ are handled andprocessed into the RDF Graph. If "collection", ArangoDB
+ lists will be processed using the RDF Collection structure. If "container",
+ lists found within the ArangoDB Graph will be processed using the
+ RDF Container structure. If "static", elements within lists will be
+ processed as individual statements. Defaults to "static".
+ :type list_conversion_mode: str
+ :param infer_type_from_adb_v_col: Specify whether `rdf:type` relationships
+ of the form (resource rdf:type adb_col) should be inferred upon
+ transferring ArangoDB Vertices into RDF. NOTE: Enabling this flag
+ is only recommended if your ArangoDB graph is "native" to ArangoDB.
+ That is, the ArangoDB graph does not originate from an RDF context.
+ :type infer_type_from_adb_v_col: bool
+ :param include_adb_key_statements: Specify whether `adb:key` relationships
+ of the form (adb_doc adb:key adb_doc["key"]) should be generated upon
+ transferring ArangoDB Documents into RDF. This can be used to
+ maintain document keys when a user is interested in round-tripping.
+ NOTE: Enabling this flag is only recommended if your ArangoDB graph
+ is "native" to ArangoDB. That is, the ArangoDB graph does not
+ originate from an RDF context.
+ :type include_adb_key_statements: bool
+ :param export_options: Keyword arguments to specify AQL query options when
+ fetching documents from the ArangoDB instance. Full parameter list:
+ https://docs.python-arango.com/en/main/specs.html#arango.aql.AQL.execute
+ :type export_options: Any
+ :return: The RDF representation of the ArangoDB Graph, along with a second
+ RDF Graph mapping the RDF Resources to their designated ArangoDB Collection.
+ The second graph, **adb_mapping**, can then be re-used in the RDF to
+ ArangoDB (PGT) process to maintain the Document-to-Collection mappings.
+ :rtype: Tuple[rdflib.graph.Graph, rdflib.graph.Graph]
+ """
+
+ self.rdf_graph = rdf_graph
+ self.__graph_supports_quads = isinstance(self.rdf_graph, RDFConjunctiveGraph)
+
+ self.__list_conversion = list_conversion_mode
+ self.__include_adb_key_statements = include_adb_key_statements
+ self.__graph_ns = f"{self.db._conn._url_prefixes[0]}/{name}#"
+
+ self.rdf_graph.bind(name, self.__graph_ns)
+ self.rdf_graph.bind("adb", self.__adb_ns)
+
+ # Maps the (soon-to-be) RDF Resources to their ArangoDB Collection
+ self.adb_mapping = RDFGraph()
+ self.adb_mapping.bind(name, self.__graph_ns)
+ self.adb_mapping.bind("adb", self.__adb_ns)
+
+ # Maps ArangoDB Document IDs to RDFLib Terms (i.e URIRef, Literal, BNode)
+ self.__term_map: Dict[str, RDFTerm] = {}
+
+ # Maps ArangoDB Document IDs to URIRefs
+ # Essential for preserving the original URIs of ArangoDB
+ # Document Properties that were once in an RDF Graph
+ self.__uri_map: Dict[str, URIRef] = {}
+
+ # Maps RDF Resources to the last Sub Graph that they been seen in (if any)
+ self.__subgraph_map: Dict[str, URIRef] = {}
+
+ self.adb_key_blacklist = {
+ "_id",
+ "_key",
+ "_rev",
+ "_rdftype",
+ "_uri",
+ "_value",
+ "_label",
+ "_from",
+ "_to",
+ "_sub_graph_uri",
+ }
+
+ adb_v_col_blacklist = {
+ f"{name}_URIRef",
+ f"{name}_BNode",
+ f"{name}_Literal",
+ f"{name}_UnknownResource",
+ }
+
+ adb_v_cols = set(metagraph["vertexCollections"])
+ adb_e_cols = set(metagraph["edgeCollections"])
+
+ doc: Json
+ edge: Json
+
+ # PGT Scenario: Build a mapping of the RDF Predicates stored in ArangoDB
+ if self.db.has_collection("Property"):
+ for doc in self.db.collection("Property"):
+ if doc.keys() >= {"_uri", "_label"}:
+ self.__uri_map[doc["_label"]] = URIRef(doc["_uri"])
+
+ term: Union[URIRef, BNode, Literal]
+ for v_col in adb_v_cols:
+ if v_col in adb_e_cols:
+ continue
+
+ v_col_uri = URIRef(f"{self.__graph_ns}{v_col}")
+
+ self.__set_iterators(f" ADB → RDF ({v_col})", "#97C423", "")
+ with Live(Group(self.__adb_iterator, self.__rdf_iterator)):
+ total: int = self.db.collection(v_col).count()
+ self.__rdf_task = self.__rdf_iterator.add_task("", total=total)
+
+ cursor = self.__fetch_adb_docs(v_col, export_options)
+ while not cursor.empty():
+ for doc in cursor.batch():
+ self.__rdf_iterator.update(self.__rdf_task, advance=1)
+
+ term = self.__process_adb_doc(doc)
+ self.__term_map[doc["_id"]] = term
+
+ if isinstance(term, Literal):
+ continue
+
+ if not self.__graph_supports_quads:
+ sg = self.__subgraph_map.get(doc["_id"])
+ self.__unpack_adb_doc(doc, term, sg)
+
+ if self.__include_adb_key_statements and type(term) is URIRef:
+ key = Literal(doc["_key"])
+ self.__add_to_rdf_graph(term, self.adb_key_uri, key)
+
+ if v_col not in adb_v_col_blacklist:
+ self.__add_to_adb_mapping(term, v_col)
+
+ if infer_type_from_adb_v_col:
+ self.__add_to_rdf_graph(term, RDF.type, v_col_uri)
+
+ cursor.batch().clear()
+ if cursor.has_more():
+ cursor.fetch()
+
+ for e_col in adb_e_cols:
+ e_col_uri = URIRef(f"{self.__graph_ns}{e_col}")
+
+ self.__set_iterators(f" ADB → RDF ({e_col})", "#5E3108", "")
+ with Live(Group(self.__adb_iterator, self.__rdf_iterator)):
+ total = self.db.collection(e_col).count()
+ self.__rdf_task = self.__rdf_iterator.add_task("", total=total)
+
+ cursor = self.__fetch_adb_docs(e_col, export_options)
+ while not cursor.empty():
+ for edge in cursor.batch():
+ self.__rdf_iterator.update(self.__rdf_task, advance=1)
+
+ self.__process_adb_edge(edge, e_col_uri)
+
+ cursor.batch().clear()
+ if cursor.has_more():
+ cursor.fetch()
+
+ # TODO: REVISIT
+ # Not a fan of this at all...
+ # Unfortunatley required to preserve subgraph information
+ if self.__graph_supports_quads:
+ for v_col, _ in metagraph["vertexCollections"].items():
+ cursor = self.__fetch_adb_docs(v_col, export_options)
+
+ while not cursor.empty():
+ for doc in cursor.batch():
+ term = self.__term_map[doc["_id"]]
+
+ if not isinstance(term, Literal):
+ sg = self.__subgraph_map.get(doc["_id"])
+ self.__unpack_adb_doc(doc, term, sg)
+
+ cursor.batch().clear()
+ if cursor.has_more():
+ cursor.fetch()
+
+ return self.rdf_graph, self.adb_mapping
+
+ def arangodb_collections_to_rdf(
+ self,
+ name: str,
+ rdf_graph: RDFGraph,
+ v_cols: Set[str],
+ e_cols: Set[str],
+ list_conversion_mode: str = "static",
+ infer_type_from_adb_v_col: bool = False,
+ include_adb_key_statements: bool = False,
+ **export_options: Any,
+ ) -> Tuple[RDFGraph, RDFGraph]:
+ """Create an RDF Graph from an ArangoDB Graph via its Collection Names.
+
+ :param name: The name of the ArangoDB Graph
+ :type name: str
+ :param rdf_graph: The target RDF Graph to insert into.
+ :type rdf_graph: rdflib.graph.Graph
+ :param v_cols: The set of ArangoDB Vertex Collections to import to RDF.
+ :type v_cols: Set[str]
+ :param e_cols: The set of ArangoDB Edge Collections to import to RDF.
+ :type e_cols: Set[str]
+ :param list_conversion_mode: Specify how ArangoDB JSON lists
+ are handled andprocessed into the RDF Graph. If "collection", ArangoDB
+ lists will be processed using the RDF Collection structure. If "container",
+ lists found within the ArangoDB Graph will be processed using the
+ RDF Container structure. If "static", elements within lists will be
+ processed as individual statements. Defaults to "static".
+ :type list_conversion_mode: str
+ :param infer_type_from_adb_v_col: Specify whether `rdf:type` relationships
+ of the form (adb_doc rdf:type adb_col) should be inferred upon
+ transferring ArangoDB Documents into RDF. NOTE: Enabling this flag
+ is only recommended if your ArangoDB graph is "native" to ArangoDB.
+ That is, the ArangoDB graph does not originate from an RDF context.
+ :type infer_type_from_adb_v_col: bool
+ :param include_adb_key_statements: Specify whether `adb:key` relationships
+ of the form (adb_doc adb:key adb_doc["key"]) should be generated upon
+ transferring ArangoDB Documents into RDF. This can be used to
+ maintain document keys when a user is interested in round-tripping.
+ NOTE: Enabling this flag is only recommended if your ArangoDB graph
+ is "native" to ArangoDB. That is, the ArangoDB graph does not
+ originate from an RDF context.
+ :type include_adb_key_statements: bool
+ :param export_options: Keyword arguments to specify AQL query options when
+ fetching documents from the ArangoDB instance. Full parameter list:
+ https://docs.python-arango.com/en/main/specs.html#arango.aql.AQL.execute
+ :type export_options: Any
+ :return: The RDF representation of the ArangoDB Graph, along with a second
+ RDF Graph mapping the RDF Resources to their designated ArangoDB Collection.
+ The second graph, **adb_mapping**, can then be re-used in the RDF to
+ ArangoDB (PGT) process to maintain the Document-to-Collection mappings.
+ :rtype: Tuple[rdflib.graph.Graph, rdflib.graph.Graph]
+ """
+ metagraph: ADBMetagraph = {
+ "vertexCollections": {col: set() for col in v_cols},
+ "edgeCollections": {col: set() for col in e_cols},
+ }
+
+ return self.arangodb_to_rdf(
+ name,
+ rdf_graph,
+ metagraph,
+ list_conversion_mode,
+ infer_type_from_adb_v_col,
+ include_adb_key_statements,
+ **export_options,
+ )
+
+ def arangodb_graph_to_rdf(
+ self,
+ name: str,
+ rdf_graph: RDFGraph,
+ list_conversion_mode: str = "static",
+ infer_type_from_adb_v_col: bool = False,
+ include_adb_key_statements: bool = False,
+ **export_options: Any,
+ ) -> Tuple[RDFGraph, RDFGraph]:
+ """Create an RDF Graph from an ArangoDB Graph via its Graph Name.
+
+ :param name: The name of the ArangoDB Graph
+ :type name: str
+ :param rdf_graph: The target RDF Graph to insert into.
+ :type rdf_graph: rdflib.graph.Graph
+ :param list_conversion_mode: Specify how ArangoDB JSON lists
+ are handled andprocessed into the RDF Graph. If "collection", ArangoDB
+ lists will be processed using the RDF Collection structure. If "container",
+ lists found within the ArangoDB Graph will be processed using the
+ RDF Container structure. If "static", elements within lists will be
+ processed as individual statements. Defaults to "static".
+ :type list_conversion_mode: str
+ :param infer_type_from_adb_v_col: Specify whether `rdf:type` relationships
+ of the form (adb_doc rdf:type adb_col) should be inferred upon
+ transferring ArangoDB Documents into RDF. NOTE: Enabling this flag
+ is only recommended if your ArangoDB graph is "native" to ArangoDB.
+ That is, the ArangoDB graph does not originate from an RDF context.
+ :type infer_type_from_adb_v_col: bool
+ :param include_adb_key_statements: Specify whether `adb:key` relationships
+ of the form (adb_doc adb:key adb_doc["key"]) should be generated upon
+ transferring ArangoDB Documents into RDF. This can be used to
+ maintain document keys when a user is interested in round-tripping.
+ NOTE: Enabling this flag is only recommended if your ArangoDB graph
+ is "native" to ArangoDB. That is, the ArangoDB graph does not
+ originate from an RDF context.
+ :type include_adb_key_statements: bool
+ :param export_options: Keyword arguments to specify AQL query options when
+ fetching documents from the ArangoDB instance. Full parameter list:
+ https://docs.python-arango.com/en/main/specs.html#arango.aql.AQL.execute
+ :type export_options: Any
+ :return: The RDF representation of the ArangoDB Graph, along with a second
+ RDF Graph mapping the RDF Resources to their designated ArangoDB Collection.
+ The second graph, **adb_mapping**, can then be re-used in the RDF to
+ ArangoDB (PGT) process to maintain the Document-to-Collection mappings.
+ :rtype: Tuple[rdflib.graph.Graph, rdflib.graph.Graph]
+ """
+ graph = self.db.graph(name)
+ v_cols = {col for col in graph.vertex_collections()}
+ e_cols = {col["edge_collection"] for col in graph.edge_definitions()}
+
+ return self.arangodb_collections_to_rdf(
+ name,
+ rdf_graph,
+ v_cols,
+ e_cols,
+ list_conversion_mode,
+ infer_type_from_adb_v_col,
+ include_adb_key_statements,
+ **export_options,
+ )
+
+ def __process_adb_doc(self, doc: Json) -> RDFTerm:
+ """An ArangoDB to RDF helper method used to process ArangoDB
+ JSON documents as an RDF Term. Returns the URIRef, BNode, or
+ Literal equivalent of **doc**. If **doc** does not have
+ "_rdftype" as a property, then the URIRef type is used.
+
+ :param doc: An arbitrary ArangoDB document.
+ :type doc: Dict[str, Any]
+ :return: The RDF Term representing the ArangoDB document
+ :rtype: URIRef | BNode | Literal
+ """
+ key_map = {
+ "URIRef": "_uri",
+ "Literal": "_value",
+ "BNode": "_key",
+ }
+
+ rdf_type = doc.get("_rdftype", "URIRef")
+ val = doc.get(key_map[rdf_type], f"{self.__graph_ns}{doc['_key']}")
+
+ if rdf_type == "URIRef":
+ return URIRef(val)
+
+ elif rdf_type == "BNode":
+ return BNode(val)
+
+ elif rdf_type == "Literal":
+ if "_lang" in doc:
+ return Literal(val, lang=doc["_lang"])
+
+ elif "_datatype" in doc:
+ return Literal(val, datatype=doc["_datatype"])
- def adb_doc_to_rdf_node(self, adb_doc: dict) -> Union[URIRef, BNode, Literal]:
- # build literal
- if "_type" in adb_doc:
- if adb_doc["_lang"] is not None:
- return Literal(adb_doc["_value"], lang=adb_doc["_lang"])
else:
- return Literal(adb_doc["_value"], datatype=adb_doc["_type"])
+ return Literal(val)
- # build URIRef
- if "_iri" in adb_doc:
- return URIRef(adb_doc["_iri"])
+ else: # pragma: no cover
+ raise ValueError(f"Unrecognized type '{rdf_type}' ({doc})")
- # build BNode
- return BNode(value=adb_doc["_key"])
+ def __process_missing_adb_doc(self, doc_id: str) -> Union[URIRef, BNode, Literal]:
+ """An ArangoDB to RDF helper method used to process missing ArangoDB
+ JSON documents as an RDF Term. A "missing" ArangoDB JSON Document is defined
+ as a document whose ID was encountered during the `self.__process_adb_edge`
+ step (i.e it is part of an arbitrary ArangoDB edge),
+ but was not originally processed & placed into the `self.term_map`.
+ This is useful for when ArangoDB Edges refer to other ArangoDB Edges.
- def save_config(self, config: dict) -> None:
- if self.db.has_collection("configurations") is False:
- self.db.create_collection("configurations")
- else:
- aql = """
- FOR c IN configurations
- FILTER c.latest == true
- UPDATE c WITH { latest: false } INTO configurations
+ Returns the URIRef, BNode, or Literal equivalent of **doc_id**.
+
+ :param doc_id: An arbitrary ArangoDB Document ID.
+ :type doc: str
+ :return: The RDF Term representing the ArangoDB document
+ :rtype: URIRef | BNode | Literal
+ """
+ if doc_id in self.__term_map:
+ return self.__term_map[doc_id]
+
+ # Expensive...
+ doc: Json = self.db.document({"_id": doc_id})
+
+ if not doc:
+ m = f"""
+ Unable to find ArangoDB Document
+ '{doc_id}' within Database {self.db.name}
"""
+ raise ValueError(m)
- self.db.aql.execute(aql)
+ elif doc.keys() >= {"_from", "_to"}:
+ e_col = doc["_id"].split("/")[0]
+ e_col_uri = URIRef(f"{self.__graph_ns}{e_col}")
- config["latest"] = True
- config["timestamp"] = time.time()
- self.db.collection("configurations").insert(config)
+ edge_uri = URIRef(f"{self.__graph_ns}{doc['_key']}")
+ self.__term_map[doc_id] = edge_uri
+
+ self.__process_adb_edge(doc, e_col_uri, True)
+
+ return edge_uri
+
+ else:
+ term = self.__process_adb_doc(doc)
+ self.__term_map[doc_id] = term
- def get_config_by_latest(self) -> dict:
- return self.get_config_by_key_value("latest", True)
+ return term
- def get_config_by_key_value(self, key: str, val: Any) -> dict:
- aql = """
- FOR c IN configurations
- FILTER c[@key] == @val
- SORT c.timestamp DESC
- LIMIT 1
- RETURN UNSET(c, "_id", "_key", "_rev")
+ def __process_adb_edge(
+ self,
+ edge: Json,
+ e_col_uri: URIRef,
+ edge_is_referenced_by_another_edge: bool = False,
+ ) -> None:
+ """An ArangoDB to RDF helper method used to process ArangoDB Edges
+ into RDF Statements. Relies on the `self.__process_missing_adb_doc`
+ method for when the "_from" or "_to" Documents have not been
+ already processed & placed in `self.term_map`.
+
+ Does the following:
+ 1. Extracts the (subjecct, predicate, object) values from **edge**
+ 2. Extracts the Subgraph URI value from the edge (if any)
+ 3. Adds the (subject, predicate, object) statement to the RDF Graph
+ 4. Unpacks any edge properties of **edge**
+ 5. Reifies the (subject, predicate, object) statement
+
+ :param edge: The ArangoDB Edge
+ :type edge: Json
+ :param e_col_uri: The URIRef associated to the ArangoDB Collection
+ of **edge**. Used if **edge** does not have a `_uri` attribute.
+ :type e_col_uri: URIRef
+ :param edge_is_referenced_by_another_edge: Set to True if the current edge
+ is set as the "_from" or "_to" value of another arbitrary ArangoDB Edge.
+ :type edge_is_referenced_by_another_edge: bool
+ """
+ _from: str = edge["_from"]
+ _to: str = edge["_to"]
+
+ subject = self.__term_map.get(_from) or self.__process_missing_adb_doc(_from)
+ predicate = URIRef(edge.get("_uri", "")) or e_col_uri
+ object = self.__term_map.get(_to) or self.__process_missing_adb_doc(_to)
+
+ sg = URIRef(edge.get("_sub_graph_uri", "")) or None
+ if sg:
+ self.__subgraph_map[edge["_from"]] = sg
+ # self.__subgraph_map[edge["_to"]] = subgraph # TODO: REVISIT
+
+ # TODO: Revisit when rdflib introduces RDF-star support
+ # edge_uri = (subject, predicate, object, sg)
+ edge_uri = URIRef(f"{self.__graph_ns}{edge['_key']}")
+ self.__unpack_adb_doc(edge, edge_uri, sg)
+
+ if (
+ len(edge.keys() - self.adb_key_blacklist) != 0
+ or edge_is_referenced_by_another_edge
+ or self.__include_adb_key_statements
+ ):
+ self.__reify_rdf_triple(
+ edge_uri, subject, predicate, object, edge["_key"], sg
+ )
+
+ elif (edge_uri, None, None) not in self.rdf_graph:
+ self.__add_to_rdf_graph(subject, predicate, object, sg)
+
+ def __reify_rdf_triple(
+ self,
+ edge_uri: URIRef,
+ s: RDFTerm,
+ p: URIRef,
+ o: RDFTerm,
+ adb_key: str,
+ sg: Optional[URIRef] = None,
+ ) -> None:
+ """Performs triple reification for the given RDF triple
+
+ Due to rdflib's missing support for RDF-star, triple reification
+ is introduced as a workaround to support transforming ArangoDB Edges
+ into RDF Statements without losing any edge properties.
+
+ :param edge_uri: The URIRef representing the ArangoDB Edge,
+ soon to be transformed into an RDF Statement.
+ :type edge_uri: URIRef
+ :param s: The RDF Subject of the RDF Statement.
+ :type s: URIRef | BNode
+ :param p: The RDF Predicate of the RDF Statement.
+ :type p: URIRef
+ :param o: The RDF Object of the RDF Statement.
+ :type o: URIRef | BNode | Literal
+ :param sg: The Sub Graph URI of the (s,p,o) statement, if any.
+ :type sg: URIRef | None
+ """
+ # Triple reification overwrites existing triple (if any)
+ self.rdf_graph.remove((s, p, o))
+
+ self.__add_to_rdf_graph(edge_uri, RDF.type, RDF.Statement, sg)
+ self.__add_to_rdf_graph(edge_uri, RDF.subject, s, sg)
+ self.__add_to_rdf_graph(edge_uri, RDF.predicate, p, sg)
+ self.__add_to_rdf_graph(edge_uri, RDF.object, o, sg)
+ self.__add_to_rdf_graph(edge_uri, self.adb_key_uri, Literal(adb_key), sg)
+
+ def __unpack_adb_doc(self, doc: Json, term: RDFTerm, sg: Optional[URIRef]) -> None:
+ """An ArangoDB-to-RDF helper method to transfer the ArangoDB
+ Document Properties of **doc** into the RDF Graph, as triples.
+
+ :param doc: The ArangoDB Document JSON
+ :type doc: Dict[str, Any]
+ :param term: The RDF representation of **doc**
+ :type term: URIRef | BNode | Literal
+ :param sg: The Sub Graph URI of **doc**, if any.
+ :type sg: URIRef | None
+ :return: Returns True if the ArangoDB Document has property data.
+ :rtype: bool
+ """
+ # TODO: Iterate through metagraph values instead?
+ for k in doc.keys() - self.adb_key_blacklist:
+ val = doc[k]
+ p = self.__uri_map.get(k, URIRef(f"{self.__graph_ns}{k}"))
+ self.__adb_val_to_rdf_val(term, p, val, sg)
+
+ def __add_to_rdf_graph(
+ self, s: RDFTerm, p: URIRef, o: RDFTerm, sg: Optional[URIRef] = None
+ ) -> None:
+ """Another ArangoDB-to-RDF helper method used to insert the statement
+ (s,p,o) into the RDF Graph as a Triple or Quad, depending on if a
+ Sub Graph URI is specified.
+
+ :param s: The RDF Subject object of the (s,p,o) statement.
+ :type s: URIRef | BNode
+ :param p: The RDF Predicate object of the (s,p,o) statement.
+ :type p: URIRef
+ :param o: The RDF Object object of the (s,p,o) statement.
+ :type o: URIRef | BNode | Literal
+ :param sg: The Sub Graph URI of the (s,p,o) statement, if any.
+ :type sg: URIRef | None
+ """
+ t = (s, p, o, sg) if sg and self.__graph_supports_quads else (s, p, o)
+ self.rdf_graph.add(t)
+
+ def __adb_val_to_rdf_val(
+ self, s: RDFTerm, p: URIRef, val: Any, sg: Optional[URIRef] = None
+ ) -> None:
+ """A helper function used to insert an arbitrary ArangoDB
+ document property value as an RDF Object in some RDF Statement.
+
+ If the ArangoDB document property **val** is of type list
+ or dict, then a recursive process is introduced to unpack
+ the ArangoDB document property into multiple RDF Statements.
+
+ Otherwise, the ArangoDB Document Property is treated as
+ a Literal in the context of RDF.
+
+ :param s: The RDF Subject of the to-be-inserted RDF Statement.
+ :type s: URIRef | BNode
+ :param p: The RDF Predicate of the to-be-inserted RDF Statement.
+ This represents the ArangoDB Document Property key name.
+ :type p: URIRef
+ :param sub_key: The ArangoDB property key of the document
+ that will be used to store the value.
+ :type sub_key: str
+ :param val: Some RDF value to insert.
+ :type val: Any
+ :param sg: The Sub Graph URI of the (s,p,val) statement, if any.
+ :type sg: URIRef | None
+ """
+
+ if type(val) is list:
+ if self.__list_conversion == "collection":
+ node: RDFTerm = BNode()
+ self.__add_to_rdf_graph(s, p, node, sg)
+
+ rest: RDFTerm
+ for i, v in enumerate(val):
+ self.__adb_val_to_rdf_val(node, RDF.first, v)
+
+ rest = RDF.nil if i == len(val) - 1 else BNode()
+ self.__add_to_rdf_graph(node, RDF.rest, rest, sg)
+ node = rest
+
+ elif self.__list_conversion == "container":
+ bnode = BNode()
+ self.__add_to_rdf_graph(s, p, bnode, sg)
+
+ for i, v in enumerate(val, 1):
+ _n = URIRef(f"{RDF}_{i}")
+ self.__adb_val_to_rdf_val(bnode, _n, v, sg)
+
+ elif self.__list_conversion == "static":
+ for v in val:
+ self.__adb_val_to_rdf_val(s, p, v, sg)
+
+ else:
+ raise ValueError("Invalid **list_conversion_mode** value")
+
+ elif type(val) is dict:
+ bnode = BNode()
+ self.__add_to_rdf_graph(s, p, bnode, sg)
+
+ for k, v in val.items():
+ p = self.__uri_map.get(k, URIRef(f"{self.__graph_ns}{k}"))
+ self.__adb_val_to_rdf_val(bnode, p, v, sg)
+
+ else:
+ # TODO: Datatype? Lang?
+ self.__add_to_rdf_graph(s, p, Literal(val), sg)
+
+ def __fetch_adb_docs(self, adb_col: str, export_options: Any) -> Result[Cursor]:
+ """Fetches ArangoDB documents within a collection.
+
+ :param adb_col: The ArangoDB collection.
+ :type adb_col: str
+ :param export_options: Keyword arguments to specify AQL query options
+ when fetching documents from the ArangoDB instance.
+ :type export_options: Any
+ :return: Result cursor.
+ :rtype: arango.cursor.Cursor
+ """
+ action = f"ArangoDB Export: {adb_col}"
+ adb_task = self.__adb_iterator.add_task("", action=action)
+
+ # TODO: Return **doc** attributes based on **metagraph**
+ aql = f"FOR doc IN {adb_col} RETURN doc"
+ cursor = self.db.aql.execute(aql, stream=True, **export_options)
+
+ self.__adb_iterator.stop_task(adb_task)
+ self.__adb_iterator.update(adb_task, visible=True)
+
+ return cursor
+
+ ###################################################################################
+ # RDF to ArangoDB & ArangoDB to RDF Shared Methods
+ # * __add_to_adb_mapping:
+ ###################################################################################
+
+ def __add_to_adb_mapping(
+ self,
+ subject: RDFTerm,
+ adb_col: str,
+ overwrite: bool = False,
+ ) -> None:
+ """Add a statement to **self.adb_mapping** of the form
+ (subject, URIRef("http://www.arangodb.com/collection"), Literal(adb_col)) .
+
+ :param subject: The RDF Subject.
+ :type subject: URIRef | BNode
+ :param adb_col: The ArangoDB Collection name.
+ :type adb_col: str
+ :param overwrite: If True, delete any existing statements of
+ the form (s, URIRef("http://www.arangodb.com/collection"), None).
+ Defaults to False.
+ :type overwrite: bool
"""
- cursor = self.db.aql.execute(aql, bind_vars={"key": key, "val": val})
- if cursor.empty():
- sys.exit("No configuration found")
+ if overwrite:
+ self.adb_mapping.remove((subject, self.adb_col_uri, None))
- return cursor.pop()
+ self.adb_mapping.add((subject, self.adb_col_uri, Literal(adb_col)))
diff --git a/arango_rdf/meta/adb.trig b/arango_rdf/meta/adb.trig
new file mode 100644
index 00000000..7cc48ade
--- /dev/null
+++ b/arango_rdf/meta/adb.trig
@@ -0,0 +1,12 @@
+@prefix rdf: .
+@prefix owl: .
+@prefix dc: .
+@prefix adb: .
+
+adb: {
+ a owl:Ontology ;
+ dc:title "The ArangoDB Schema vocabulary (ADB)" .
+
+ adb:collection a rdf:Property .
+ adb:key a rdf:Property .
+}
diff --git a/arango_rdf/meta/dc.trig b/arango_rdf/meta/dc.trig
new file mode 100644
index 00000000..5138b1e0
--- /dev/null
+++ b/arango_rdf/meta/dc.trig
@@ -0,0 +1,28 @@
+@prefix dc: .
+@prefix rdf: .
+@prefix rdfs: .
+@prefix owl: .
+
+dc: {
+ a owl:Ontology ;
+ dc:title "The Dublin Core concepts vocabulary (DC)" .
+
+ dc:title a rdf:Property ;
+ rdfs:comment "A name given to the resource." ;
+ rdfs:isDefinedBy ;
+ rdfs:label "Title" ;
+ rdfs:range rdfs:Literal .
+
+ dc:date a rdf:Property ;
+ dc:description "Date may be used to express temporal information at any level of granularity. Recommended practice is to express the date, date/time, or period of time according to ISO 8601-1 [[ISO 8601-1](https://www.iso.org/iso-8601-date-and-time-format.html)] or a published profile of the ISO standard, such as the W3C Note on Date and Time Formats [[W3CDTF](https://www.w3.org/TR/NOTE-datetime)] or the Extended Date/Time Format Specification [[EDTF](http://www.loc.gov/standards/datetime/)]. If the full date is unknown, month and year (YYYY-MM) or just year (YYYY) may be used. Date ranges may be specified using ISO 8601 period of time specification in which start and end dates are separated by a '/' (slash) character. Either the start or end date may be missing." ;
+ rdfs:comment "A point or period of time associated with an event in the lifecycle of the resource." ;
+ rdfs:isDefinedBy ;
+ rdfs:label "Date" ;
+ rdfs:range rdfs:Literal .
+
+ dc:description a rdf:Property ;
+ dc:description "Description may include but is not limited to: an abstract, a table of contents, a graphical representation, or a free-text account of the resource." ;
+ rdfs:comment "An account of the resource." ;
+ rdfs:isDefinedBy ;
+ rdfs:label "Description" .
+}
\ No newline at end of file
diff --git a/arango_rdf/meta/owl.trig b/arango_rdf/meta/owl.trig
new file mode 100644
index 00000000..473c1b01
--- /dev/null
+++ b/arango_rdf/meta/owl.trig
@@ -0,0 +1,552 @@
+@prefix dc: .
+@prefix grddl: .
+@prefix owl: .
+@prefix rdf: .
+@prefix rdfs: .
+@prefix xml: .
+@prefix xsd: .
+
+owl: {
+ a owl:Ontology ;
+ dc:title "The OWL 2 Schema vocabulary (OWL 2)" ;
+ rdfs:comment """
+ This ontology partially describes the built-in classes and
+ properties that together form the basis of the RDF/XML syntax of OWL 2.
+ The content of this ontology is based on Tables 6.1 and 6.2
+ in Section 6.4 of the OWL 2 RDF-Based Semantics specification,
+ available at http://www.w3.org/TR/owl2-rdf-based-semantics/.
+ Please note that those tables do not include the different annotations
+ (labels, comments and rdfs:isDefinedBy links) used in this file.
+ Also note that the descriptions provided in this ontology do not
+ provide a complete and correct formal description of either the syntax
+ or the semantics of the introduced terms (please see the OWL 2
+ recommendations for the complete and normative specifications).
+ Furthermore, the information provided by this ontology may be
+ misleading if not used with care. This ontology SHOULD NOT be imported
+ into OWL ontologies. Importing this file into an OWL 2 DL ontology
+ will cause it to become an OWL 2 Full ontology and may have other,
+ unexpected, consequences.
+ """ ;
+ rdfs:isDefinedBy
+ ,
+ ,
+ ;
+ rdfs:seeAlso ,
+ ;
+ owl:imports ;
+ owl:versionIRI ;
+ owl:versionInfo "$Date: 2009/11/15 10:54:12 $" .
+
+
+ owl:AllDifferent a rdfs:Class ;
+ rdfs:label "AllDifferent" ;
+ rdfs:comment "The class of collections of pairwise different individuals." ;
+ rdfs:isDefinedBy ;
+ rdfs:subClassOf rdfs:Resource .
+
+ owl:AllDisjointClasses a rdfs:Class ;
+ rdfs:label "AllDisjointClasses" ;
+ rdfs:comment "The class of collections of pairwise disjoint classes." ;
+ rdfs:isDefinedBy ;
+ rdfs:subClassOf rdfs:Resource .
+
+ owl:AllDisjointProperties a rdfs:Class ;
+ rdfs:label "AllDisjointProperties" ;
+ rdfs:comment "The class of collections of pairwise disjoint properties." ;
+ rdfs:isDefinedBy ;
+ rdfs:subClassOf rdfs:Resource .
+
+ owl:Annotation a rdfs:Class ;
+ rdfs:label "Annotation" ;
+ rdfs:comment "The class of annotated annotations for which the RDF serialization consists of an annotated subject, predicate and object." ;
+ rdfs:isDefinedBy ;
+ rdfs:subClassOf rdfs:Resource .
+
+ owl:AnnotationProperty a rdfs:Class ;
+ rdfs:label "AnnotationProperty" ;
+ rdfs:comment "The class of annotation properties." ;
+ rdfs:isDefinedBy ;
+ rdfs:subClassOf rdf:Property .
+
+ owl:AsymmetricProperty a rdfs:Class ;
+ rdfs:label "AsymmetricProperty" ;
+ rdfs:comment "The class of asymmetric properties." ;
+ rdfs:isDefinedBy ;
+ rdfs:subClassOf owl:ObjectProperty .
+
+ owl:Axiom a rdfs:Class ;
+ rdfs:label "Axiom" ;
+ rdfs:comment "The class of annotated axioms for which the RDF serialization consists of an annotated subject, predicate and object." ;
+ rdfs:isDefinedBy ;
+ rdfs:subClassOf rdfs:Resource .
+
+ owl:Class a rdfs:Class ;
+ rdfs:label "Class" ;
+ rdfs:comment "The class of OWL classes." ;
+ rdfs:isDefinedBy ;
+ rdfs:subClassOf rdfs:Class .
+
+ owl:DataRange a rdfs:Class ;
+ rdfs:label "DataRange" ;
+ rdfs:comment "The class of OWL data ranges, which are special kinds of datatypes. Note: The use of the IRI owl:DataRange has been deprecated as of OWL 2. The IRI rdfs:Datatype SHOULD be used instead." ;
+ rdfs:isDefinedBy ;
+ rdfs:subClassOf rdfs:Datatype .
+
+ owl:DatatypeProperty a rdfs:Class ;
+ rdfs:label "DatatypeProperty" ;
+ rdfs:comment "The class of data properties." ;
+ rdfs:isDefinedBy ;
+ rdfs:subClassOf rdf:Property .
+
+ owl:DeprecatedClass a rdfs:Class ;
+ rdfs:label "DeprecatedClass" ;
+ rdfs:comment "The class of deprecated classes." ;
+ rdfs:isDefinedBy ;
+ rdfs:subClassOf rdfs:Class .
+
+ owl:DeprecatedProperty a rdfs:Class ;
+ rdfs:label "DeprecatedProperty" ;
+ rdfs:comment "The class of deprecated properties." ;
+ rdfs:isDefinedBy ;
+ rdfs:subClassOf rdf:Property .
+
+ owl:FunctionalProperty a rdfs:Class ;
+ rdfs:label "FunctionalProperty" ;
+ rdfs:comment "The class of functional properties." ;
+ rdfs:isDefinedBy ;
+ rdfs:subClassOf rdf:Property .
+
+ owl:InverseFunctionalProperty a rdfs:Class ;
+ rdfs:label "InverseFunctionalProperty" ;
+ rdfs:comment "The class of inverse-functional properties." ;
+ rdfs:isDefinedBy ;
+ rdfs:subClassOf owl:ObjectProperty .
+
+ owl:IrreflexiveProperty a rdfs:Class ;
+ rdfs:label "IrreflexiveProperty" ;
+ rdfs:comment "The class of irreflexive properties." ;
+ rdfs:isDefinedBy ;
+ rdfs:subClassOf owl:ObjectProperty .
+
+ owl:NamedIndividual a rdfs:Class ;
+ rdfs:label "NamedIndividual" ;
+ rdfs:comment "The class of named individuals." ;
+ rdfs:isDefinedBy ;
+ rdfs:subClassOf owl:Thing .
+
+ owl:NegativePropertyAssertion a rdfs:Class ;
+ rdfs:label "NegativePropertyAssertion" ;
+ rdfs:comment "The class of negative property assertions." ;
+ rdfs:isDefinedBy ;
+ rdfs:subClassOf rdfs:Resource .
+
+ owl:Nothing a owl:Class ;
+ rdfs:label "Nothing" ;
+ rdfs:comment "This is the empty class." ;
+ rdfs:isDefinedBy ;
+ rdfs:subClassOf owl:Thing .
+
+ owl:ObjectProperty a rdfs:Class ;
+ rdfs:label "ObjectProperty" ;
+ rdfs:comment "The class of object properties." ;
+ rdfs:isDefinedBy ;
+ rdfs:subClassOf rdf:Property .
+
+ owl:Ontology a rdfs:Class ;
+ rdfs:label "Ontology" ;
+ rdfs:comment "The class of ontologies." ;
+ rdfs:isDefinedBy ;
+ rdfs:subClassOf rdfs:Resource .
+
+ owl:OntologyProperty a rdfs:Class ;
+ rdfs:label "OntologyProperty" ;
+ rdfs:comment "The class of ontology properties." ;
+ rdfs:isDefinedBy ;
+ rdfs:subClassOf rdf:Property .
+
+ owl:ReflexiveProperty a rdfs:Class ;
+ rdfs:label "ReflexiveProperty" ;
+ rdfs:comment "The class of reflexive properties." ;
+ rdfs:isDefinedBy ;
+ rdfs:subClassOf owl:ObjectProperty .
+
+ owl:Restriction a rdfs:Class ;
+ rdfs:label "Restriction" ;
+ rdfs:comment "The class of property restrictions." ;
+ rdfs:isDefinedBy ;
+ rdfs:subClassOf owl:Class .
+
+ owl:SymmetricProperty a rdfs:Class ;
+ rdfs:label "SymmetricProperty" ;
+ rdfs:comment "The class of symmetric properties." ;
+ rdfs:isDefinedBy ;
+ rdfs:subClassOf owl:ObjectProperty .
+
+ owl:TransitiveProperty a rdfs:Class ;
+ rdfs:label "TransitiveProperty" ;
+ rdfs:comment "The class of transitive properties." ;
+ rdfs:isDefinedBy ;
+ rdfs:subClassOf owl:ObjectProperty .
+
+ owl:Thing a owl:Class ;
+ rdfs:label "Thing" ;
+ rdfs:comment "The class of OWL individuals." ;
+ rdfs:isDefinedBy .
+
+ owl:allValuesFrom a rdf:Property ;
+ rdfs:label "allValuesFrom" ;
+ rdfs:comment "The property that determines the class that a universal property restriction refers to." ;
+ rdfs:domain owl:Restriction ;
+ rdfs:isDefinedBy ;
+ rdfs:range rdfs:Class .
+
+ owl:annotatedProperty a rdf:Property ;
+ rdfs:label "annotatedProperty" ;
+ rdfs:comment "The property that determines the predicate of an annotated axiom or annotated annotation." ;
+ rdfs:domain rdfs:Resource ;
+ rdfs:isDefinedBy ;
+ rdfs:range rdfs:Resource .
+
+ owl:annotatedSource a rdf:Property ;
+ rdfs:label "annotatedSource" ;
+ rdfs:comment "The property that determines the subject of an annotated axiom or annotated annotation." ;
+ rdfs:domain rdfs:Resource ;
+ rdfs:isDefinedBy ;
+ rdfs:range rdfs:Resource .
+
+ owl:annotatedTarget a rdf:Property ;
+ rdfs:label "annotatedTarget" ;
+ rdfs:comment "The property that determines the object of an annotated axiom or annotated annotation." ;
+ rdfs:domain rdfs:Resource ;
+ rdfs:isDefinedBy ;
+ rdfs:range rdfs:Resource .
+
+ owl:assertionProperty a rdf:Property ;
+ rdfs:label "assertionProperty" ;
+ rdfs:comment "The property that determines the predicate of a negative property assertion." ;
+ rdfs:domain owl:NegativePropertyAssertion ;
+ rdfs:isDefinedBy ;
+ rdfs:range rdf:Property .
+
+ owl:backwardCompatibleWith a owl:AnnotationProperty, owl:OntologyProperty ;
+ rdfs:label "backwardCompatibleWith" ;
+ rdfs:comment "The annotation property that indicates that a given ontology is backward compatible with another ontology." ;
+ rdfs:domain owl:Ontology ;
+ rdfs:isDefinedBy ;
+ rdfs:range owl:Ontology .
+
+ owl:bottomDataProperty a owl:DatatypeProperty ;
+ rdfs:label "bottomDataProperty" ;
+ rdfs:comment "The data property that does not relate any individual to any data value." ;
+ rdfs:domain owl:Thing ;
+ rdfs:isDefinedBy ;
+ rdfs:range rdfs:Literal .
+
+ owl:bottomObjectProperty a owl:ObjectProperty ;
+ rdfs:label "bottomObjectProperty" ;
+ rdfs:comment "The object property that does not relate any two individuals." ;
+ rdfs:domain owl:Thing ;
+ rdfs:isDefinedBy ;
+ rdfs:range owl:Thing .
+
+ owl:cardinality a rdf:Property ;
+ rdfs:label "cardinality" ;
+ rdfs:comment "The property that determines the cardinality of an exact cardinality restriction." ;
+ rdfs:domain owl:Restriction ;
+ rdfs:isDefinedBy ;
+ rdfs:range xsd:nonNegativeInteger .
+
+ owl:complementOf a rdf:Property ;
+ rdfs:label "complementOf" ;
+ rdfs:comment "The property that determines that a given class is the complement of another class." ;
+ rdfs:domain owl:Class ;
+ rdfs:isDefinedBy ;
+ rdfs:range owl:Class .
+
+ owl:datatypeComplementOf a rdf:Property ;
+ rdfs:label "datatypeComplementOf" ;
+ rdfs:comment "The property that determines that a given data range is the complement of another data range with respect to the data domain." ;
+ rdfs:domain rdfs:Datatype ;
+ rdfs:isDefinedBy ;
+ rdfs:range rdfs:Datatype .
+
+ owl:deprecated a owl:AnnotationProperty ;
+ rdfs:label "deprecated" ;
+ rdfs:comment "The annotation property that indicates that a given entity has been deprecated." ;
+ rdfs:domain rdfs:Resource ;
+ rdfs:isDefinedBy ;
+ rdfs:range rdfs:Resource .
+
+ owl:differentFrom a rdf:Property ;
+ rdfs:label "differentFrom" ;
+ rdfs:comment "The property that determines that two given individuals are different." ;
+ rdfs:domain owl:Thing ;
+ rdfs:isDefinedBy ;
+ rdfs:range owl:Thing .
+
+ owl:disjointUnionOf a rdf:Property ;
+ rdfs:label "disjointUnionOf" ;
+ rdfs:comment "The property that determines that a given class is equivalent to the disjoint union of a collection of other classes." ;
+ rdfs:domain owl:Class ;
+ rdfs:isDefinedBy ;
+ rdfs:range rdf:List .
+
+ owl:disjointWith a rdf:Property ;
+ rdfs:label "disjointWith" ;
+ rdfs:comment "The property that determines that two given classes are disjoint." ;
+ rdfs:domain owl:Class ;
+ rdfs:isDefinedBy ;
+ rdfs:range owl:Class .
+
+ owl:distinctMembers a rdf:Property ;
+ rdfs:label "distinctMembers" ;
+ rdfs:comment "The property that determines the collection of pairwise different individuals in a owl:AllDifferent axiom." ;
+ rdfs:domain owl:AllDifferent ;
+ rdfs:isDefinedBy ;
+ rdfs:range rdf:List .
+
+ owl:equivalentClass a rdf:Property ;
+ rdfs:label "equivalentClass" ;
+ rdfs:comment "The property that determines that two given classes are equivalent, and that is used to specify datatype definitions." ;
+ rdfs:domain rdfs:Class ;
+ rdfs:isDefinedBy ;
+ rdfs:range rdfs:Class .
+
+ owl:equivalentProperty a rdf:Property ;
+ rdfs:label "equivalentProperty" ;
+ rdfs:comment "The property that determines that two given properties are equivalent." ;
+ rdfs:domain rdf:Property ;
+ rdfs:isDefinedBy ;
+ rdfs:range rdf:Property .
+
+ owl:hasKey a rdf:Property ;
+ rdfs:label "hasKey" ;
+ rdfs:comment "The property that determines the collection of properties that jointly build a key." ;
+ rdfs:domain owl:Class ;
+ rdfs:isDefinedBy ;
+ rdfs:range rdf:List .
+
+ owl:hasSelf a rdf:Property ;
+ rdfs:label "hasSelf" ;
+ rdfs:comment "The property that determines the property that a self restriction refers to." ;
+ rdfs:domain owl:Restriction ;
+ rdfs:isDefinedBy ;
+ rdfs:range rdfs:Resource .
+
+ owl:hasValue a rdf:Property ;
+ rdfs:label "hasValue" ;
+ rdfs:comment "The property that determines the individual that a has-value restriction refers to." ;
+ rdfs:domain owl:Restriction ;
+ rdfs:isDefinedBy ;
+ rdfs:range rdfs:Resource .
+
+ owl:imports a owl:OntologyProperty ;
+ rdfs:label "imports" ;
+ rdfs:comment "The property that is used for importing other ontologies into a given ontology." ;
+ rdfs:domain owl:Ontology ;
+ rdfs:isDefinedBy ;
+ rdfs:range owl:Ontology .
+
+ owl:incompatibleWith a owl:AnnotationProperty, owl:OntologyProperty ;
+ rdfs:label "incompatibleWith" ;
+ rdfs:comment "The annotation property that indicates that a given ontology is incompatible with another ontology." ;
+ rdfs:domain owl:Ontology ;
+ rdfs:isDefinedBy ;
+ rdfs:range owl:Ontology .
+
+ owl:intersectionOf a rdf:Property ;
+ rdfs:label "intersectionOf" ;
+ rdfs:comment "The property that determines the collection of classes or data ranges that build an intersection." ;
+ rdfs:domain rdfs:Class ;
+ rdfs:isDefinedBy ;
+ rdfs:range rdf:List .
+
+ owl:inverseOf a rdf:Property ;
+ rdfs:label "inverseOf" ;
+ rdfs:comment "The property that determines that two given properties are inverse." ;
+ rdfs:domain owl:ObjectProperty ;
+ rdfs:isDefinedBy ;
+ rdfs:range owl:ObjectProperty .
+
+ owl:maxCardinality a rdf:Property ;
+ rdfs:label "maxCardinality" ;
+ rdfs:comment "The property that determines the cardinality of a maximum cardinality restriction." ;
+ rdfs:domain owl:Restriction ;
+ rdfs:isDefinedBy ;
+ rdfs:range xsd:nonNegativeInteger .
+
+ owl:maxQualifiedCardinality a rdf:Property ;
+ rdfs:label "maxQualifiedCardinality" ;
+ rdfs:comment "The property that determines the cardinality of a maximum qualified cardinality restriction." ;
+ rdfs:domain owl:Restriction ;
+ rdfs:isDefinedBy ;
+ rdfs:range xsd:nonNegativeInteger .
+
+ owl:members a rdf:Property ;
+ rdfs:label "members" ;
+ rdfs:comment "The property that determines the collection of members in either a owl:AllDifferent, owl:AllDisjointClasses or owl:AllDisjointProperties axiom." ;
+ rdfs:domain rdfs:Resource ;
+ rdfs:isDefinedBy ;
+ rdfs:range rdf:List .
+
+ owl:minCardinality a rdf:Property ;
+ rdfs:label "minCardinality" ;
+ rdfs:comment "The property that determines the cardinality of a minimum cardinality restriction." ;
+ rdfs:domain owl:Restriction ;
+ rdfs:isDefinedBy ;
+ rdfs:range xsd:nonNegativeInteger .
+
+ owl:minQualifiedCardinality a rdf:Property ;
+ rdfs:label "minQualifiedCardinality" ;
+ rdfs:comment "The property that determines the cardinality of a minimum qualified cardinality restriction." ;
+ rdfs:domain owl:Restriction ;
+ rdfs:isDefinedBy ;
+ rdfs:range xsd:nonNegativeInteger .
+
+ owl:onClass a rdf:Property ;
+ rdfs:label "onClass" ;
+ rdfs:comment "The property that determines the class that a qualified object cardinality restriction refers to." ;
+ rdfs:domain owl:Restriction ;
+ rdfs:isDefinedBy ;
+ rdfs:range owl:Class .
+
+ owl:onDataRange a rdf:Property ;
+ rdfs:label "onDataRange" ;
+ rdfs:comment "The property that determines the data range that a qualified data cardinality restriction refers to." ;
+ rdfs:domain owl:Restriction ;
+ rdfs:isDefinedBy ;
+ rdfs:range rdfs:Datatype .
+
+ owl:onDatatype a rdf:Property ;
+ rdfs:label "onDatatype" ;
+ rdfs:comment "The property that determines the datatype that a datatype restriction refers to." ;
+ rdfs:domain rdfs:Datatype ;
+ rdfs:isDefinedBy ;
+ rdfs:range rdfs:Datatype .
+
+ owl:oneOf a rdf:Property ;
+ rdfs:label "oneOf" ;
+ rdfs:comment "The property that determines the collection of individuals or data values that build an enumeration." ;
+ rdfs:domain rdfs:Class ;
+ rdfs:isDefinedBy ;
+ rdfs:range rdf:List .
+
+ owl:onProperties a rdf:Property ;
+ rdfs:label "onProperties" ;
+ rdfs:comment "The property that determines the n-tuple of properties that a property restriction on an n-ary data range refers to." ;
+ rdfs:domain owl:Restriction ;
+ rdfs:isDefinedBy ;
+ rdfs:range rdf:List .
+
+ owl:onProperty a rdf:Property ;
+ rdfs:label "onProperty" ;
+ rdfs:comment "The property that determines the property that a property restriction refers to." ;
+ rdfs:domain owl:Restriction ;
+ rdfs:isDefinedBy ;
+ rdfs:range rdf:Property .
+
+ owl:priorVersion a owl:AnnotationProperty, owl:OntologyProperty ;
+ rdfs:label "priorVersion" ;
+ rdfs:comment "The annotation property that indicates the predecessor ontology of a given ontology." ;
+ rdfs:domain owl:Ontology ;
+ rdfs:isDefinedBy ;
+ rdfs:range owl:Ontology .
+
+ owl:propertyChainAxiom a rdf:Property ;
+ rdfs:label "propertyChainAxiom" ;
+ rdfs:comment "The property that determines the n-tuple of properties that build a sub property chain of a given property." ;
+ rdfs:domain owl:ObjectProperty ;
+ rdfs:isDefinedBy ;
+ rdfs:range rdf:List .
+
+ owl:propertyDisjointWith a rdf:Property ;
+ rdfs:label "propertyDisjointWith" ;
+ rdfs:comment "The property that determines that two given properties are disjoint." ;
+ rdfs:domain rdf:Property ;
+ rdfs:isDefinedBy ;
+ rdfs:range rdf:Property .
+
+ owl:qualifiedCardinality a rdf:Property ;
+ rdfs:label "qualifiedCardinality" ;
+ rdfs:comment "The property that determines the cardinality of an exact qualified cardinality restriction." ;
+ rdfs:domain owl:Restriction ;
+ rdfs:isDefinedBy ;
+ rdfs:range xsd:nonNegativeInteger .
+
+ owl:sameAs a rdf:Property ;
+ rdfs:label "sameAs" ;
+ rdfs:comment "The property that determines that two given individuals are equal." ;
+ rdfs:domain owl:Thing ;
+ rdfs:isDefinedBy ;
+ rdfs:range owl:Thing .
+
+ owl:someValuesFrom a rdf:Property ;
+ rdfs:label "someValuesFrom" ;
+ rdfs:comment "The property that determines the class that an existential property restriction refers to." ;
+ rdfs:domain owl:Restriction ;
+ rdfs:isDefinedBy ;
+ rdfs:range rdfs:Class .
+
+ owl:sourceIndividual a rdf:Property ;
+ rdfs:label "sourceIndividual" ;
+ rdfs:comment "The property that determines the subject of a negative property assertion." ;
+ rdfs:domain owl:NegativePropertyAssertion ;
+ rdfs:isDefinedBy ;
+ rdfs:range owl:Thing .
+
+ owl:targetIndividual a rdf:Property ;
+ rdfs:label "targetIndividual" ;
+ rdfs:comment "The property that determines the object of a negative object property assertion." ;
+ rdfs:domain owl:NegativePropertyAssertion ;
+ rdfs:isDefinedBy ;
+ rdfs:range owl:Thing .
+
+ owl:targetValue a rdf:Property ;
+ rdfs:label "targetValue" ;
+ rdfs:comment "The property that determines the value of a negative data property assertion." ;
+ rdfs:domain owl:NegativePropertyAssertion ;
+ rdfs:isDefinedBy ;
+ rdfs:range rdfs:Literal .
+
+ owl:topDataProperty a owl:DatatypeProperty ;
+ rdfs:label "topDataProperty" ;
+ rdfs:comment "The data property that relates every individual to every data value." ;
+ rdfs:domain owl:Thing ;
+ rdfs:isDefinedBy ;
+ rdfs:range rdfs:Literal .
+
+ owl:topObjectProperty a owl:ObjectProperty ;
+ rdfs:label "topObjectProperty" ;
+ rdfs:comment "The object property that relates every two individuals." ;
+ rdfs:domain owl:Thing ;
+ rdfs:isDefinedBy ;
+ rdfs:range owl:Thing .
+
+ owl:unionOf a rdf:Property ;
+ rdfs:label "unionOf" ;
+ rdfs:comment "The property that determines the collection of classes or data ranges that build a union." ;
+ rdfs:domain rdfs:Class ;
+ rdfs:isDefinedBy ;
+ rdfs:range rdf:List .
+
+ owl:versionInfo a owl:AnnotationProperty ;
+ rdfs:label "versionInfo" ;
+ rdfs:comment "The annotation property that provides version information for an ontology or another OWL construct." ;
+ rdfs:domain rdfs:Resource ;
+ rdfs:isDefinedBy ;
+ rdfs:range rdfs:Resource .
+
+ owl:versionIRI a owl:OntologyProperty ;
+ rdfs:label "versionIRI" ;
+ rdfs:comment "The property that identifies the version IRI of an ontology." ;
+ rdfs:domain owl:Ontology ;
+ rdfs:isDefinedBy ;
+ rdfs:range owl:Ontology .
+
+ owl:withRestrictions a rdf:Property ;
+ rdfs:label "withRestrictions" ;
+ rdfs:comment "The property that determines the collection of facet-value pairs that define a datatype restriction." ;
+ rdfs:domain rdfs:Datatype ;
+ rdfs:isDefinedBy ;
+ rdfs:range rdf:List .
+}
\ No newline at end of file
diff --git a/arango_rdf/meta/rdf.trig b/arango_rdf/meta/rdf.trig
new file mode 100644
index 00000000..3ed16c17
--- /dev/null
+++ b/arango_rdf/meta/rdf.trig
@@ -0,0 +1,151 @@
+@prefix rdf: .
+@prefix rdfs: .
+@prefix owl: .
+@prefix dc: .
+
+rdf: {
+ a owl:Ontology ;
+ dc:title "The RDF Concepts Vocabulary (RDF)" ;
+ dc:date "2019-12-16" ;
+ dc:description "This is the RDF Schema for the RDF vocabulary terms in the RDF Namespace, defined in RDF 1.1 Concepts." .
+
+ rdf:HTML a rdfs:Datatype ;
+ rdfs:subClassOf rdfs:Literal ;
+ rdfs:isDefinedBy ;
+ rdfs:seeAlso ;
+ rdfs:label "HTML" ;
+ rdfs:comment "The datatype of RDF literals storing fragments of HTML content" .
+
+ rdf:langString a rdfs:Datatype ;
+ rdfs:subClassOf rdfs:Literal ;
+ rdfs:isDefinedBy ;
+ rdfs:seeAlso ;
+ rdfs:label "langString" ;
+ rdfs:comment "The datatype of language-tagged string values" .
+
+ rdf:PlainLiteral a rdfs:Datatype ;
+ rdfs:isDefinedBy ;
+ rdfs:subClassOf rdfs:Literal ;
+ rdfs:seeAlso ;
+ rdfs:label "PlainLiteral" ;
+ rdfs:comment "The class of plain (i.e. untyped) literal values, as used in RIF and OWL 2" .
+
+ rdf:type a rdf:Property ;
+ rdfs:isDefinedBy ;
+ rdfs:label "type" ;
+ rdfs:comment "The subject is an instance of a class." ;
+ rdfs:range rdfs:Class ;
+ rdfs:domain rdfs:Resource .
+
+ rdf:Property a rdfs:Class ;
+ rdfs:isDefinedBy ;
+ rdfs:label "Property" ;
+ rdfs:comment "The class of RDF properties." ;
+ rdfs:subClassOf rdfs:Resource .
+
+ rdf:Statement a rdfs:Class ;
+ rdfs:isDefinedBy ;
+ rdfs:label "Statement" ;
+ rdfs:subClassOf rdfs:Resource ;
+ rdfs:comment "The class of RDF statements." .
+
+ rdf:subject a rdf:Property ;
+ rdfs:isDefinedBy ;
+ rdfs:label "subject" ;
+ rdfs:comment "The subject of the subject RDF statement." ;
+ rdfs:domain rdf:Statement ;
+ rdfs:range rdfs:Resource .
+
+ rdf:predicate a rdf:Property ;
+ rdfs:isDefinedBy ;
+ rdfs:label "predicate" ;
+ rdfs:comment "The predicate of the subject RDF statement." ;
+ rdfs:domain rdf:Statement ;
+ rdfs:range rdfs:Resource .
+
+ rdf:object a rdf:Property ;
+ rdfs:isDefinedBy ;
+ rdfs:label "object" ;
+ rdfs:comment "The object of the subject RDF statement." ;
+ rdfs:domain rdf:Statement ;
+ rdfs:range rdfs:Resource .
+
+ rdf:Bag a rdfs:Class ;
+ rdfs:isDefinedBy ;
+ rdfs:label "Bag" ;
+ rdfs:comment "The class of unordered containers." ;
+ rdfs:subClassOf rdfs:Container .
+
+ rdf:Seq a rdfs:Class ;
+ rdfs:isDefinedBy ;
+ rdfs:label "Seq" ;
+ rdfs:comment "The class of ordered containers." ;
+ rdfs:subClassOf rdfs:Container .
+
+ rdf:Alt a rdfs:Class ;
+ rdfs:isDefinedBy ;
+ rdfs:label "Alt" ;
+ rdfs:comment "The class of containers of alternatives." ;
+ rdfs:subClassOf rdfs:Container .
+
+ rdf:value a rdf:Property ;
+ rdfs:isDefinedBy ;
+ rdfs:label "value" ;
+ rdfs:comment "Idiomatic property used for structured values." ;
+ rdfs:domain rdfs:Resource ;
+ rdfs:range rdfs:Resource .
+
+ rdf:List a rdfs:Class ;
+ rdfs:isDefinedBy ;
+ rdfs:label "List" ;
+ rdfs:comment "The class of RDF Lists." ;
+ rdfs:subClassOf rdfs:Resource .
+
+ rdf:first a rdf:Property ;
+ rdfs:isDefinedBy ;
+ rdfs:label "first" ;
+ rdfs:comment "The first item in the subject RDF list." ;
+ rdfs:domain rdf:List ;
+ rdfs:range rdfs:Resource .
+
+ rdf:rest a rdf:Property ;
+ rdfs:isDefinedBy ;
+ rdfs:label "rest" ;
+ rdfs:comment "The rest of the subject RDF list after the first item." ;
+ rdfs:domain rdf:List ;
+ rdfs:range rdf:List .
+
+ rdf:XMLLiteral a rdfs:Datatype ;
+ rdfs:subClassOf rdfs:Literal ;
+ rdfs:isDefinedBy ;
+ rdfs:label "XMLLiteral" ;
+ rdfs:comment "The datatype of XML literal values." .
+
+ rdf:JSON a rdfs:Datatype ;
+ rdfs:label "JSON" ;
+ rdfs:comment "The datatype of RDF literals storing JSON content." ;
+ rdfs:subClassOf rdfs:Literal ;
+ rdfs:isDefinedBy ;
+ rdfs:seeAlso .
+
+ rdf:CompoundLiteral a rdfs:Class ;
+ rdfs:label "CompoundLiteral" ;
+ rdfs:comment "A class representing a compound literal." ;
+ rdfs:subClassOf rdfs:Resource ;
+ rdfs:isDefinedBy ;
+ rdfs:seeAlso .
+
+ rdf:language a rdf:Property ;
+ rdfs:label "language" ;
+ rdfs:comment "The language component of a CompoundLiteral." ;
+ rdfs:domain rdf:CompoundLiteral ;
+ rdfs:isDefinedBy ;
+ rdfs:seeAlso .
+
+ rdf:direction a rdf:Property ;
+ rdfs:label "direction" ;
+ rdfs:comment "The base direction component of a CompoundLiteral." ;
+ rdfs:domain rdf:CompoundLiteral ;
+ rdfs:isDefinedBy ;
+ rdfs:seeAlso .
+}
\ No newline at end of file
diff --git a/arango_rdf/meta/rdfs.trig b/arango_rdf/meta/rdfs.trig
new file mode 100644
index 00000000..2fba1385
--- /dev/null
+++ b/arango_rdf/meta/rdfs.trig
@@ -0,0 +1,111 @@
+@prefix rdf: .
+@prefix rdfs: .
+@prefix owl: .
+@prefix dc: .
+
+rdfs: {
+ a owl:Ontology ;
+ dc:title "The RDF Schema vocabulary (RDFS)" .
+
+ rdfs:Resource a rdfs:Class ;
+ rdfs:isDefinedBy ;
+ rdfs:label "Resource" ;
+ rdfs:comment "The class resource, everything." .
+
+ rdfs:Class a rdfs:Class ;
+ rdfs:isDefinedBy ;
+ rdfs:label "Class" ;
+ rdfs:comment "The class of classes." ;
+ rdfs:subClassOf rdfs:Resource .
+
+ rdfs:subClassOf a rdf:Property ;
+ rdfs:isDefinedBy ;
+ rdfs:label "subClassOf" ;
+ rdfs:comment "The subject is a subclass of a class." ;
+ rdfs:range rdfs:Class ;
+ rdfs:domain rdfs:Class .
+
+ rdfs:subPropertyOf a rdf:Property ;
+ rdfs:isDefinedBy ;
+ rdfs:label "subPropertyOf" ;
+ rdfs:comment "The subject is a subproperty of a property." ;
+ rdfs:range rdf:Property ;
+ rdfs:domain rdf:Property .
+
+ rdfs:comment a rdf:Property ;
+ rdfs:isDefinedBy ;
+ rdfs:label "comment" ;
+ rdfs:comment "A description of the subject resource." ;
+ rdfs:domain rdfs:Resource ;
+ rdfs:range rdfs:Literal .
+
+ rdfs:label a rdf:Property ;
+ rdfs:isDefinedBy ;
+ rdfs:label "label" ;
+ rdfs:comment "A human-readable name for the subject." ;
+ rdfs:domain rdfs:Resource ;
+ rdfs:range rdfs:Literal .
+
+ rdfs:domain a rdf:Property ;
+ rdfs:isDefinedBy ;
+ rdfs:label "domain" ;
+ rdfs:comment "A domain of the subject property." ;
+ rdfs:range rdfs:Class ;
+ rdfs:domain rdf:Property .
+
+ rdfs:range a rdf:Property ;
+ rdfs:isDefinedBy ;
+ rdfs:label "range" ;
+ rdfs:comment "A range of the subject property." ;
+ rdfs:range rdfs:Class ;
+ rdfs:domain rdf:Property .
+
+ rdfs:seeAlso a rdf:Property ;
+ rdfs:isDefinedBy ;
+ rdfs:label "seeAlso" ;
+ rdfs:comment "Further information about the subject resource." ;
+ rdfs:range rdfs:Resource ;
+ rdfs:domain rdfs:Resource .
+
+ rdfs:isDefinedBy a rdf:Property ;
+ rdfs:isDefinedBy ;
+ rdfs:subPropertyOf rdfs:seeAlso ;
+ rdfs:label "isDefinedBy" ;
+ rdfs:comment "The defininition of the subject resource." ;
+ rdfs:range rdfs:Resource ;
+ rdfs:domain rdfs:Resource .
+
+ rdfs:Literal a rdfs:Class ;
+ rdfs:isDefinedBy ;
+ rdfs:label "Literal" ;
+ rdfs:comment "The class of literal values, eg. textual strings and integers." ;
+ rdfs:subClassOf rdfs:Resource .
+
+ rdfs:Container a rdfs:Class ;
+ rdfs:isDefinedBy ;
+ rdfs:label "Container" ;
+ rdfs:subClassOf rdfs:Resource ;
+ rdfs:comment "The class of RDF containers." .
+
+ rdfs:ContainerMembershipProperty a rdfs:Class ;
+ rdfs:isDefinedBy ;
+ rdfs:label "ContainerMembershipProperty" ;
+ rdfs:comment """The class of container membership properties, rdf:_1, rdf:_2, ...,
+ all of which are sub-properties of 'member'.""" ;
+ rdfs:subClassOf rdf:Property .
+
+ rdfs:member a rdf:Property ;
+ rdfs:isDefinedBy ;
+ rdfs:label "member" ;
+ rdfs:comment "A member of the subject resource." ;
+ rdfs:domain rdfs:Resource ;
+ rdfs:range rdfs:Resource .
+
+ rdfs:Datatype a rdfs:Class ;
+ rdfs:isDefinedBy ;
+ rdfs:label "Datatype" ;
+ rdfs:comment "The class of RDF datatypes." ;
+ rdfs:subClassOf rdfs:Class .
+
+ rdfs:seeAlso .
+}
\ No newline at end of file
diff --git a/arango_rdf/meta/xsd.trig b/arango_rdf/meta/xsd.trig
new file mode 100644
index 00000000..615b57e8
--- /dev/null
+++ b/arango_rdf/meta/xsd.trig
@@ -0,0 +1,12 @@
+@prefix xsd: .
+@prefix rdf: .
+@prefix rdfs: .
+@prefix owl: .
+@prefix dc: .
+
+xsd: {
+ a owl:Ontology ;
+ dc:title "The XML Schema vocabulary (XSD)" .
+
+ xsd:nonNegativeInteger a rdfs:Class .
+}
diff --git a/arango_rdf/typings.py b/arango_rdf/typings.py
new file mode 100644
index 00000000..38484d8d
--- /dev/null
+++ b/arango_rdf/typings.py
@@ -0,0 +1,30 @@
+__all__ = [
+ "Json",
+ "ADBMetagraph",
+ "ADBDocs",
+ "RDFListHeads",
+ "RDFListData",
+ "RDFTerm",
+ "RDFTermMeta",
+ "PredicateScope",
+ "TypeMap",
+]
+
+from typing import Any, DefaultDict, Dict, Set, Tuple, Union
+
+from rdflib import BNode, Literal, URIRef
+
+Json = Dict[str, Any]
+ADBMetagraph = Dict[str, Dict[str, Set[str]]]
+
+# ADBDocsRPT = DefaultDict[str, List[Json]]
+ADBDocs = DefaultDict[str, DefaultDict[str, Json]]
+
+RDFTerm = Union[URIRef, BNode, Literal]
+RDFTermMeta = Tuple[RDFTerm, str, str, str] # RDFTermMeta
+
+RDFListHeads = DefaultDict[RDFTerm, Dict[RDFTerm, Json]]
+RDFListData = DefaultDict[str, DefaultDict[RDFTerm, Json]]
+
+PredicateScope = DefaultDict[URIRef, DefaultDict[str, Set[Tuple[str, str]]]]
+TypeMap = DefaultDict[RDFTerm, Set[str]]
diff --git a/arango_rdf/utils.py b/arango_rdf/utils.py
new file mode 100644
index 00000000..dbb2d0fb
--- /dev/null
+++ b/arango_rdf/utils.py
@@ -0,0 +1,88 @@
+import logging
+import os
+from typing import Any, DefaultDict, Dict, List, Set
+
+from rich.progress import (
+ BarColumn,
+ Progress,
+ SpinnerColumn,
+ TaskProgressColumn,
+ TextColumn,
+ TimeElapsedColumn,
+)
+
+logger = logging.getLogger(__package__)
+handler = logging.StreamHandler()
+formatter = logging.Formatter(
+ f"[%(asctime)s] [{os.getpid()}] [%(levelname)s] - %(name)s: %(message)s",
+ "%Y/%m/%d %H:%M:%S %z",
+)
+handler.setFormatter(formatter)
+logger.addHandler(handler)
+
+
+def rdf_track(text: str, color: str) -> Progress:
+ return Progress(
+ TextColumn(text),
+ BarColumn(complete_style=color, finished_style=color),
+ TaskProgressColumn(),
+ TextColumn("({task.completed}/{task.total})"),
+ TimeElapsedColumn(),
+ )
+
+
+def adb_track(text: str) -> Progress:
+ return Progress(
+ TextColumn(text),
+ TimeElapsedColumn(),
+ TextColumn("{task.fields[action]}"),
+ SpinnerColumn("aesthetic", "#5BC0DE"),
+ )
+
+
+def empty_function(*args: Any) -> None:
+ pass
+
+
+class Node:
+ def __init__(self, name: str, depth: int = 0) -> None:
+ self.name = name
+ self.depth = depth
+ self.children: List[Node] = []
+
+
+class Tree:
+ # TODO: Revisit recursive Tree structure, as it is not needed
+ # We only use `Tree` for being able to extract the depth of each node
+ # The structure itself is irrelevant
+ def __init__(self, root: Node, submap: DefaultDict[str, Set[str]]) -> None:
+ self.root = root
+ self.submap = submap
+ self.nodes: Dict[str, Node] = {}
+ self.build_tree(root, root.name)
+
+ def build_tree(self, current: Node, parent: str, depth: int = 0) -> None:
+ self.nodes[current.name] = current
+ for sub_val in self.submap[parent]:
+ child_node = Node(sub_val, depth + 1)
+ current.children.append(child_node)
+ self.build_tree(child_node, child_node.name, depth + 1)
+
+ def get_node_depth(self, node_id: str) -> int:
+ if node_id in self.nodes:
+ return self.nodes[node_id].depth
+
+ return -1
+
+ def __contains__(self, node_id: str) -> bool:
+ return node_id in self.nodes
+
+ def show(self) -> None: # pragma: no cover
+ print("\n==================")
+ self.show_rec(self.root)
+ print("==================\n")
+
+ def show_rec(self, node: Node) -> None: # pragma: no cover
+ print("|" + "-" * node.depth + node.name)
+ for child_node in node.children:
+ self.show_rec(child_node)
diff --git a/examples/ArangoRDF.ipynb b/examples/ArangoRDF.ipynb
index 884d8286..ab253c9a 100644
--- a/examples/ArangoRDF.ipynb
+++ b/examples/ArangoRDF.ipynb
@@ -1,313 +1,1608 @@
{
- "cells": [
- {
- "cell_type": "markdown",
- "metadata": {
- "id": "VBMhu_S_A2-3"
- },
- "source": [
- "# **ArangoRDF**\n",
- "\n",
- "\n",
- ""
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {
- "id": "44mc2EvIAzDy"
- },
- "source": [
- "# Setup"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {
- "id": "W4WlZsOCAjgV"
- },
- "outputs": [],
- "source": [
- "%%capture\n",
- "!pip install adb-cloud-connector\n",
- "!pip install arango-rdf\n",
- "!git clone -b \"main\" https://github.com/ArangoDB-Community/ArangoRDF.git "
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {
- "id": "wMtfVlvuApDp"
- },
- "outputs": [],
- "source": [
- "from arango import ArangoClient\n",
- "from adb_cloud_connector import get_temp_credentials\n",
- "import json\n",
- "\n",
- "from arango_rdf import ArangoRDF"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {
- "id": "KnQifktFAxHx"
- },
- "source": [
- "# Create a Temporary ArangoDB Cloud Instance"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {
- "id": "ETS8l_NSAv0F"
- },
- "outputs": [],
- "source": [
- "# Request temporary instance from the managed ArangoDB Cloud Service.\n",
- "con = get_temp_credentials()\n",
- "print(json.dumps(con, indent=2))\n",
- "\n",
- "# Connect to the db via the python-arango driver\n",
- "db = ArangoClient(hosts=con[\"url\"]).db(con[\"dbName\"], con[\"username\"], con[\"password\"], verify=True)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {
- "id": "yRuJ3OIGE2Yr"
- },
- "source": [
- "# About RDF\n",
- "RDF is a standard model for data interchange on the Web. RDF has features that facilitate data merging even if the underlying schemas differ, and it specifically supports the evolution of schemas over time without requiring all the data consumers to be changed.\n",
- "\n",
- "RDF extends the linking structure of the Web to use URIs to name the relationship between things as well as the two ends of the link (this is usually referred to as a \"triple\"). Using this simple model, it allows structured and semi-structured data to be mixed, exposed, and shared across different applications.\n",
- "\n",
- "This linking structure forms a directed, labeled graph, where the edges represent the named link between two resources, represented by the graph nodes. This graph view is the easiest possible mental model for RDF and is often used in easy-to-understand visual explanations.\n",
- "\n",
- "Resources to get started:\n",
- "\n",
- "* [RDF Primer](https://www.w3.org/TR/rdf11-concepts/)\n",
- "* [RDFLib (Python)](https://pypi.org/project/rdflib/)\n",
- "* [One Example for Modeling RDF as ArangoDB Graphs](https://www.arangodb.com/docs/stable/data-modeling-graphs-from-rdf.html)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {
- "id": "gFn4w2HlGAMN"
- },
- "source": [
- "# Get Started with ArangoRDF\n",
- "\n",
- "\n",
- "## Initialization\n",
- "The first steps to take when attempting to import RDF data to ArangoDB involves:\n",
- "* Passing a database connection to the ArangoRDF constructor\n",
- "* You can also set a `default_graph` or `sub_graph`\n",
- " * A `sub_graph` is equivalent to a named RDF graph and for now is only stored on the documents that are imported.\n",
- "* Instantiating ArangoRDF also creates an ArangoDB named graph for `default_graph`\n",
- "* Set any configuration options and metadata. Currenlty, the only supported option is `normalize_literals` which is `False` by default. You can write any other metadata to save here as well.\n",
- "* Finally, initialize the collections. Here you can use the defaults or set your own collection names, we set the blank nodes collection to `Blank`.\n",
- "\n"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {
- "id": "mlq5BpN7ErhJ"
- },
- "outputs": [],
- "source": [
- "# Clean up existing data and collections\n",
- "if db.has_graph(\"default_graph\"):\n",
- " db.delete_graph(\"default_graph\", drop_collections=True, ignore_missing=True)\n",
- "\n",
- "# Initializes default_graph and sets RDF graph identifier (ArangoDB sub_graph)\n",
- "# Optional: sub_graph (stores graph name as the 'graph' attribute on all edges in Statement collection)\n",
- "# Optional: default_graph (name of ArangoDB Named Graph, defaults to 'default_graph',\n",
- "# is root graph that contains all collections/relations)\n",
- "adb_rdf = ArangoRDF(db, sub_graph=\"http://data.sfgov.org/ontology\") \n",
- "print(\"initialized graph\")\n",
- "config = {\"normalize_literals\": False} # default: False\n",
- "\n",
- "# RDF Import\n",
- "adb_rdf.init_rdf_collections(bnode=\"Blank\")\n",
- "print(\"initialized collections\")"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {
- "id": "6UpzYTVCIMMo"
- },
- "source": [
- "# Import RDF Data\n",
- "Now that we have setup our scaffolding for the RDF graphs, let's import some data.\n",
- "\n",
- "## Import an Ontology\n",
- "To import RDF graphs you simply call the `import_rdf` function and pass in the:\n",
- "\n",
- "\n",
- "* file path\n",
- "* format\n",
- "* `config` object\n",
- "* `save_config` is boolean that stores any config and metadata in the configuration collection to be used later.\n",
- "\n",
- "\n",
- "\n",
- "\n",
- "\n"
- ]
+ "cells": [
+ {
+ "attachments": {},
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "VBMhu_S_A2-3"
+ },
+ "source": [
+ "# **ArangoRDF**\n",
+ "\n",
+ "\n",
+ ""
+ ]
+ },
+ {
+ "attachments": {},
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "44mc2EvIAzDy"
+ },
+ "source": [
+ "# Setup"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "id": "W4WlZsOCAjgV"
+ },
+ "outputs": [],
+ "source": [
+ "%%capture\n",
+ "!pip install adb-cloud-connector\n",
+ "!pip install arango-rdf==0.1.0\n",
+ "!git clone https://github.com/ArangoDB-Community/ArangoRDF.git "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "id": "wMtfVlvuApDp"
+ },
+ "outputs": [],
+ "source": [
+ "from adb_cloud_connector import get_temp_credentials\n",
+ "from arango import ArangoClient\n",
+ "import json\n",
+ "\n",
+ "from rdflib import Graph, ConjunctiveGraph, URIRef, Literal, Namespace\n",
+ "from rdflib.namespace import RDFS, XSD\n",
+ "\n",
+ "from arango_rdf import ArangoRDF"
+ ]
+ },
+ {
+ "attachments": {},
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "yRuJ3OIGE2Yr"
+ },
+ "source": [
+ "# Understanding RDF"
+ ]
+ },
+ {
+ "attachments": {},
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "z1SFNei_imUf"
+ },
+ "source": [
+ "RDF is a standard model for data interchange on the Web. RDF has features that facilitate data merging even if the underlying schemas differ, and it specifically supports the evolution of schemas over time without requiring all the data consumers to be changed.\n",
+ "\n",
+ "RDF extends the linking structure of the Web to use URIs to name the relationship between things as well as the two ends of the link (this is usually referred to as a \"triple\"). Using this simple model, it allows structured and semi-structured data to be mixed, exposed, and shared across different applications.\n",
+ "\n",
+ "This linking structure forms a directed, labeled graph, where the edges represent the named link between two resources, represented by the graph nodes. This graph view is the easiest possible mental model for RDF and is often used in easy-to-understand visual explanations.\n",
+ "\n",
+ "Resources to get started:\n",
+ "\n",
+ "* [RDF Primer](https://www.w3.org/TR/rdf11-concepts/)\n",
+ "* [RDFLib (Python)](https://pypi.org/project/rdflib/)\n",
+ "* [One Example for Modeling RDF as ArangoDB Graphs](https://www.arangodb.com/docs/stable/data-modeling-graphs-from-rdf.html)\n"
+ ]
+ },
+ {
+ "attachments": {},
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "4VzvpJ2EuuMJ"
+ },
+ "source": [
+ "![image.png](data:image/png;base64,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)"
+ ]
+ },
+ {
+ "attachments": {},
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "ssq_ohNZu0q_"
+ },
+ "source": [
+ "[RDFLib](https://github.com/RDFLib/rdflib) is a pure Python package for working with RDF. \n",
+ "\n",
+ "RDFLib aims to be a pythonic RDF API. RDFLib's main data object is a `Graph` which is a Python collection of RDF Subject, Predicate, Object Triples:\n",
+ "\n",
+ "To create graph and load it with RDF data from DBPedia then print the results:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "id": "xiRxsYqY52BJ"
+ },
+ "outputs": [],
+ "source": [
+ "g = Graph()\n",
+ "g.parse('http://dbpedia.org/resource/Semantic_Web')\n",
+ "\n",
+ "for s, p, o in g:\n",
+ " print(s, p, o)"
+ ]
+ },
+ {
+ "attachments": {},
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "tibPlFLZ56cZ"
+ },
+ "source": [
+ "The components of the triples are URIs (resources) or Literals (values).\n",
+ "\n",
+ "URIs are grouped together by namespace, common namespaces are included in RDFLib:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "id": "vhpFtnqF57x9"
+ },
+ "outputs": [],
+ "source": [
+ "from rdflib.namespace import DC, DCTERMS, DOAP, FOAF, SKOS, OWL, RDF, RDFS, VOID, XMLNS, XSD"
+ ]
+ },
+ {
+ "attachments": {},
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "f1tb4nb15_Gl"
+ },
+ "source": [
+ "You can use them like this:\n",
+ "\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "id": "66dQYVV46BLH"
+ },
+ "outputs": [],
+ "source": [
+ "g = Graph()\n",
+ "semweb = URIRef('http://dbpedia.org/resource/Semantic_Web')\n",
+ "val = g.value(semweb, RDFS.label)"
+ ]
+ },
+ {
+ "attachments": {},
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "1dz2_jcA6Cls"
+ },
+ "source": [
+ "Where `RDFS` is the RDFS namespace, `XSD` the XML Schema Datatypes namespace and `g.value` returns an object of the triple-pattern given (or an arbitrary one if multiple exist).\n",
+ "\n",
+ "Or like this, adding a triple to a graph `g`:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "id": "G-zlPMs36LF3"
+ },
+ "outputs": [],
+ "source": [
+ "g.add((\n",
+ " URIRef(\"http://example.com/person/nick\"),\n",
+ " FOAF.givenName,\n",
+ " Literal(\"Nick\", datatype=XSD.string)\n",
+ "))"
+ ]
+ },
+ {
+ "attachments": {},
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "4UvegpOD6QBC"
+ },
+ "source": [
+ "The triple (in n-triples notation) ` \"Nick\"^^ .` is created where the property `FOAF.givenName` is the URI `` and `XSD.string` is the URI ``.\n",
+ "\n",
+ "You can bind namespaces to prefixes to shorten the URIs for RDF/XML, Turtle, N3, TriG, TriX & JSON-LD serializations:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "id": "XXVr_m726aGX"
+ },
+ "outputs": [],
+ "source": [
+ "g.bind(\"foaf\", FOAF)\n",
+ "g.bind(\"xsd\", XSD)"
+ ]
+ },
+ {
+ "attachments": {},
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "5ag8fFkN6cDK"
+ },
+ "source": [
+ "This will allow the n-triples triple above to be serialised like this:\n",
+ "\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "id": "VBiHxQ8I6ckC"
+ },
+ "outputs": [],
+ "source": [
+ "print(g.serialize(format=\"turtle\"))"
+ ]
+ },
+ {
+ "attachments": {},
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "fU3ZW4Up6kft"
+ },
+ "source": [
+ "New Namespaces can also be defined:\n",
+ "\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "id": "g9y5vAR96m5J"
+ },
+ "outputs": [],
+ "source": [
+ "dbpedia = Namespace('http://dbpedia.org/ontology/')\n",
+ "\n",
+ "abstracts = list(x for x in g.objects(semweb, dbpedia['abstract']) if x.language=='en')"
+ ]
+ },
+ {
+ "attachments": {},
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "KnQifktFAxHx"
+ },
+ "source": [
+ "# Create a Temporary ArangoDB Cloud Instance"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "colab": {
+ "base_uri": "https://localhost:8080/"
},
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {
- "id": "Lzv1ibrAJKe6"
- },
- "outputs": [],
- "source": [
- "print(\"importing ontology...\")\n",
- "# Start with importing the ontology\n",
- "adb_rdf.import_rdf(\"./ArangoRDF/examples/data/airport-ontology.owl\", format=\"xml\", config=config, save_config=True)\n",
- "print(\"Ontology imported\")"
- ]
+ "id": "ETS8l_NSAv0F",
+ "outputId": "b32451f1-b737-4fc9-a7ee-443265acef31"
+ },
+ "outputs": [],
+ "source": [
+ "# Request temporary instance from the managed ArangoDB Cloud Service.\n",
+ "con = get_temp_credentials()\n",
+ "print(json.dumps(con, indent=2))\n",
+ "\n",
+ "# Connect to the db via the python-arango driver\n",
+ "db = ArangoClient(hosts=con[\"url\"]).db(con[\"dbName\"], con[\"username\"], con[\"password\"], verify=True)"
+ ]
+ },
+ {
+ "attachments": {},
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "7y81WHO8eG8_"
+ },
+ "source": [
+ "# Data Import"
+ ]
+ },
+ {
+ "attachments": {},
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "BM0iRYPDeG8_"
+ },
+ "source": [
+ "For demo purposes, we will be using the [ArangoDB Game Of Thrones Dataset](https://github.com/arangodb/example-datasets/tree/master/GameOfThrones)."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "colab": {
+ "base_uri": "https://localhost:8080/"
},
- {
- "cell_type": "markdown",
- "metadata": {
- "id": "LPinn1CrKFpY"
- },
- "source": [
- "Notice that we supplied `xml` for the format, this is because this owl file is serialized to RDF/xml. If we hadn't supplied this we would receive an error and the application would halt. "
- ]
+ "id": "7bgGJ3QkeG8_",
+ "outputId": "fbbe33ff-9df9-459a-93e1-3232ea3733bc"
+ },
+ "outputs": [],
+ "source": [
+ "!chmod -R 755 ArangoRDF/\n",
+ "!./ArangoRDF/tests/tools/arangorestore -c none --server.endpoint http+ssl://{con[\"hostname\"]}:{con[\"port\"]} --server.username {con[\"username\"]} --server.database {con[\"dbName\"]} --server.password {con[\"password\"]} --replication-factor 3 --input-directory \"ArangoRDF/tests/data/adb/got_dump\""
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "id": "IkWQ9W4UZcIz"
+ },
+ "outputs": [],
+ "source": [
+ "if not db.has_graph(\"GameOfThrones\"):\n",
+ " db.create_graph(\n",
+ " \"GameOfThrones\",\n",
+ " edge_definitions=[\n",
+ " {\n",
+ " \"edge_collection\": \"ChildOf\",\n",
+ " \"from_vertex_collections\": [\"Characters\"],\n",
+ " \"to_vertex_collections\": [\"Characters\"],\n",
+ " },\n",
+ " ],\n",
+ " orphan_collections=[\"Traits\", \"Locations\"],\n",
+ " )"
+ ]
+ },
+ {
+ "attachments": {},
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "QfE_tKxneG9A"
+ },
+ "source": [
+ "# Instantiate ArangoRDF"
+ ]
+ },
+ {
+ "attachments": {},
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "kGfhzPT9eG9A"
+ },
+ "source": [
+ "Connect ArangoRDF to our temporary ArangoDB cluster:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "colab": {
+ "base_uri": "https://localhost:8080/"
},
- {
- "cell_type": "markdown",
- "metadata": {
- "id": "dkwHfYwOJlk5"
- },
- "source": [
- "## Import RDF Data Graphs\n",
- "\n",
- "Now that you have imported your ontology it is time to import some data.\n",
- "The process is the same, the only difference is now we are now importing using a [Turtle](https://www.w3.org/2007/02/turtle/primer/) file whose format is `ttl`.\n",
- "\n",
- "I have also gone ahead and added an item to our config dictionary so that we can see how to get saved configuration information later."
- ]
+ "id": "oG496kBeeG9A",
+ "outputId": "57cb6237-dfea-48b6-a2c3-af4ad52cff7b"
+ },
+ "outputs": [],
+ "source": [
+ "adbrdf = ArangoRDF(db)"
+ ]
+ },
+ {
+ "attachments": {},
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "znQCjOwt7zBz"
+ },
+ "source": [
+ "# RDF to ArangoDB"
+ ]
+ },
+ {
+ "attachments": {},
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "0qry3Bcy-160"
+ },
+ "source": [
+ "#### RPT vs PGT"
+ ]
+ },
+ {
+ "attachments": {},
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "0ONWNS6t8x7A"
+ },
+ "source": [
+ "RDF-to-ArangoDB functionality has been implemented using concepts described in the paper [*Transforming RDF-star to Property Graphs: A Preliminary Analysis of Transformation Approaches*](https://arxiv.org/abs/2210.05781).\n",
+ "\n",
+ "In other words, ArangoRDF offers 2 RDF-to-ArangoDB transformation methods:\n",
+ "\n",
+ "1. RDF-topology Preserving Transformation (RPT): `ArangoRDF.rdf_to_arangodb_by_rpt()`\n",
+ "2. Property Graph Transformation (PGT): `ArangoRDF.rdf_to_arangodb_by_pgt()`\n",
+ "\n",
+ "RPT preserves the RDF Graph structure by transforming each RDF Statement into an ArangoDB Edge.\n",
+ "\n",
+ "PGT on the other hand ensures that Datatype Property Statements are mapped as ArangoDB Document Properties."
+ ]
+ },
+ {
+ "attachments": {},
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "U_sBs3jc96e3"
+ },
+ "source": [
+ "```ttl\n",
+ "@prefix ex: .\n",
+ "@prefix xsd: .\n",
+ "ex:book ex:publish_date \"1963-03-22\"^^xsd:date .\n",
+ "ex:book ex:pages \"100\"^^xsd:integer .\n",
+ "ex:book ex:cover 20 .\n",
+ "ex:book ex:index 55 .\n",
+ "```\n",
+ "\n",
+ "| RPT | PGT |\n",
+ "|:-------------------------:|:-------------------------:|\n",
+ "| ![image](https://user-images.githubusercontent.com/43019056/232347662-ab48ebfb-e215-4aff-af28-a5915414a8fd.png) | ![image](https://user-images.githubusercontent.com/43019056/232347681-c899ef09-53c7-44de-861e-6a98d448b473.png) |\n"
+ ]
+ },
+ {
+ "attachments": {},
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "mRutdKii-Pk5"
+ },
+ "source": [
+ "#### Simple RPT & PGT Examples"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "id": "cy_BWXK2AX5n"
+ },
+ "outputs": [],
+ "source": [
+ "# Case 1: Standard RDF statement\n",
+ "\n",
+ "data = \"\"\"\n",
+ "@prefix ex: .\n",
+ "\n",
+ "ex:alice a ex:Person .\n",
+ "ex:bob a ex:Person .\n",
+ "ex:alice ex:meets ex:bob .\n",
+ "\"\"\"\n",
+ "\n",
+ "g = Graph()\n",
+ "g.parse(data=data)\n",
+ "\n",
+ "adbrdf.rdf_to_arangodb_by_rpt(\"DataRPT\", g, overwrite_graph=True)\n",
+ "adbrdf.rdf_to_arangodb_by_pgt(\"DataPGT\", g, overwrite_graph=True)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "id": "9BFNRAzLDmzU"
+ },
+ "outputs": [],
+ "source": [
+ "# Case 2: The predicate of an RDF statement is subject in another statement\n",
+ "# Case 2.1: Predicate as subject and literal as object\n",
+ "\n",
+ "data = \"\"\"\n",
+ "@prefix ex: .\n",
+ "@prefix rdfs: .\n",
+ "\n",
+ "ex:Sam ex:mentor ex:Lee .\n",
+ "ex:mentor rdfs:label \"project supervisor\" .\n",
+ "ex:mentor ex:name \"mentor's name\" .\n",
+ "\n",
+ "ex:Sam a ex:Person .\n",
+ "ex:Lee a ex:Person .\n",
+ "\"\"\"\n",
+ "\n",
+ "g = Graph()\n",
+ "g.parse(data=data)\n",
+ "\n",
+ "adbrdf.rdf_to_arangodb_by_rpt(\"DataRPT\", g, overwrite_graph=True)\n",
+ "adbrdf.rdf_to_arangodb_by_pgt(\"DataPGT\", g, overwrite_graph=True)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "id": "E6t4VRcsD2m7"
+ },
+ "outputs": [],
+ "source": [
+ "# Case 2: The predicate of an RDF statement is subject in another statement\n",
+ "# Case 2.2: Predicate as subject and RDF resource as object\n",
+ "\n",
+ "data = \"\"\"\n",
+ "@prefix ex: .\n",
+ "\n",
+ "ex:Martin ex:mentorJoe ex:Joe.\n",
+ "ex:mentorJoe ex:alias ex:teacher .\n",
+ "\"\"\"\n",
+ "\n",
+ "g = Graph()\n",
+ "g.parse(data=data)\n",
+ "\n",
+ "adbrdf.rdf_to_arangodb_by_rpt(\"DataRPT\", g, overwrite_graph=True)\n",
+ "adbrdf.rdf_to_arangodb_by_pgt(\"DataPGT\", g, overwrite_graph=True)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "id": "NEDGhDfzEEhg"
+ },
+ "outputs": [],
+ "source": [
+ "# Case 2: The predicate of an RDF statement is subject in another statement\n",
+ "# Case 2.3: Predicate as subject and RDF property as object - rdfs:subPropertyOf\n",
+ "\n",
+ "data = \"\"\"\n",
+ "@prefix rdfs: .\n",
+ "@prefix ex: .\n",
+ "\n",
+ "ex:Jan a ex:Person .\n",
+ "ex:Leo a ex:Person .\n",
+ "ex:Jan ex:supervise ex:Leo .\n",
+ "\n",
+ "ex:supervise rdfs:subPropertyOf ex:administer .\n",
+ "\"\"\"\n",
+ "\n",
+ "g = Graph()\n",
+ "g.parse(data=data)\n",
+ "\n",
+ "adbrdf.rdf_to_arangodb_by_rpt(\"DataRPT\", g, overwrite_graph=True)\n",
+ "adbrdf.rdf_to_arangodb_by_pgt(\"DataPGT\", g, overwrite_graph=True)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "id": "WraNcreKcJ35"
+ },
+ "outputs": [],
+ "source": [
+ "# Case 2: The predicate of an RDF statement is subject in another statement\n",
+ "# Case 2.4: Predicate as subject and RDF class as object - rdf:type\n",
+ "\n",
+ "data = \"\"\"\n",
+ "@prefix ex: .\n",
+ "@prefix rdf: .\n",
+ "\n",
+ "ex:Tom ex:friend ex:Chris .\n",
+ "ex:friend rdf:type ex:relation .\n",
+ "\"\"\"\n",
+ "\n",
+ "g = Graph()\n",
+ "g.parse(data=data)\n",
+ "\n",
+ "adbrdf.rdf_to_arangodb_by_rpt(\"DataRPT\", g, overwrite_graph=True)\n",
+ "adbrdf.rdf_to_arangodb_by_pgt(\"DataPGT\", g, overwrite_graph=True)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "id": "00ooim92Ekxv"
+ },
+ "outputs": [],
+ "source": [
+ "# Case 3: Data types and language tags\n",
+ "# Case 3.1: Datatype property statements with different data types of the literal objects\n",
+ "\n",
+ "data = \"\"\"\n",
+ "@prefix ex: .\n",
+ "@prefix xsd: .\n",
+ "\n",
+ "ex:book ex:publish_date \"1963-03-22\" .\n",
+ "ex:book ex:pages \"100\"^^xsd:integer .\n",
+ "ex:book ex:cover 20 .\n",
+ "ex:book ex:index \"55\" .\n",
+ "\"\"\"\n",
+ "\n",
+ "g = Graph()\n",
+ "g.parse(data=data)\n",
+ "\n",
+ "adbrdf.rdf_to_arangodb_by_rpt(\"DataRPT\", g, overwrite_graph=True)\n",
+ "adbrdf.rdf_to_arangodb_by_pgt(\"DataPGT\", g, overwrite_graph=True)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "id": "UmIn_SZWccN2"
+ },
+ "outputs": [],
+ "source": [
+ "# Case 3: Data types and language tags\n",
+ "# Case 3.2: Datatype property statements with different language tags of the literal objects\n",
+ "# NOTE: PGT will currently discard the language tags!\n",
+ "\n",
+ "data = \"\"\"\n",
+ "@prefix ex: .\n",
+ "\n",
+ "ex:book ex:title \"Book\"@en.\n",
+ "ex:book ex:title \"Bog\"@da.\n",
+ "\"\"\"\n",
+ "\n",
+ "g = Graph()\n",
+ "g.parse(data=data)\n",
+ "\n",
+ "adbrdf.rdf_to_arangodb_by_rpt(\"DataRPT\", g, overwrite_graph=True)\n",
+ "adbrdf.rdf_to_arangodb_by_pgt(\"DataPGT\", g, overwrite_graph=True)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "id": "SQaaqperccbA"
+ },
+ "outputs": [],
+ "source": [
+ "# Case 4: RDF list\n",
+ "\n",
+ "data = \"\"\"\n",
+ "@prefix ex: .\n",
+ "\n",
+ "ex:List1 ex:contents (\"one\" \"two\" \"three\").\n",
+ "\"\"\"\n",
+ "\n",
+ "g = Graph()\n",
+ "g.parse(data=data)\n",
+ "\n",
+ "adbrdf.rdf_to_arangodb_by_rpt(\"DataRPT\", g, overwrite_graph=True)\n",
+ "adbrdf.rdf_to_arangodb_by_pgt(\"DataPGT\", g, overwrite_graph=True)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "id": "r0kHJogZFEKO"
+ },
+ "outputs": [],
+ "source": [
+ "# Case 5: Blank nodes\n",
+ "\n",
+ "data = \"\"\"\n",
+ "@prefix ex: .\n",
+ "\n",
+ "ex:bob ex:nationality _:c .\n",
+ "_:c ex:country \"Canada\" .\n",
+ "\"\"\"\n",
+ "\n",
+ "g = Graph()\n",
+ "g.parse(data=data)\n",
+ "\n",
+ "adbrdf.rdf_to_arangodb_by_rpt(\"DataRPT\", g, overwrite_graph=True)\n",
+ "adbrdf.rdf_to_arangodb_by_pgt(\"DataPGT\", g, overwrite_graph=True)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "id": "noAXcHOJFJvG"
+ },
+ "outputs": [],
+ "source": [
+ "# Case 6: Named graphs\n",
+ "\n",
+ "data = \"\"\"\n",
+ "@prefix ex: .\n",
+ "@prefix rdf: .\n",
+ "@prefix rdfs: .\n",
+ "\n",
+ "ex:Monica ex:employer ex:ArangoDB .\n",
+ "\n",
+ "ex:Graph1 {\n",
+ " ex:Monica a ex:Entity .\n",
+ " ex:Management a ex:Skill .\n",
+ " ex:Monica ex:name \"Monica\" .\n",
+ " ex:Monica ex:homepage .\n",
+ " ex:Monica ex:hasSkill ex:Management .\n",
+ "}\n",
+ "\n",
+ "ex:Graph2 {\n",
+ " ex:Programming a ex:Skill .\n",
+ " a ex:Website .\n",
+ " ex:Monica a ex:Person .\n",
+ " ex:Person rdfs:subClassOf ex:Entity .\n",
+ " ex:Monica ex:hasSkill ex:Programming .\n",
+ "}\n",
+ "\"\"\"\n",
+ "\n",
+ "cg = ConjunctiveGraph()\n",
+ "cg.parse(data=data, format='trig')\n",
+ "\n",
+ "adbrdf.rdf_to_arangodb_by_rpt(\"DataRPT\", cg, overwrite_graph=True)\n",
+ "adbrdf.rdf_to_arangodb_by_pgt(\"DataPGT\", cg, overwrite_graph=True)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "id": "gtHKG7PiGyyF"
+ },
+ "outputs": [],
+ "source": [
+ "# Case 7: Multiple types for resources - rdf:type\n",
+ "\n",
+ "data = \"\"\"\n",
+ "@prefix : .\n",
+ "@prefix rdfs: .\n",
+ "@prefix adb: .\n",
+ "@prefix owl: .\n",
+ "\n",
+ ":alice a :Arson .\n",
+ ":alice a :Author .\n",
+ "\n",
+ ":Zenkey rdfs:subClassOf :Zebra .\n",
+ ":Zenkey rdfs:subClassOf :Donkey .\n",
+ ":Donkey rdfs:subClassOf :Animal . \n",
+ ":Zebra rdfs:subClassOf :Animal .\n",
+ ":Human rdfs:subClassOf :Animal .\n",
+ ":Animal rdfs:subClassOf :LivingThing .\n",
+ ":LivingThing rdfs:subClassOf :Thing .\n",
+ ":Thing rdfs:subClassOf :Object .\n",
+ "\n",
+ ":charlie a :LivingThing .\n",
+ ":charlie a :Animal .\n",
+ ":charlie a :Zenkey .\n",
+ "\n",
+ ":marty a :LivingThing .\n",
+ ":marty a :Animal .\n",
+ ":marty a :Human .\n",
+ "\n",
+ ":john a :Singer .\n",
+ ":john a :Writer .\n",
+ ":john a :Guitarist .\n",
+ ":john adb:collection \"Artist\" .\n",
+ "\"\"\"\n",
+ "\n",
+ "g = Graph()\n",
+ "g.parse(data=data)\n",
+ "\n",
+ "adbrdf.rdf_to_arangodb_by_rpt(\"DataRPT\", g, overwrite_graph=True)\n",
+ "adbrdf.rdf_to_arangodb_by_pgt(\"DataPGT\", g, overwrite_graph=True)"
+ ]
+ },
+ {
+ "attachments": {},
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "7ZeZNno5k3XW"
+ },
+ "source": [
+ "Cases 8 - 15: RDF-Star\n",
+ "\n",
+ "`rdflib` has yet to introduce support for [Quoted Triples](https://www.w3.org/TR/rdf12-concepts/#dfn-quoted-triple), so ArangoRDF's support for RDF-star is based on [Triple Reification](https://www.w3.org/wiki/RdfReification)."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "colab": {
+ "base_uri": "https://localhost:8080/",
+ "height": 100,
+ "referenced_widgets": [
+ "8787ef8386834db8a1fabdd1dd84fa53",
+ "0d91a444dc224f9db01cceb0a800e11b",
+ "5c28912b58204b69aca48cef391be3b8",
+ "9f1771b2fea94c3bac52f3834bebdce2",
+ "0304960babaf40e4a92bbbc118e10a24",
+ "5838a39a9e134909bc2a1c1d24052997"
+ ]
},
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {
- "id": "kMBaIZDAKDIb"
- },
- "outputs": [],
- "source": [
- "config['Avocados_Are_Delicious'] = True\n",
- "\n",
- "print(\"importing aircraft data...\")\n",
- "\n",
- "# Next, let's import the actual graph data\n",
- "adb_rdf.import_rdf(f\"./ArangoRDF/examples/data/sfo-aircraft-partial.ttl\", format=\"ttl\", config=config, save_config=True)\n",
- "print(\"aircraft data imported\")\n"
- ]
+ "id": "XevGMv7qdPgI",
+ "outputId": "50495223-2b27-4d2e-e84c-01cc964b8432"
+ },
+ "outputs": [],
+ "source": [
+ "# Case 8: Embedded object property statement in subject position\n",
+ "\n",
+ "\"\"\"\n",
+ "@prefix ex: .\n",
+ "<< ex:alice ex:likes ex:bob >> ex:certainty 0.5 .\n",
+ "\"\"\"\n",
+ "\n",
+ "data = \"\"\"\n",
+ "@prefix ex: .\n",
+ "@prefix rdf: .\n",
+ "\n",
+ "[] a rdf:Statement;\n",
+ " rdf:subject ex:alice;\n",
+ " rdf:predicate ex:likes;\n",
+ " rdf:object ex:bob ;\n",
+ " ex:certainty 0.5 .\n",
+ "\n",
+ "\"\"\"\n",
+ "\n",
+ "g = Graph()\n",
+ "g.parse(data=data)\n",
+ "\n",
+ "adbrdf.rdf_to_arangodb_by_rpt(\"DataRPT\", g, overwrite_graph=True)\n",
+ "adbrdf.rdf_to_arangodb_by_pgt(\"DataPGT\", g, overwrite_graph=True)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "id": "KAs-MpmAp8_c"
+ },
+ "outputs": [],
+ "source": [
+ "# Case 9: Embedded datatype property statement in subject position\n",
+ "# Note: PGT does not support this case \n",
+ "\n",
+ "\"\"\"\n",
+ "@prefix ex: .\n",
+ "<< ex:Mark ex:age 28 >> ex:certainty 1 .\n",
+ "\"\"\"\n",
+ "\n",
+ "data = \"\"\"\n",
+ "@prefix ex: .\n",
+ "@prefix rdf: .\n",
+ "\n",
+ "[] a rdf:Statement;\n",
+ " rdf:subject ex:Mark;\n",
+ " rdf:predicate ex:age;\n",
+ " rdf:object 28 ;\n",
+ " ex:certainty 1 .\n",
+ "\n",
+ "\"\"\"\n",
+ "\n",
+ "g = Graph()\n",
+ "g.parse(data=data)\n",
+ "\n",
+ "adbrdf.rdf_to_arangodb_by_rpt(\"DataRPT\", g, overwrite_graph=True)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "colab": {
+ "base_uri": "https://localhost:8080/",
+ "height": 116,
+ "referenced_widgets": [
+ "3fe6ce0b39d444f8aa1fa45b6b6f1aa7",
+ "9713b9bae1574359a4059ee19d26bbef",
+ "57e31b0d41514940b9cfcc5397d6ee2d",
+ "2f50e0986425434fa1ac6566e86cd574",
+ "6be66e4518f04ee197f3a89a23a63e73",
+ "17756af9fd3f455a9a9da6c02e4b358d"
+ ]
},
- {
- "cell_type": "markdown",
- "metadata": {
- "id": "SS6_ZE8CLAEm"
- },
- "source": [
- "## Configuration\n",
- "\n",
- "Now that we have stored the configuration information we have a couple easy ways to retrieve it should we ever need to.\n",
- "\n",
- "We can lookup saved configurations using to functions:\n",
- "* `get_config_by_latest()`\n",
- "* `get_config_by_key_value('key', 'value')`\n",
- "\n",
- "Now, you can pass this config dictionary to `import_rdf()` as is or change options in it. If no config is found, the application halts to avoid excessive import time with wrong configuration information."
- ]
+ "id": "_ZWrGS9Uqoc1",
+ "outputId": "24b962a7-76db-48e7-f05f-57f860d29c3f"
+ },
+ "outputs": [],
+ "source": [
+ "# Case 10: Embedded object property statement in object position\n",
+ "\n",
+ "\"\"\"\n",
+ "@prefix ex: .\n",
+ "ex:bobshomepage ex:source << ex:mainPage ex:writer ex:alice >> .\n",
+ "\"\"\"\n",
+ "\n",
+ "data = \"\"\"\n",
+ "@prefix ex: .\n",
+ "@prefix rdf: .\n",
+ "\n",
+ "_:x a rdf:Statement;\n",
+ " rdf:subject ex:mainPage;\n",
+ " rdf:predicate ex:writer;\n",
+ " rdf:object ex:alice .\n",
+ "\n",
+ "ex:bobshomepage ex:source _:x .\n",
+ "\n",
+ "\"\"\"\n",
+ "\n",
+ "g = Graph()\n",
+ "g.parse(data=data)\n",
+ "\n",
+ "adbrdf.rdf_to_arangodb_by_rpt(\"DataRPT\", g, overwrite_graph=True)\n",
+ "adbrdf.rdf_to_arangodb_by_pgt(\"DataPGT\", g, overwrite_graph=True)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "colab": {
+ "base_uri": "https://localhost:8080/",
+ "height": 83,
+ "referenced_widgets": [
+ "f1aabec820b64be5b8e996a2a2c19643",
+ "51a410d1637f485494bbf71029df078c",
+ "bbd9f6489d6d4b4bacc62c3c743b09ee",
+ "d09e8f0f2980498ea82ee47e5487a0ac",
+ "1cd62a510adb45908e518c37d286a220",
+ "2f521f91fe8b4a05af9e5f492342b27a"
+ ]
},
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {
- "id": "piHGbfiCLff4"
- },
- "outputs": [],
- "source": [
- "# Get the last saved config\n",
- "config = adb_rdf.get_config_by_latest()\n",
- "print(config)\n",
- "print('')\n",
- "\n",
- "# Get the most recent config that matches our search \n",
- "config = adb_rdf.get_config_by_key_value('Avocados_Are_Delicious', True)\n",
- "print(config)"
- ]
+ "id": "E_iK33XDSiml",
+ "outputId": "cbd8922b-02fc-4137-b1d4-79c8881748d8"
+ },
+ "outputs": [],
+ "source": [
+ "# Case 11: Embedded object property statement in subject position and non-literal object\n",
+ "# Case 11.1: Asserted statement with non-literal object\n",
+ "\n",
+ "\"\"\"\n",
+ "@prefix ex: .\n",
+ "<< ex:mainPage ex:writer ex:alice >> ex:source ex:bobshomepage .\n",
+ "\"\"\"\n",
+ "\n",
+ "data = \"\"\"\n",
+ "@prefix ex: .\n",
+ "@prefix rdf: .\n",
+ "\n",
+ "[] a rdf:Statement;\n",
+ " rdf:subject ex:mainPage;\n",
+ " rdf:predicate ex:writer;\n",
+ " rdf:object ex:alice ;\n",
+ " ex:source ex:bobshomepage .\n",
+ "\n",
+ "\"\"\"\n",
+ "\n",
+ "g = Graph()\n",
+ "g.parse(data=data)\n",
+ "\n",
+ "adbrdf.rdf_to_arangodb_by_rpt(\"DataRPT\", g, overwrite_graph=True)\n",
+ "adbrdf.rdf_to_arangodb_by_pgt(\"DataPGT\", g, overwrite_graph=True)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "colab": {
+ "base_uri": "https://localhost:8080/",
+ "height": 67,
+ "referenced_widgets": [
+ "74986d09ef0142c7a5a7c21a3a5110ab",
+ "994b46fd0e0a446aa28211961585a578",
+ "465bc346393e44689437b9cc9077cd09",
+ "379e753ec1784adcb56e316c2b700d63",
+ "415dc3a7e28a47aeac3b8324e7fbe62d",
+ "c537bd80b0884e8fa3c156533ddd19ff"
+ ]
},
- {
- "cell_type": "markdown",
- "metadata": {
- "id": "qic66nCoMA5A"
- },
- "source": [
- "## Exporting to RDF\n",
- "Should the need ever arise that you are required to export data from ArangoDB you can do so using the `export_rdf()` function.\n",
- "\n",
- "Export only takes in the:\n",
- "* output filename\n",
- "* format "
- ]
+ "id": "0oZbDeLeS6ll",
+ "outputId": "7d370296-8c0b-4f5d-cefe-f05c820e58fd"
+ },
+ "outputs": [],
+ "source": [
+ "# Case 11: Embedded object property statement in subject position and non-literal object\n",
+ "# Case 11.2: Asserted statement with non-literal object that appears in another asserted statement\n",
+ "\n",
+ "\"\"\"\n",
+ "@prefix ex: .\n",
+ "ex:alex ex:age 25 .\n",
+ "<< ex:alice ex:friend ex:bob >> ex:mentionedBy ex:alex .\n",
+ "\"\"\"\n",
+ "\n",
+ "data = \"\"\"\n",
+ "@prefix ex: .\n",
+ "@prefix rdf: .\n",
+ "\n",
+ "ex:alex ex:age 25 .\n",
+ "\n",
+ "[] a rdf:Statement;\n",
+ " rdf:subject ex:alice;\n",
+ " rdf:predicate ex:friend;\n",
+ " rdf:object ex:bob ;\n",
+ " ex:mentionedBy ex:alex .\n",
+ "\n",
+ "\"\"\"\n",
+ "\n",
+ "g = Graph()\n",
+ "g.parse(data=data)\n",
+ "\n",
+ "adbrdf.rdf_to_arangodb_by_rpt(\"DataRPT\", g, overwrite_graph=True)\n",
+ "adbrdf.rdf_to_arangodb_by_pgt(\"DataPGT\", g, overwrite_graph=True)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "colab": {
+ "base_uri": "https://localhost:8080/",
+ "height": 100,
+ "referenced_widgets": [
+ "b0083d492a3448e196aeb877f5ee2385",
+ "95116252d81e4a1aa9b344d96da31308",
+ "221d020caf68478ca7c1dcaf84b13f10",
+ "c529f4531a4242059f59383d7a111486",
+ "a74ca78dbcd44771892b708dd9705551",
+ "e1ffa38988d84dfe91f89d25c4f33535"
+ ]
},
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {
- "id": "lNaZVt0jMr8I"
- },
- "outputs": [],
- "source": [
- "print(\"exporting data...\")\n",
- "adb_rdf.export_rdf(f\"./ArangoRDF/examples/data/rdfExport.xml\", format=\"xml\")\n",
- "print(\"export complete\")"
- ]
+ "id": "woNfHiZ5S__t",
+ "outputId": "855406f6-c3b0-419e-a8c6-0d815c4cef22"
+ },
+ "outputs": [],
+ "source": [
+ "# Case 12: Embedded statement in subject position - object property with rdf:type predicate\n",
+ "# Case 12.1: Asserted statement with rdf:type as predicate\n",
+ "\n",
+ "\"\"\"\n",
+ "@prefix ex: .\n",
+ "@prefix rdf: .\n",
+ "\n",
+ "<< ex:mainPage ex:writer ex:alice >> rdf:type ex:bobshomepage .\n",
+ "\"\"\"\n",
+ "\n",
+ "data = \"\"\"\n",
+ "@prefix ex: .\n",
+ "@prefix rdf: .\n",
+ "\n",
+ "[] a rdf:Statement;\n",
+ " rdf:subject ex:mainPage;\n",
+ " rdf:predicate ex:writer;\n",
+ " rdf:object ex:alice ;\n",
+ " rdf:type ex:bobshomepage .\n",
+ "\n",
+ "\"\"\"\n",
+ "\n",
+ "g = Graph()\n",
+ "g.parse(data=data)\n",
+ "\n",
+ "adbrdf.rdf_to_arangodb_by_rpt(\"DataRPT\", g, overwrite_graph=True)\n",
+ "adbrdf.rdf_to_arangodb_by_pgt(\"DataPGT\", g, overwrite_graph=True)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "colab": {
+ "base_uri": "https://localhost:8080/",
+ "height": 100,
+ "referenced_widgets": [
+ "a4dea9f78e014a9d9ddd35cc5b0256fb",
+ "2882bb947da84d6d8ebe063364cacfff",
+ "2823ee8e9ecf4ce2bdac9f641c3f037f",
+ "75b23a1142834c47843d9f451fea17d8",
+ "c84484cf3f6b4035963ef5de688027a9",
+ "e1d3b02470c3404fa5faf0b6a0a8acb6"
+ ]
},
- {
- "cell_type": "markdown",
- "metadata": {
- "id": "yx190M9ENiR9"
- },
- "source": [
- "# What's Next?\n",
- "\n",
- "### We need your help!\n",
- "This is a fresh community project that has a lot more to be done. We gladly welcome any feedback, issues, and PRs. You can find the repository in the ArangoDB-Community GitHub organization, [here](https://github.com/ArangoDB-Community/ArangoRDF)."
- ]
- }
- ],
- "metadata": {
+ "id": "lbbdb2lwS_2M",
+ "outputId": "d9ae1108-1ba5-4dd9-b61f-8bae40cabe39"
+ },
+ "outputs": [],
+ "source": [
+ "# Case 12: Embedded statement in subject position - object property with rdf:type predicate\n",
+ "# Case 12.2: Embedded statement with rdf:type as predicate\n",
+ "\n",
+ "\"\"\"\n",
+ "@prefix ex: .\n",
+ "@prefix rdf: .\n",
+ "\n",
+ "<< ex:lara rdf:type ex:writer >> ex:owner ex:journal .\n",
+ "\"\"\"\n",
+ "\n",
+ "data = \"\"\"\n",
+ "@prefix ex: .\n",
+ "@prefix rdf: .\n",
+ "\n",
+ "[] a rdf:Statement;\n",
+ " rdf:subject ex:lara;\n",
+ " rdf:predicate rdf:type;\n",
+ " rdf:object ex:writer ;\n",
+ " ex:owner ex:journal .\n",
+ "\n",
+ "\"\"\"\n",
+ "\n",
+ "g = Graph()\n",
+ "g.parse(data=data)\n",
+ "\n",
+ "adbrdf.rdf_to_arangodb_by_rpt(\"DataRPT\", g, overwrite_graph=True)\n",
+ "adbrdf.rdf_to_arangodb_by_pgt(\"DataPGT\", g, overwrite_graph=True)\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "id": "4TwIzKZ4S_tN"
+ },
+ "outputs": [],
+ "source": [
+ "# Case 13: Double nested RDF-star statement in subject position\n",
+ "\n",
+ "\"\"\"\n",
+ "@prefix ex: .\n",
+ "@prefix rdf: .\n",
+ "\n",
+ "<< << ex:Steve ex:position ex:CEO >> ex:mentionedBy ex:book >> ex:source ex:Journal .\n",
+ "\"\"\"\n",
+ "\n",
+ "data = \"\"\"\n",
+ "@prefix ex: .\n",
+ "@prefix rdf: .\n",
+ "\n",
+ "_:x a rdf:Statement;\n",
+ " rdf:subject ex:Steve;\n",
+ " rdf:predicate ex:position;\n",
+ " rdf:object ex:CEO .\n",
+ "\n",
+ "[] a rdf:Statement;\n",
+ " rdf:subject _:x;\n",
+ " rdf:predicate ex:mentionedBy;\n",
+ " rdf:object ex:book;\n",
+ " ex:source ex:Journal .\n",
+ "\n",
+ "\"\"\"\n",
+ "\n",
+ "g = Graph()\n",
+ "g.parse(data=data)\n",
+ "\n",
+ "adbrdf.rdf_to_arangodb_by_rpt(\"DataRPT\", g, overwrite_graph=True)\n",
+ "adbrdf.rdf_to_arangodb_by_pgt(\"DataPGT\", g, overwrite_graph=True)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
"colab": {
- "name": "ArangoRDF.ipynb",
- "provenance": []
+ "base_uri": "https://localhost:8080/",
+ "height": 67,
+ "referenced_widgets": [
+ "3b3582c5b8c34e6fa0aa8438adca7c94",
+ "be3545f15d0a484a98b0a049586fb907",
+ "8f7985fd0a84462eb451d85e428dbeff",
+ "a199384048dc44249882c3e0c65fda2f",
+ "b5d4fa7d28e143e2abd243f6846c6bef",
+ "536e9c1c2fbc456e8271519408c21f66"
+ ]
},
- "gpuClass": "standard",
- "kernelspec": {
- "display_name": "Python 3",
- "name": "python3"
+ "id": "yZDLiPMkS_kG",
+ "outputId": "97d98723-6c93-4860-cc8a-a829fee5f8dc"
+ },
+ "outputs": [],
+ "source": [
+ "# Case 14: Multi-valued properties\n",
+ "# Case 14.1: RDF statements with same subject and predicate and different objects\n",
+ "\n",
+ "data = \"\"\"\n",
+ "@prefix ex: .\n",
+ "\n",
+ "ex:college_page ex:subject \"Info_Page\";\n",
+ " ex:subject \"aau_page\" .\n",
+ "\"\"\"\n",
+ "\n",
+ "g = Graph()\n",
+ "g.parse(data=data)\n",
+ "\n",
+ "adbrdf.rdf_to_arangodb_by_rpt(\"DataRPT\", g, overwrite_graph=True)\n",
+ "adbrdf.rdf_to_arangodb_by_pgt(\"DataPGT\", g, overwrite_graph=True)\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "colab": {
+ "base_uri": "https://localhost:8080/",
+ "height": 100,
+ "referenced_widgets": [
+ "671b4473d89c4e7db560ecda1995cbca",
+ "5d82d19332db417998f81a504843e33b",
+ "96174346c25947fc9b1f7cee248378e0",
+ "b6e7e282065a4dc2b4efe8f72f2eaf31",
+ "fab828255ae74fa286ab18bdfa024314",
+ "5a7eb5c6b8a6441b956e9b778124e3b1"
+ ]
},
- "language_info": {
- "name": "python"
+ "id": "X_qBsxffS_br",
+ "outputId": "663faaf3-32c0-478d-a80a-50332feec396"
+ },
+ "outputs": [],
+ "source": [
+ "# Case 14: Multi-valued properties\n",
+ "# Case 14.2: RDF-star statements with the same subject and predicate and different objects\n",
+ "\n",
+ "\"\"\"\n",
+ "@prefix ex: .\n",
+ "@prefix rdf: .\n",
+ "\n",
+ "<< ex:Mary ex:likes ex:Matt >> ex:certainty 0.5 .\n",
+ "<< ex:Mary ex:likes ex:Matt >> ex:certainty 1 .\n",
+ "\"\"\"\n",
+ "\n",
+ "data = \"\"\"\n",
+ "@prefix ex: .\n",
+ "@prefix rdf: .\n",
+ "@prefix xsd: .\n",
+ "\n",
+ "[] a rdf:Statement;\n",
+ " rdf:subject ex:Mary;\n",
+ " rdf:predicate ex:likes;\n",
+ " rdf:object ex:Matt ;\n",
+ " ex:certainty 0.5 .\n",
+ "\n",
+ "[] a rdf:Statement;\n",
+ " rdf:subject ex:Mary;\n",
+ " rdf:predicate ex:likes;\n",
+ " rdf:object ex:Matt ;\n",
+ " ex:certainty 1 .\n",
+ "\n",
+ "\"\"\"\n",
+ "\n",
+ "g = Graph()\n",
+ "g.parse(data=data)\n",
+ "\n",
+ "adbrdf.rdf_to_arangodb_by_rpt(\"DataRPT\", g, overwrite_graph=True)\n",
+ "adbrdf.rdf_to_arangodb_by_pgt(\"DataPGT\", g, overwrite_graph=True)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "colab": {
+ "base_uri": "https://localhost:8080/",
+ "height": 67,
+ "referenced_widgets": [
+ "119b38887a7044c3adecda48e34a171c",
+ "da3cdc7228ca4857b9cc2059d172942d",
+ "fcb5ff7dbef141b587166e4907b43852",
+ "ea6f3e9d8e494193b08118820f53476c",
+ "ae84e2c9b2514e2f98af669dee2387b2",
+ "1e161fa8e44947dc8c4210c6db49fd90"
+ ]
},
- "orig_nbformat": 4
+ "id": "cD-S3cZ-S_Ta",
+ "outputId": "c068301a-f806-442b-9995-ef77b5fff94d"
+ },
+ "outputs": [],
+ "source": [
+ "# Case 15: Identical embedded RDF-star statements with different asserted statements\n",
+ "\n",
+ "\"\"\"\n",
+ "@prefix ex: .\n",
+ "@prefix rdf: .\n",
+ "\n",
+ "<< ex:Mary ex:likes ex:Matt >> ex:certainty 0.5 .\n",
+ "<< ex:Mary ex:likes ex:Matt >> ex:source \"text\" .\n",
+ "\"\"\"\n",
+ "\n",
+ "data = \"\"\"\n",
+ "@prefix ex: .\n",
+ "@prefix rdf: .\n",
+ "@prefix xsd: .\n",
+ "\n",
+ "[] a rdf:Statement;\n",
+ " rdf:subject ex:Mary;\n",
+ " rdf:predicate ex:likes;\n",
+ " rdf:object ex:Matt;\n",
+ " ex:certainty 0.5 .\n",
+ "\n",
+ "[] a rdf:Statement;\n",
+ " rdf:subject ex:Mary;\n",
+ " rdf:predicate ex:likes;\n",
+ " rdf:object ex:Matt;\n",
+ " ex:source \"text\" .\n",
+ "\n",
+ "\"\"\"\n",
+ "\n",
+ "g = Graph()\n",
+ "g.parse(data=data)\n",
+ "\n",
+ "adbrdf.rdf_to_arangodb_by_rpt(\"DataRPT\", g, overwrite_graph=True)\n",
+ "adbrdf.rdf_to_arangodb_by_pgt(\"DataPGT\", g, overwrite_graph=True)"
+ ]
+ },
+ {
+ "attachments": {},
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "0SWi4e3wIMtw"
+ },
+ "source": [
+ "#### RDF to ArangoDB w/ Graph Contextualization"
+ ]
+ },
+ {
+ "attachments": {},
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "vec21mb9MkhR"
+ },
+ "source": [
+ "Contextualizing an RDF Graph within ArangoDB is a work-in-progress feature that attempts to enhance the Terminology Box of the original RDF Graph. This is done by:\n",
+ "\n",
+ "1. Loading the OWL, RDF, and RDFS Ontologies as 3 sub-graphs via `ArangoRDF.load_meta_ontology()`\n",
+ "2. Setting the `contextualize_graph` flag to `True` in any of the `rdf_to_arangodb` methods.\n"
+ ]
+ },
+ {
+ "attachments": {},
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "dUOCXzn5Owhj"
+ },
+ "source": [
+ "Enabling the `contextualize_graph` flag currently provides the following features:\n",
+ "\n",
+ "1. Process RDF Predicates within the RDF Graph as their own ArangoDB Document, and cast a (predicate RDF.type RDF.Property) edge relationship into the ArangoDB graph for every RDF predicate used in the form (subject predicate object) within the RDF Graph.\n",
+ "\n",
+ "2. Provide RDFS.Domain & RDFS.Range Inference on all RDF Resources within the RDF Graph, so long that no RDF.Type statement already exists in RDF Graph for the given resource.\n",
+ "\n",
+ "3. Provide RDFS.Domain & RDFS.Range Introspection on all RDF Predicates within the RDF Graph, so long that no RDFS.Domain or RDFS.Range statement already exists for the given predicate."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "id": "P9oGi91RJbAI"
+ },
+ "outputs": [],
+ "source": [
+ "data = \"\"\"\n",
+ "PREFIX : \n",
+ "PREFIX rdf: \n",
+ "PREFIX rdfs: \n",
+ "PREFIX xsd: \n",
+ "\n",
+ ":The_Beatles rdf:type :Band .\n",
+ ":The_Beatles :name \"The Beatles\" .\n",
+ ":The_Beatles :member :John_Lennon .\n",
+ ":The_Beatles :member :Paul_McCartney .\n",
+ ":The_Beatles :member :Ringo_Starr .\n",
+ ":The_Beatles :member :George_Harrison .\n",
+ ":John_Lennon rdf:type :SoloArtist .\n",
+ ":Paul_McCartney rdf:type :SoloArtist .\n",
+ ":Ringo_Starr rdf:type :SoloArtist .\n",
+ ":George_Harrison rdf:type :SoloArtist .\n",
+ ":Please_Please_Me rdf:type :Album .\n",
+ ":Please_Please_Me :name \"Please Please Me\" .\n",
+ ":Please_Please_Me :date \"1963-03-22\"^^xsd:date .\n",
+ ":Please_Please_Me :artist :The_Beatles .\n",
+ ":Please_Please_Me :track :Love_Me_Do .\n",
+ ":Love_Me_Do rdf:type :Song .\n",
+ ":Love_Me_Do :name \"Love Me Do\" .\n",
+ ":Love_Me_Do :length 125 .\n",
+ ":Love_Me_Do :writer :John_Lennon .\n",
+ ":Love_Me_Do :writer :Paul_McCartney .\n",
+ "\n",
+ ":McCartney rdf:type :Album .\n",
+ ":McCartney :name \"McCartney\" .\n",
+ ":McCartney :date \"1970-04-17\"^^xsd:date .\n",
+ ":McCartney :artist :Paul_McCartney .\n",
+ "\n",
+ ":Imagine rdf:type :Album .\n",
+ ":Imagine :name \"Imagine\" .\n",
+ ":Imagine :date \"1971-10-11\"^^xsd:date .\n",
+ ":Imagine :artist :John_Lennon .\n",
+ "\"\"\"\n",
+ "\n",
+ "g = Graph()\n",
+ "g.parse(data=data)\n",
+ "\n",
+ "g = adbrdf.load_meta_ontology(g)\n",
+ "\n",
+ "adbrdf.rdf_to_arangodb_by_rpt(\"DataRPT\", g, contextualize_graph=True, overwrite_graph=True)\n",
+ "adbrdf.rdf_to_arangodb_by_pgt(\"DataPGT\", g, contextualize_graph=True, overwrite_graph=True)"
+ ]
+ },
+ {
+ "attachments": {},
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "9gBg-hDs77i7"
+ },
+ "source": [
+ "# ArangoDB to RDF"
+ ]
+ },
+ {
+ "attachments": {},
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "_kkM4P0fWR4e"
+ },
+ "source": [
+ "The `arangodb_graph_to_rdf` and `arangodb_collections_to_rdf` methods return two objects:\n",
+ "\n",
+ "1. The RDF representation of the ArangoDB Graph, i.e `rdf_graph`\n",
+ "2. Another RDF Graph mapping the RDF Resources to their designated ArangoDB Collection, i.e `adb_mapping`.\n",
+ "\n",
+ "The second graph, `adb_mapping`, can be re-used in the RDF to ArangoDB (PGT) process to maintain the Document-to-Collection mappings."
+ ]
+ },
+ {
+ "attachments": {},
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "UCQ9ppnUQa7e"
+ },
+ "source": [
+ "#### Non-native"
+ ]
+ },
+ {
+ "attachments": {},
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "mYZIEAzhQ5CO"
+ },
+ "source": [
+ "Non-native: An ArangoDB Graph that originates from an RDF Context, which has been brought over via one of the `rdf_to_arangodb` methods (RPT/PGT)."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "id": "orwoPEIOQjHO"
+ },
+ "outputs": [],
+ "source": [
+ "data = \"\"\"\n",
+ "PREFIX : \n",
+ "PREFIX rdf: \n",
+ "PREFIX rdfs: \n",
+ "PREFIX xsd: \n",
+ "\n",
+ ":The_Beatles rdf:type :Band .\n",
+ ":The_Beatles :name \"The Beatles\" .\n",
+ ":The_Beatles :member :John_Lennon .\n",
+ ":The_Beatles :member :Paul_McCartney .\n",
+ ":The_Beatles :member :Ringo_Starr .\n",
+ ":The_Beatles :member :George_Harrison .\n",
+ ":John_Lennon rdf:type :SoloArtist .\n",
+ ":Paul_McCartney rdf:type :SoloArtist .\n",
+ ":Ringo_Starr rdf:type :SoloArtist .\n",
+ ":George_Harrison rdf:type :SoloArtist .\n",
+ ":Please_Please_Me rdf:type :Album .\n",
+ ":Please_Please_Me :name \"Please Please Me\" .\n",
+ ":Please_Please_Me :date \"1963-03-22\"^^xsd:date .\n",
+ ":Please_Please_Me :artist :The_Beatles .\n",
+ ":Please_Please_Me :track :Love_Me_Do .\n",
+ ":Love_Me_Do rdf:type :Song .\n",
+ ":Love_Me_Do :name \"Love Me Do\" .\n",
+ ":Love_Me_Do :length 125 .\n",
+ ":Love_Me_Do :writer :John_Lennon .\n",
+ ":Love_Me_Do :writer :Paul_McCartney .\n",
+ "\n",
+ ":McCartney rdf:type :Album .\n",
+ ":McCartney :name \"McCartney\" .\n",
+ ":McCartney :date \"1970-04-17\"^^xsd:date .\n",
+ ":McCartney :artist :Paul_McCartney .\n",
+ "\n",
+ ":Imagine rdf:type :Album .\n",
+ ":Imagine :name \"Imagine\" .\n",
+ ":Imagine :date \"1971-10-11\"^^xsd:date .\n",
+ ":Imagine :artist :John_Lennon .\n",
+ "\"\"\"\n",
+ "\n",
+ "g = Graph()\n",
+ "g.parse(data=data)\n",
+ "\n",
+ "# Selecting RPT or PGT for this example does not matter, as the\n",
+ "# end-result is the same.\n",
+ "adbrdf.rdf_to_arangodb_by_pgt(\"DataPGT\", g, overwrite_graph=True)\n",
+ "# adbrdf.rdf_to_arangodb_by_rpt(\"DataRPT\", g, overwrite_graph=True)\n",
+ "\n",
+ "# ArangoDB to RDF via Graph Name\n",
+ "g2, adb_mapping_2 = adbrdf.arangodb_graph_to_rdf(\"DataPGT\", Graph())\n",
+ "\n",
+ "# ArangoDB to RDF via Collection Names\n",
+ "g3, adb_mapping_3 = adbrdf.arangodb_collections_to_rdf(\n",
+ " \"DataPGT\",\n",
+ " Graph(),\n",
+ " v_cols={\"Album\", \"Band\", \"Class\", \"Property\", \"SoloArtist\", \"Song\"},\n",
+ " e_cols={\"artist\", \"member\", \"track\", \"type\", \"writer\"},\n",
+ ")\n",
+ "\n",
+ "print(len(g2), len(adb_mapping_2))\n",
+ "print(len(g3), len(adb_mapping_3))\n",
+ "\n",
+ "print('--------------------')\n",
+ "print(g2.serialize())\n",
+ "print('--------------------')\n",
+ "print(adb_mapping_2.serialize())\n",
+ "print('--------------------')"
+ ]
+ },
+ {
+ "attachments": {},
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "uxp9AW7kQkM5"
+ },
+ "source": [
+ "#### Native"
+ ]
+ },
+ {
+ "attachments": {},
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "xoza5AvUVqWP"
+ },
+ "source": [
+ "Native: ArangoDB Graphs that originate from an ArangoDB context. We'll be using the [ArangoDB Game Of Thrones Dataset](https://github.com/arangodb/example-datasets/tree/master/GameOfThrones).\n",
+ "\n",
+ "Since we are dealing with a \"native\" ArangoDB Graph, we can rely on the `infer_type_from_adb_col` flag to indicate that `rdf:type` statements of the form (adb_doc rdf:type adb_col) should be inferred upon transferring ArangoDB Documents into RDF.\n",
+ "\n",
+ "We can also take advantage of the `include_adb_key_statements` flag to indicate that `adb:key` statements of the form (adb_doc adb:key adb_doc[\"key\"]) should be generated upon transferring ArangoDB Documents into RDF.\n",
+ "\n",
+ "Note that enabling `infer_type_from_adb_col` `include_adb_key_statements` is only recommended if your ArangoDB graph is \"native\" to ArangoDB. That is, the ArangoDB graph does not originate from an RDF context.\n",
+ "\n",
+ "Finally, we set the `list_conversion_mode` flag to `collection` to indicate that JSON Lists within ArangoDB Documents should be converted into RDF Collections (other options include `container`, and `static`). "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "id": "yQ85OY7paqMM"
+ },
+ "outputs": [],
+ "source": [
+ "rdf_graph, adb_mapping = adbrdf.arangodb_graph_to_rdf(\"GameOfThrones\", rdf_graph=Graph(), list_conversion_mode=\"collection\", infer_type_from_adb_v_col=True, include_adb_key_statements=True)\n",
+ "print(rdf_graph.serialize())"
+ ]
+ }
+ ],
+ "metadata": {
+ "colab": {
+ "collapsed_sections": [
+ "44mc2EvIAzDy",
+ "yRuJ3OIGE2Yr",
+ "KnQifktFAxHx",
+ "7y81WHO8eG8_",
+ "QfE_tKxneG9A",
+ "znQCjOwt7zBz",
+ "0qry3Bcy-160",
+ "mRutdKii-Pk5",
+ "0SWi4e3wIMtw",
+ "9gBg-hDs77i7",
+ "UCQ9ppnUQa7e",
+ "uxp9AW7kQkM5"
+ ],
+ "provenance": []
+ },
+ "gpuClass": "standard",
+ "kernelspec": {
+ "display_name": "Python 3",
+ "name": "python3"
},
- "nbformat": 4,
- "nbformat_minor": 0
+ "language_info": {
+ "name": "python"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 0
}
diff --git a/examples/data/airport-ontology.owl b/examples/data/airport-ontology.owl
deleted file mode 100644
index 75160d16..00000000
--- a/examples/data/airport-ontology.owl
+++ /dev/null
@@ -1,684 +0,0 @@
-
-
-
-
- Airport Ontology
- 2018-05-09T04:24:15.108Z
-
- Airport Ontology
-
-
- <http://cambridgesemantics.com/ontologies/2009/05/LinkedData#linkingUriPrefix>/<http://cambridgesemantics.com/ontologies/2009/05/LinkedData#linkingTypeName>/<http://data.sfgov.org/ontology#hasActivityPeriodValue>
-
- Normalizes Activity Period from source data
- ActivityPeriod
- Normalizes Activity Period from source data
- ActivityPeriod
-
-
-
- <http://cambridgesemantics.com/ontologies/2009/05/LinkedData#linkingUriPrefix>/<http://cambridgesemantics.com/ontologies/2009/05/LinkedData#linkingTypeName>/<http://data.sfgov.org/ontology#hasTailNumber>
-
- Specific Aircraft identified by Tail Number
- Aircraft
- Specific Aircraft identified by Tail Number
- Aircraft
-
-
-
-
- Aircraft Body Style
- Aircraft Body Style
-
-
- Aircraft Configuration
- Aircraft Configuration
-
-
-
-
- Aircraft Manufacturer
- Aircraft Manufacturer
-
-
-
- <http://cambridgesemantics.com/ontologies/2009/05/LinkedData#linkingUriPrefix>/<http://cambridgesemantics.com/ontologies/2009/05/LinkedData#linkingTypeName>/<http://data.sfgov.org/ontology#hasModelName>
-
- Aircraft Model
- Aircraft Model
-
-
-
-
- <http://cambridgesemantics.com/ontologies/2009/05/LinkedData#linkingUriPrefix>/<http://cambridgesemantics.com/ontologies/2009/05/LinkedData#linkingTypeName>/<http://data.sfgov.org/ontology#hasModelVersion>
-
- Aircraft Model Version
- Aircraft Model Version
-
-
-
-
-
- Airline
-
-
-
- <http://cambridgesemantics.com/ontologies/2009/05/LinkedData#linkingUriPrefix>/<http://cambridgesemantics.com/ontologies/2009/05/LinkedData#linkingTypeName>/<http://data.sfgov.org/ontology#hasAirportName>
-
- Airport
- Airport
-
-
- Airport Activity
- Airport Activity
-
-
-
- <http://cambridgesemantics.com/ontologies/2009/05/LinkedData#linkingUriPrefix>/<http://www.w3.org/1999/02/22-rdf-syntax-ns#type>/<http://data.sfgov.org/ontology#p_hasAirportName>/<http://data.sfgov.org/ontology#hasTerminalName>
-
- Airport Terminal
- Airport Terminal
-
-
- Boarding Area
-
-
-
-
-
-
-
- Combi Aircraft
- Combi Aircraft
-
-
-
-
- <http://cambridgesemantics.com/ontologies/2009/05/LinkedData#linkingUriPrefix>Company/<http://data.sfgov.org/ontology#hasCompanyName>
-
- Company
- Company
-
-
-
-
- Freighter Aircraft
- Freighter Aircraft
-
-
-
- <http://cambridgesemantics.com/ontologies/2009/05/LinkedData#linkingUriPrefix>/<http://cambridgesemantics.com/ontologies/2009/05/LinkedData#linkingTypeName>/<http://data.sfgov.org/ontology#hasRegionName>
-
- Geo Region
- Geo Region
-
-
-
-
-
- has activity period
-
- has activity period
-
-
-
-
-
-
- has Activity Period Value
-
- has Activity Period Value
-
-
-
-
-
- has aircraft id
-
-
-
-
-
- has aircraft manufacturer
-
-
-
-
-
- has aircraft model
-
-
-
-
-
- has Aircraft Model Version
-
- has Aircraft Model Version
-
-
-
-
-
- has Aircraft Model Version
-
- has Aircraft Model Version
-
-
-
-
-
- has airport IATA2Digit code
-
-
-
-
- has Airport Name
-
- has Airport Name
-
-
-
-
-
- has Boarding Area Terminal
-
- has Boarding Area Terminal
-
-
-
-
-
- has company name
-
-
-
-
-
- has creation date
-
-
-
-
- has geo region
-
- has geo region
-
-
-
-
-
- has Airline IATA 2 digit Code
-
-
-
-
-
- has Landing Airport
-
- has Landing Airport
-
-
-
-
-
- has landing count
-
-
-
-
-
- has Manufacturer
-
- has Manufacturer
-
-
-
-
- has model name
-
- has model name
-
-
-
-
- has Model Version Name
-
- has Model Version Name
-
-
-
-
-
- has modification date
-
-
-
-
-
- has operating airline
-
-
-
-
-
- has Passenger Boarding Area
-
- has Passenger Boarding Area
-
-
-
-
-
- has passenger count
-
-
-
-
-
- has published airline
-
-
-
-
-
- has region name
-
-
-
-
-
- hasregion summary
-
-
-
-
-
-
- has tail number
-
- has tail number
-
-
-
-
-
- has Terminal Airport
-
- has Terminal Airport
-
-
-
-
-
- has boarding area name
-
- has boarding area name
-
-
-
-
- has Terminal Name
-
- has Terminal Name
-
-
-
-
-
- has total landed weight
-
-
-
-
- has Version
-
- has Version
-
-
-
-
-
- is active
-
-
-
-
-
- is Version Of
-
- is Version Of
-
-
-
-
-
- Landing Activity
- Landing Activity
-
-
-
-
-
- Narrow Body
- Narrow Body
-
-
-
-
-
- owning airline
-
-
-
-
-
- has Aircraft Model
-
- has Aircraft Model
-
-
-
-
-
- hasAirportName
-
- hasAirportName
-
-
-
-
-
- <http://cambridgesemantics.com/ontologies/2009/05/LinkedData#linkingUriPrefix>/<http://cambridgesemantics.com/ontologies/2009/05/LinkedData#linkingTypeName>/<http://cambridgesemantics.com/ontologies/2009/05/LinkedData#linkingUniqueID>
-
- Passenger Activity
- Passenger Activity
-
-
-
-
-
- Passenger Aircraft
- Passenger Aircraft
-
-
-
-
-
-
- Passenger Deplaned
- Passenger Deplaned
-
-
-
-
-
-
- Passenger Enplaned
- Passenger Enplaned
-
-
-
-
-
-
- Passenger Transit
- Passenger Transit
-
-
-
-
-
- Regional Jet
- Regional Jet
-
-
-
-
-
- Turbo Prop
- Turbo Prop
-
-
-
-
-
- Wide body
-
-
-
-
-
-
-
- /
-
-
-
-
-
-
-
- 1
- 1
-
-
-
-
-
-
-
-
-
-
-
- /
-
-
-
- /
-
-
-
-
-
-
-
- /
-
-
-
- /
-
-
-
-
-
-
-
- /
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- /
-
-
-
-
-
-
- /
-
-
-
-
-
-
-
- /
-
-
-
- 1
- 1
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- /
-
-
-
- /
-
-
-
-
-
-
-
- 1
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- /
-
-
-
-
-
-
-
-
-
-
-
- Company/
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- /
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 1
- 1
-
-
-
- /
-
-
-
-
-
-
-
- /
-
-
-
- 1
-
-
-
- /
-
-
-
-
-
-
-
-
-
-
- /
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/examples/data/sfo-aircraft-partial.ttl b/examples/data/sfo-aircraft-partial.ttl
deleted file mode 100644
index ce924c19..00000000
--- a/examples/data/sfo-aircraft-partial.ttl
+++ /dev/null
@@ -1,35 +0,0 @@
- ;
- "A321" ;
- a .
-
- "200" ;
- a ;
- a .
-
- ;
-