Repository: ArangoDB-Community/ArangoRDF (language: python)

Issue:
Consider bulk import into ArangoDB
The ArangoRDF `import_rdf` API is currently inserting RDF objects into ArangoDB [one at a time](https://github.com/ArangoDB-Community/ArangoRDF/blob/main/arango_rdf/main.py#L437-L470), resulting in having to make **at least one HTTP request per object to insert**. 

We should consider re-configuring how the RDF data is handled such that we can make use of https://docs.python-arango.com/en/dev/specs.html#arango.collection.StandardCollection.import_bulk


Discussion / hints:

Fix patch:
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..305a9b0
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,2 @@
+*.ipynb filter=nbstripout
+*.ipynb diff=ipynb
diff --git a/.gitchangelog.rc b/.gitchangelog.rc
new file mode 100644
index 0000000..e3a6284
--- /dev/null
+++ b/.gitchangelog.rc
@@ -0,0 +1,290 @@
+# -*- coding: utf-8; mode: python -*-
+##
+## Format
+##
+##   ACTION: [AUDIENCE:] COMMIT_MSG [!TAG ...]
+##
+## Description
+##
+##   ACTION is one of 'chg', 'fix', 'new'
+##
+##       Is WHAT the change is about.
+##
+##       'chg' is for refactor, small improvement, cosmetic changes...
+##       'fix' is for bug fixes
+##       'new' is for new features, big improvement
+##
+##   AUDIENCE is optional and one of 'dev', 'usr', 'pkg', 'test', 'doc'
+##
+##       Is WHO is concerned by the change.
+##
+##       'dev'  is for developpers (API changes, refactors...)
+##       'usr'  is for final users (UI changes)
+##       'pkg'  is for packagers   (packaging changes)
+##       'test' is for testers     (test only related changes)
+##       'doc'  is for doc guys    (doc only changes)
+##
+##   COMMIT_MSG is ... well ... the commit message itself.
+##
+##   TAGs are additionnal adjective as 'refactor' 'minor' 'cosmetic'
+##
+##       They are preceded with a '!' or a '@' (prefer the former, as the
+##       latter is wrongly interpreted in github.) Commonly used tags are:
+##
+##       'refactor' is obviously for refactoring code only
+##       'minor' is for a very meaningless change (a typo, adding a comment)
+##       'cosmetic' is for cosmetic driven change (re-indentation, 80-col...)
+##       'wip' is for partial functionality but complete subfunctionality.
+##
+## Example:
+##
+##   new: usr: support of bazaar implemented
+##   chg: re-indentend some lines !cosmetic
+##   new: dev: updated code to be compatible with last version of killer lib.
+##   fix: pkg: updated year of licence coverage.
+##   new: test: added a bunch of test around user usability of feature X.
+##   fix: typo in spelling my name in comment. !minor
+##
+##   Please note that multi-line commit message are supported, and only the
+##   first line will be considered as the "summary" of the commit message. So
+##   tags, and other rules only applies to the summary.  The body of the commit
+##   message will be displayed in the changelog without reformatting.
+
+
+##
+## ``ignore_regexps`` is a line of regexps
+##
+## Any commit having its full commit message matching any regexp listed here
+## will be ignored and won't be reported in the changelog.
+##
+ignore_regexps = [
+    r'@minor', r'!minor',
+    r'@cosmetic', r'!cosmetic',
+    r'@refactor', r'!refactor',
+    r'!gitchangelog',
+    r'@wip', r'!wip',
+    r'^([cC]hg|[fF]ix|[nN]ew)\s*:\s*[p|P]kg:',
+    r'^([cC]hg|[fF]ix|[nN]ew)\s*:\s*[d|D]ev:',
+    r'^(.{3,3}\s*:)?\s*[fF]irst commit.?\s*$',
+    r'^$',  ## ignore commits with empty messages
+]
+
+
+## ``section_regexps`` is a list of 2-tuples associating a string label and a
+## list of regexp
+##
+## Commit messages will be classified in sections thanks to this. Section
+## titles are the label, and a commit is classified under this section if any
+## of the regexps associated is matching.
+##
+## Please note that ``section_regexps`` will only classify commits and won't
+## make any changes to the contents. So you'll probably want to go check
+## ``subject_process`` (or ``body_process``) to do some changes to the subject,
+## whenever you are tweaking this variable.
+##
+section_regexps = [
+    ('New', [
+        r'^[nN]ew\s*:\s*((dev|use?r|pkg|test|doc)\s*:\s*)?([^\n]*)$',
+     ]),
+    ('Changes', [
+        r'^[cC]hg\s*:\s*((dev|use?r|pkg|test|doc)\s*:\s*)?([^\n]*)$',
+     ]),
+    ('Fix', [
+        r'^[fF]ix\s*:\s*((dev|use?r|pkg|test|doc)\s*:\s*)?([^\n]*)$',
+     ]),
+
+    ('Other', None ## Match all lines
+     ),
+
+]
+
+
+## ``body_process`` is a callable
+##
+## This callable will be given the original body and result will
+## be used in the changelog.
+##
+## Available constructs are:
+##
+##   - any python callable that take one txt argument and return txt argument.
+##
+##   - ReSub(pattern, replacement): will apply regexp substitution.
+##
+##   - Indent(chars="  "): will indent the text with the prefix
+##     Please remember that template engines gets also to modify the text and
+##     will usually indent themselves the text if needed.
+##
+##   - Wrap(regexp=r"\n\n"): re-wrap text in separate paragraph to fill 80-Columns
+##
+##   - noop: do nothing
+##
+##   - ucfirst: ensure the first letter is uppercase.
+##     (usually used in the ``subject_process`` pipeline)
+##
+##   - final_dot: ensure text finishes with a dot
+##     (usually used in the ``subject_process`` pipeline)
+##
+##   - strip: remove any spaces before or after the content of the string
+##
+##   - SetIfEmpty(msg="No commit message."): will set the text to
+##     whatever given ``msg`` if the current text is empty.
+##
+## Additionally, you can `pipe` the provided filters, for instance:
+#body_process = Wrap(regexp=r'\n(?=\w+\s*:)') | Indent(chars="  ")
+#body_process = Wrap(regexp=r'\n(?=\w+\s*:)')
+#body_process = noop
+body_process = ReSub(r'((^|\n)[A-Z]\w+(-\w+)*: .*(\n\s+.*)*)+$', r'') | strip
+
+
+## ``subject_process`` is a callable
+##
+## This callable will be given the original subject and result will
+## be used in the changelog.
+##
+## Available constructs are those listed in ``body_process`` doc.
+subject_process = (strip |
+    ReSub(r'^([cC]hg|[fF]ix|[nN]ew)\s*:\s*((dev|use?r|pkg|test|doc)\s*:\s*)?([^\n@]*)(@[a-z]+\s+)*$', r'\4') |
+    SetIfEmpty("No commit message.") | ucfirst | final_dot)
+
+
+## ``tag_filter_regexp`` is a regexp
+##
+## Tags that will be used for the changelog must match this regexp.
+##
+tag_filter_regexp = r'^[0-9]+\.[0-9]+(\.[0-9]+)?((a|b|rc)[0-9]+)?(\.post[0-9]+)?(\.dev[0-9]+)?$'
+
+
+## ``unreleased_version_label`` is a string or a callable that outputs a string
+##
+## This label will be used as the changelog Title of the last set of changes
+## between last valid tag and HEAD if any.
+unreleased_version_label = "(unreleased)"
+
+
+## ``output_engine`` is a callable
+##
+## This will change the output format of the generated changelog file
+##
+## Available choices are:
+##
+##   - rest_py
+##
+##        Legacy pure python engine, outputs ReSTructured text.
+##        This is the default.
+##
+##   - mustache(<template_name>)
+##
+##        Template name could be any of the available templates in
+##        ``templates/mustache/*.tpl``.
+##        Requires python package ``pystache``.
+##        Examples:
+##           - mustache("markdown")
+##           - mustache("restructuredtext")
+##
+##   - makotemplate(<template_name>)
+##
+##        Template name could be any of the available templates in
+##        ``templates/mako/*.tpl``.
+##        Requires python package ``mako``.
+##        Examples:
+##           - makotemplate("restructuredtext")
+##
+#output_engine = rest_py
+#output_engine = mustache("restructuredtext")
+output_engine = mustache("markdown")
+#output_engine = makotemplate("restructuredtext")
+
+
+## ``include_merge`` is a boolean
+##
+## This option tells git-log whether to include merge commits in the log.
+## The default is to include them.
+include_merge = True
+
+
+## ``log_encoding`` is a string identifier
+##
+## This option tells gitchangelog what encoding is outputed by ``git log``.
+## The default is to be clever about it: it checks ``git config`` for
+## ``i18n.logOutputEncoding``, and if not found will default to git's own
+## default: ``utf-8``.
+#log_encoding = 'utf-8'
+
+
+## ``publish`` is a callable
+##
+## Sets what ``gitchangelog`` should do with the output generated by
+## the output engine. ``publish`` is a callable taking one argument
+## that is an interator on lines from the output engine.
+##
+## Some helper callable are provided:
+##
+## Available choices are:
+##
+##   - stdout
+##
+##        Outputs directly to standard output
+##        (This is the default)
+##
+##   - FileInsertAtFirstRegexMatch(file, pattern, idx=lamda m: m.start())
+##
+##        Creates a callable that will parse given file for the given
+##        regex pattern and will insert the output in the file.
+##        ``idx`` is a callable that receive the matching object and
+##        must return a integer index point where to insert the
+##        the output in the file. Default is to return the position of
+##        the start of the matched string.
+##
+##   - FileRegexSubst(file, pattern, replace, flags)
+##
+##        Apply a replace inplace in the given file. Your regex pattern must
+##        take care of everything and might be more complex. Check the README
+##        for a complete copy-pastable example.
+##
+# publish = FileInsertIntoFirstRegexMatch(
+#     "CHANGELOG.rst",
+#     r'/(?P<rev>[0-9]+\.[0-9]+(\.[0-9]+)?)\s+\([0-9]+-[0-9]{2}-[0-9]{2}\)\n--+\n/',
+#     idx=lambda m: m.start(1)
+# )
+#publish = stdout
+
+
+## ``revs`` is a list of callable or a list of string
+##
+## callable will be called to resolve as strings and allow dynamical
+## computation of these. The result will be used as revisions for
+## gitchangelog (as if directly stated on the command line). This allows
+## to filter exaclty which commits will be read by gitchangelog.
+##
+## To get a full documentation on the format of these strings, please
+## refer to the ``git rev-list`` arguments. There are many examples.
+##
+## Using callables is especially useful, for instance, if you
+## are using gitchangelog to generate incrementally your changelog.
+##
+## Some helpers are provided, you can use them::
+##
+##   - FileFirstRegexMatch(file, pattern): will return a callable that will
+##     return the first string match for the given pattern in the given file.
+##     If you use named sub-patterns in your regex pattern, it'll output only
+##     the string matching the regex pattern named "rev".
+##
+##   - Caret(rev): will return the rev prefixed by a "^", which is a
+##     way to remove the given revision and all its ancestor.
+##
+## Please note that if you provide a rev-list on the command line, it'll
+## replace this value (which will then be ignored).
+##
+## If empty, then ``gitchangelog`` will act as it had to generate a full
+## changelog.
+##
+## The default is to use all commits to make the changelog.
+#revs = ["^1.0.3", ]
+#revs = [
+#    Caret(
+#        FileFirstRegexMatch(
+#            "CHANGELOG.rst",
+#            r"(?P<rev>[0-9]+\.[0-9]+(\.[0-9]+)?)\s+\([0-9]+-[0-9]{2}-[0-9]{2}\)\n--+\n")),
+#    "HEAD"
+#]
+revs = []
\ No newline at end of file
diff --git a/.github/workflows/analyze.yml b/.github/workflows/analyze.yml
new file mode 100644
index 0000000..5e798d2
--- /dev/null
+++ b/.github/workflows/analyze.yml
@@ -0,0 +1,69 @@
+# For most projects, this workflow file will not need changing; you simply need
+# to commit it to your repository.
+#
+# You may wish to alter this file to override the set of languages analyzed,
+# or to provide custom queries or build logic.
+#
+# ******** NOTE ********
+# We have attempted to detect the languages in your repository. Please check
+# the `language` matrix defined below to confirm you have the correct set of
+# supported CodeQL languages.
+#
+name: analyze
+on:
+  workflow_dispatch:
+  push:
+    branches: [ main ]
+  pull_request:
+    branches: [ main ]
+  schedule:
+    - cron: "00 9 1,15 * *"
+jobs:
+  analyze:
+    name: Analyze
+    runs-on: ubuntu-latest
+    permissions:
+      actions: read
+      contents: read
+      security-events: write
+
+    strategy:
+      fail-fast: false
+      matrix:
+        language: ["python"]
+        # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
+        # Learn more:
+        # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
+
+    steps:
+      - name: Checkout repository
+        uses: actions/checkout@v2
+
+      # Initializes the CodeQL tools for scanning.
+      - name: Initialize CodeQL
+        uses: github/codeql-action/init@v2
+        with:
+          languages: ${{ matrix.language }}
+          # If you wish to specify custom queries, you can do so here or in a config file.
+          # By default, queries listed here will override any specified in a config file.
+          # Prefix the list here with "+" to use these queries and those in the config file.
+          # queries: ./path/to/local/query, your-org/your-repo/queries@main
+
+      # Autobuild attempts to build any compiled languages  (C/C++, C#, or Java).
+      # If this step fails, then you should remove it and run the build manually (see below)
+      - name: Autobuild
+        uses: github/codeql-action/autobuild@v2
+
+      # ℹ️ Command-line programs to run using the OS shell.
+      # 📚 https://git.io/JvXDl
+
+      # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
+      #    and modify them (or add more) to build your code if your project
+      #    uses a compiled language
+
+      #- run: |
+      #   make bootstrap
+      #   make release
+
+      - name: Perform CodeQL Analysis
+        uses: github/codeql-action/analyze@v2
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
new file mode 100644
index 0000000..1baa8cf
--- /dev/null
+++ b/.github/workflows/build.yml
@@ -0,0 +1,42 @@
+name: build
+on:
+  workflow_dispatch:
+  push:
+    branches: [ main ]
+  pull_request:
+    branches: [ main ]
+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
\ No newline at end of file
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 0000000..8b6a666
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,127 @@
+name: release
+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
+
+      - name: Fetch complete history for all tags and branches
+        run: git fetch --prune --unshallow
+
+      - name: Setup python
+        uses: actions/setup-python@v2
+        with:
+          python-version: "3.8"
+
+      - 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
+        env:
+          TWINE_USERNAME: __token__
+          TWINE_PASSWORD: ${{ secrets.TWINE_PASSWORD_TEST }}
+        run: twine upload --repository testpypi dist/* #--skip-existing
+      - name: Publish to PyPI
+        env:
+          TWINE_USERNAME: __token__
+          TWINE_PASSWORD: ${{ secrets.TWINE_PASSWORD }}
+        run: twine upload --repository pypi dist/* #--skip-existing
+
+  changelog:
+    needs: release
+    runs-on: ubuntu-latest
+    name: Update Changelog
+    steps:
+      - uses: actions/checkout@v2
+        with:
+          fetch-depth: 0
+
+      - name: Create new branch
+        run: git checkout -b actions/changelog
+
+      - name: Set branch upstream
+        run: git push -u origin actions/changelog
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
+      - name: Setup python
+        uses: actions/setup-python@v2
+        with:
+          python-version: "3.8"
+
+      - name: Install release packages
+        run: pip install wheel gitchangelog pystache
+
+      - name: Set variables
+        run: echo "VERSION=$(curl ${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/releases/latest | python -c "import sys; import json; print(json.load(sys.stdin)['tag_name'])")" >> $GITHUB_ENV
+
+      - name: Generate newest changelog
+        run: gitchangelog ${{env.VERSION}} > CHANGELOG.md
+
+      - name: Make commit for auto-generated changelog
+        uses: EndBug/add-and-commit@v7
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+        with:
+          add: "CHANGELOG.md"
+          branch: actions/changelog
+          message: "!gitchangelog"
+
+      - name: Create pull request for the auto generated changelog
+        run: |
+          echo "PR_URL=$(gh pr create \
+            --title "changelog: release ${{env.VERSION}}" \
+            --body "beep boop, i am a robot" \
+            --label documentation)" >> $GITHUB_ENV
+        env:
+          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
diff --git a/.gitignore b/.gitignore
index 1ff228e..56dcf1f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -116,6 +116,9 @@ venv.bak/
 localdata/
 
 # setuptools_scm
-adbdgl_adapter/version.py
+arango_rdf/version.py
 
-.vscode
\ No newline at end of file
+.vscode
+
+# export rdf file
+examples/data/rdfExport.xml
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..e69de29
diff --git a/MANIFEST.in b/MANIFEST.in
index 60086e3..3d73851 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -1,5 +1,3 @@
 include README.md LICENSE
-include examples/assets/adb_logo.png
-include examples/assets/rdf_logo.png
 prune tests
 prune examples
\ No newline at end of file
diff --git a/README.md b/README.md
index 585ece3..783de97 100644
--- a/README.md
+++ b/README.md
@@ -3,6 +3,11 @@ ___
 
 # Arango-RDF
 
+[![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)
+[![Coverage Status](https://coveralls.io/repos/github/ArangoDB-Community/ArangoRDF/badge.svg?branch=main)](https://coveralls.io/github/ArangoDB-Community/ArangoRDF?branch=main)
+[![Last commit](https://img.shields.io/github/last-commit/ArangoDB-Community/ArangoRDF)](https://github.com/ArangoDB-Community/ArangoRDF/commits/main)
+
 [![PyPI version badge](https://img.shields.io/pypi/v/arango-rdf?color=3775A9&style=for-the-badge&logo=pypi&logoColor=FFD43B)](https://pypi.org/project/arango-rdf/)
 [![Python versions badge](https://img.shields.io/pypi/pyversions/arango-rdf?color=3776AB&style=for-the-badge&logo=python&logoColor=FFD43B)](https://pypi.org/project/arango-rdf/)
 
@@ -10,8 +15,8 @@ ___
 [![Code style: black](https://img.shields.io/static/v1?style=for-the-badge&label=code%20style&message=black&color=black)](https://github.com/psf/black)
 [![Downloads](https://img.shields.io/badge/dynamic/json?style=for-the-badge&color=282661&label=Downloads&query=total_downloads&url=https://api.pepy.tech/api/projects/arango-rdf)](https://pepy.tech/project/arango-rdf)
 
-<a href="https://www.arangodb.com/" rel="arangodb.com"><img src="./examples/assets/adb_logo.png" width=10%/>
-<a href="https://www.w3.org/RDF/" rel="w3.org/RDF"><img src="./examples/assets/rdf_logo.png" width=7% /></a>
+<a href="https://www.arangodb.com/" rel="arangodb.com"><img src="https://raw.githubusercontent.com/ArangoDB-Community/ArangoRDF/main/examples/assets/adb_logo.png" width=10%/>
+<a href="https://www.w3.org/RDF/" rel="w3.org/RDF"><img src="https://raw.githubusercontent.com/ArangoDB-Community/ArangoRDF/main/examples/assets/rdf_logo.png" width=7% /></a>
 
 Import/Export RDF graphs with ArangoDB
 
@@ -64,10 +69,10 @@ config = {"normalize_literals": False}  # default: False
 adb_rdf.init_rdf_collections(bnode="Blank")
 
 # Start with importing the ontology
-adb_rdf.import_rdf("./examples/data/airport-ontology.owl", format="xml", config=config, save_config=True)
+adb_graph = adb_rdf.import_rdf("./examples/data/airport-ontology.owl", format="xml", config=config, save_config=True)
 
 # Next, let's import the actual graph data
-adb_rdf.import_rdf(f"./examples/data/sfo-aircraft-partial.ttl", format="ttl", config=config, save_config=True)
+adb_graph = adb_rdf.import_rdf(f"./examples/data/sfo-aircraft-partial.ttl", format="ttl", config=config, save_config=True)
 
 
 # RDF Export
@@ -75,7 +80,7 @@ adb_rdf.import_rdf(f"./examples/data/sfo-aircraft-partial.ttl", format="ttl", co
 # Exports ALL collections of the database,
 # currently does not account for default_graph or sub_graph
 # Results may vary, minifying may occur
-adb_rdf.export(f"./examples/data/rdfExport.xml", format="xml")
+rdf_graph = adb_rdf.export_rdf(f"./examples/data/rdfExport.xml", format="xml")
 
 # Drop graph and ALL documents and collections to test import from exported data
 if db.has_graph("default_graph"):
@@ -92,7 +97,7 @@ config = adb_rdf.get_config_by_latest() # gets the last config saved
 # config = adb_rdf.get_config_by_key_value('AnyKeySuppliedInConfig', 'SomeValue')
 
 # Re-import Exported data
-adb_rdf.import_rdf(f"./examples/data/rdfExport.xml", format="xml", config=config)
+adb_graph = adb_rdf.import_rdf(f"./examples/data/rdfExport.xml", format="xml", config=config)
 
 ```
 
@@ -103,4 +108,14 @@ adb_rdf.import_rdf(f"./examples/data/rdfExport.xml", format="xml", config=config
 3. (create virtual environment of choice)
 4. `pip install -e .[dev]`
 5. (create an ArangoDB instance with method of choice)
-6. `python tests/test.py` (assumes `username=root`, `password=openSesame`)
+6. `pytest --url <> --dbName <> --username <> --password <>`
+
+**Note**: A `pytest` parameter can be omitted if the endpoint is using its default value:
+```python
+def pytest_addoption(parser):
+    parser.addoption("--url", action="store", default="http://localhost:8529")
+    parser.addoption("--dbName", action="store", default="_system")
+    parser.addoption("--username", action="store", default="root")
+    parser.addoption("--password", action="store", default="")
+```
+
diff --git a/arango_rdf/main.py b/arango_rdf/main.py
index 815285c..9e5dc90 100644
--- a/arango_rdf/main.py
+++ b/arango_rdf/main.py
@@ -5,17 +5,18 @@
 # author @Arthur Keen (ArangoDB)
 
 import hashlib
-import string
 import sys
 import time
-from typing import Any, List, Optional, Union
+from collections import defaultdict
+from typing import Any, Dict, List, Optional, Union
 
 # from rdflib.namespace import RDFS, OWL
-from arango import ArangoClient
-from arango.collection import EdgeCollection, StandardCollection
-from arango.cursor import Cursor
 from arango.database import StandardDatabase
-from rdflib import BNode, Graph, Literal, URIRef
+from arango.graph import Graph as ArangoGraph
+from rdflib import BNode
+from rdflib import Graph as RDFGraph
+from rdflib import Literal, URIRef
+from tqdm import tqdm
 
 
 class ArangoRDF:
@@ -48,9 +49,10 @@ def __init__(
             self.graph = self.db.create_graph(default_graph)
 
         self.__set_sub_graph = sub_graph is not None
-        self.rdf_graph = Graph(identifier=sub_graph)
+        self.rdf_graph = RDFGraph(identifier=sub_graph)
 
-        self.collections = {}
+        # Maps the default RDF collection names to the user-specified RDF collection names
+        self.col_map: Dict[str, str] = {}
 
     def init_rdf_collections(
         self,
@@ -81,46 +83,6 @@ def init_rdf_collections(
             edge, [iri, bnode], [iri, literal, bnode], "statement"
         )
 
-    # DO NOT USE ontology-specific functions, incomplete
-    def init_ontology_collections(
-        self,
-        cls: str = "Class",
-        rel: str = "Relationship",
-        prop: str = "Property",
-        sub_cls: str = "SubClassOf",
-        sub_prop: str = "SubPropertyOf",
-        range: str = "Range",
-        domain: str = "Domain",
-    ):
-        """
-        Creates the node and edge collections for ontology import.
-
-        Parameters
-        ----------
-        cls: str
-            the name of the collection that will store classes (default is "Class")
-        rel: str
-            the name of the collection that will store relationships (default is "Relationship")
-        prop: str
-            the name of the collection that will store properties (default is "Property")
-        sub_cls: str
-            the name of the edge collection that will connect classes (default is "SubClassOf")
-        sub_prop: str
-            the name of the edge collection that will connect properties (default is "SubPropertyOf")
-        range: str
-            the name of the edge collection that will connect relationships to range classes (default is "Range")
-        domain: str
-            the name of the edge collection that will connect relationships to domain classes (default is "Domain")
-        """
-        self.init_collection(cls, "class")
-        self.init_collection(rel, "rel")
-        self.init_collection(prop, "prop")
-
-        self.init_edge_collection(sub_cls, [cls], [cls], "sub_class")
-        self.init_edge_collection(sub_prop, [prop], [prop], "sub_prop")
-        self.init_edge_collection(range, [rel], [cls], "range")
-        self.init_edge_collection(domain, [rel], [cls], "domain")
-
     def init_collection(self, name: str, default_name: str) -> None:
         """
         Creates collection if it doesn't already exist
@@ -132,11 +94,10 @@ def init_collection(self, name: str, default_name: str) -> None:
         default_name: str
             the name that will be used to reference the collection in the code
         """
-        if self.db.has_collection(name):
-            collection = self.db.collection(name)
-        else:
-            collection = self.db.create_collection(name)
-        self.collections[default_name] = collection
+        if self.db.has_collection(name) is False:
+            self.db.create_collection(name)
+
+        self.col_map[default_name] = name
 
     def init_edge_collection(
         self,
@@ -187,15 +148,14 @@ def init_edge_collection(
                 to_vertex_collections=new_to_vc,
             )
 
-            collection = self.graph.edge_collection(name)
         else:
-            collection = self.graph.create_edge_definition(
+            self.graph.create_edge_definition(
                 edge_collection=name,
                 from_vertex_collections=parent_collections,
                 to_vertex_collections=child_collections,
             )
 
-        self.collections[default_name] = collection
+        self.col_map[default_name] = name
 
     def import_rdf(
         self,
@@ -203,7 +163,8 @@ def import_rdf(
         format: str = "xml",
         config: dict = {},
         save_config: bool = False,
-    ) -> None:
+        **import_options: Any,
+    ) -> ArangoGraph:
         """
         Imports an rdf graph from a file into Arangodb
 
@@ -225,167 +186,119 @@ def import_rdf(
 
         graph_id = self.rdf_graph.identifier.toPython()
 
+        normalize_literals = config.get("normalize_literals", False)
         if config and save_config:
-            config["normalize_literals"] = config.get("normalize_literals", False)
+            config["normalize_literals"] = normalize_literals
             config["default_graph"] = self.default_graph
             if self.__set_sub_graph:
                 config["sub_graph"] = self.sub_graph
 
             self.save_config(config)
 
-        for s, p, o in self.rdf_graph:
+        adb_documents = defaultdict(list)
+
+        file_name = data.split("/")[-1]
+        for s, p, o in tqdm(self.rdf_graph, desc=file_name, colour="#88a049"):
 
             # build subject doc
             if isinstance(s, URIRef):
-                collection = "iri"
-                doc = self.build_iri_doc(s)
+                s_collection = "iri"
+                s_doc = self.build_iri_doc(s)
             elif isinstance(s, BNode):
-                collection = "bnode"
-                doc = self.build_bnode_doc(s)
+                s_collection = "bnode"
+                s_doc = self.build_bnode_doc(s)
             else:
                 raise ValueError("Subject must be IRI or Blank Node")
 
-            s_id = self.insert_doc(self.collections[collection], doc)
+            s_id = self.col_map[s_collection] + "/" + s_doc["_key"]
+            adb_documents[s_collection].append(s_doc)
 
             # build object doc
             if isinstance(o, URIRef):
-                collection = "iri"
-                doc = self.build_iri_doc(o)
-                o_id = self.insert_doc(self.collections[collection], doc)
+                o_collection = "iri"
+                o_doc = self.build_iri_doc(o)
             elif isinstance(o, BNode):
-                collection = "bnode"
-                doc = self.build_bnode_doc(o)
-                o_id = self.insert_doc(self.collections[collection], doc)
+                o_collection = "bnode"
+                o_doc = self.build_bnode_doc(o)
             elif isinstance(o, Literal):
-                collection = "literal"
-                normalize = config.get("normalize_literals", False)
-                doc = self.build_literal_doc(o, normalize)
-                o_id = self.insert_literal_doc(
-                    self.collections[collection], doc, normalize
-                )
+                o_collection = "literal"
+                o_doc = self.build_literal_doc(o, normalize_literals)
             else:
                 raise ValueError("Object must be IRI, Blank Node, or Literal")
 
+            o_id = self.col_map[o_collection] + "/" + o_doc["_key"]
+            adb_documents[o_collection].append(o_doc)
+
             # build and insert edge
             edge = self.build_statement_edge(p, s_id, o_id, graph_id)
 
             # add RDF Graph id as edge property
             if self.__set_sub_graph:
-                edge["graph"] = graph_id
-
-            self.insert_edge(self.collections["statement"], edge)
+                edge["_graph"] = graph_id
 
-        return
+            adb_documents["statement"].append(edge)
 
-    # DO NOT USE ontology-specific functions, incomplete
-    def import_ontology(self, data: str, format: str = "xml") -> None:
-        """
-        Imports an ontology from a file into Arangodb
-
-        Parameters
-        ----------
-        data: str
-            path to rdf file
-        format: str
-            format of the rdf file (default is "xml")
-        """
-
-        self.rdf_graph.parse(data, format=format)
-        graph_id = self.rdf_graph.identifier.toPython()
-
-        for s, p, o in self.rdf_graph:
-            if isinstance(o, Literal) is False:
-                if "#Class" in o.toPython():
-                    self.build_and_insert_node(self.collections["class"], s)
-                elif "#ObjectProperty" in o.toPython():
-                    self.build_and_insert_node(self.collections["rel"], s)
-                elif "#DatatypeProperty" in o.toPython():
-                    self.build_and_insert_node(self.collections["prop"], s)
-                else:
-                    raise ValueError(f"Unrecognized object {o.toPython()}")
-
-            else:
-                # if predicate is subclass of, add s and o to class collection and connect them w/ subClassOf edge
-                if "#subClassOf" in p.toPython():
-                    o_id = self.build_and_insert_node(self.collections["class"], o)
-                    s_id = self.build_and_insert_node(self.collections["class"], s)
+        # Set default ArangoDB `import_bulk` behavior to update/insert
+        if "on_duplicate" not in import_options:
+            import_options["on_duplicate"] = "update"
 
-                    edge = self.build_statement_edge(p, s_id, o_id, graph_id)
-                    self.insert_edge(self.collections["sub_class"], edge)
-
-                # if predicate is #domain create relationship node and connect to class node
-                elif "#domain" in p.toPython():
-                    s_id = self.build_and_insert_node(self.collections["rel"], s)
-                    o_id = self.build_and_insert_node(self.collections["class"], o)
-
-                    edge = self.build_statement_edge(p, s_id, o_id, graph_id)
-                    self.insert_edge(self.collections["domain"], edge)
-
-                elif "#range" in p.toPython():
-                    s_id = self.build_and_insert_node(self.collections["rel"], s)
-                    o_id = self.build_and_insert_node(self.collections["class"], o)
-
-                    edge = self.build_statement_edge(p, s_id, o_id, graph_id)
-                    self.insert_edge(self.collections["range"], edge)
-
-                elif "#subPropertyOf" in p.toPython():
-                    o_id = self.build_and_insert_node(self.collections["prop"], o)
-                    s_id = self.build_and_insert_node(self.collections["prop"], o)
+        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
+            )
 
-                    edge = self.build_statement_edge(p, s_id, o_id, graph_id)
-                    self.insert_edge(self.collections["sub_prop"], edge)
-                else:
-                    raise ValueError(f"Unrecognized predicate {p.toPython()}")
+        return self.graph
 
-    def export(self, file_name: str, format: str) -> None:
+    def export_rdf(
+        self,
+        file_name: Optional[str] = None,
+        format: Optional[str] = None,
+        **query_options: Any,
+    ) -> RDFGraph:
         """
         Builds a rdf graph from the database graph and exports to a file
 
         Parameters
         ----------
-        file_name: str
+        file_name: str | none
             path to where file will be exported
-        format: str
+        format: str | none
             format of the rdf file
         """
         # init rdf graph
-        g = Graph()
-
-        # get all collections from db
-        collections = self.db.collections()
-        names = []
-        for i in collections:
-            if i["name"][0] != "_":
-                names.append(i["name"])
+        g = RDFGraph()
+
+        aql = """
+            FOR edge IN @@col
+                RETURN {
+                    iri: edge["_iri"],
+                    from: DOCUMENT(edge._from),
+                    to: DOCUMENT(edge._to)
+                }
+        """
 
-        all_adb_docs = self.get_all_docs(names)
+        data_cursor = self.db.aql.execute(
+            aql,
+            bind_vars={"@col": self.col_map["statement"]},
+            count=True,
+            **query_options,
+        )
 
-        for n in all_adb_docs:
-            if "_to" in n:
-                # find and build subect/object
-                to_node = self.find_by_id(n["_to"], all_adb_docs)
-                from_node = self.find_by_id(n["_from"], all_adb_docs)
+        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"]
 
-                _iri = n["_iri"]
-                # add triple to graph
-                g.add((from_node, URIRef(_iri), to_node))
+            # add triple to graph
+            g.add((from_node, URIRef(_iri), to_node))
 
         # output graph
-        g.serialize(destination=file_name, format=format)
-
-        return
-
-    def build_and_insert_node(
-        self, collection: StandardCollection, doc: Union[URIRef, BNode]
-    ) -> dict:
-        if isinstance(doc, URIRef):
-            node = self.build_iri_doc(doc)
-        elif isinstance(doc, BNode):
-            node = self.build_bnode_doc(doc)
-        else:
-            raise ValueError("Document must be IRI or Blank Node")
+        if file_name:
+            g.serialize(destination=file_name, format=format)
 
-        return self.insert_doc(collection, node)
+        return g
 
     def build_iri_doc(self, iri: URIRef) -> dict:
         return {
@@ -409,8 +322,10 @@ def build_literal_doc(self, literal: Literal, normalize: bool) -> dict:
         doc = {"_value": value, "_type": type, "_lang": lang}
         if normalize:
             key_string = value + type + lang
-            key = hashlib.md5(key_string.encode("utf-8")).hexdigest()
-            doc["_key"] = key
+        else:
+            key_string = str(time.time())
+
+        doc["_key"] = hashlib.md5(key_string.encode("utf-8")).hexdigest()
 
         return doc
 
@@ -434,64 +349,20 @@ def build_statement_edge(
 
         return doc
 
-    def insert_doc(self, collection: StandardCollection, doc: dict) -> dict:
-        return collection.insert(doc, overwrite_mode="update")["_id"]
-
-    def insert_literal_doc(
-        self, collection: StandardCollection, doc: dict, normalize=False
-    ) -> dict:
-        if normalize:  # TODO: Clean this up
-            try:
-                return collection.insert(doc)["_id"]
-            except:
-                main_doc = collection.get(doc["_key"])
-                if "duplicates" in main_doc:
-                    main_doc["duplicates"].append(doc)
-                else:
-                    main_doc["duplicates"] = [doc]
-
-                return collection.update(main_doc)["_id"]
-        else:
-            return collection.insert(doc)["_id"]
-
-    def insert_edge(self, collection: EdgeCollection, edge: dict) -> dict:
-        # TODO: Verify intended behavior and consider replacing this spaghet AQL with collection.insert(edge)
-        col_name = collection.name
-        aql = f"""
-            UPSERT {{ _from: '{edge['_from']}', _to: '{edge['_to']}' }}
-            INSERT  {edge}
-            UPDATE {{}}
-            IN {col_name}
-            LET doc = IS_NULL(OLD) ? NEW : OLD
-            RETURN {{ _id: doc._id}}
-        """
-        cursor = self.db.aql.execute(aql)
-
-        return cursor.pop()
-
-    def get_all_docs(self, cols: List[str]) -> Cursor:
-        docs = []
-        for col in cols:
-            docs.extend([doc for doc in self.db.collection(col).all()])
-
-        return docs
-
-    def find_by_id(self, id: string, nodes: list) -> Union[URIRef, BNode, Literal]:
-        node = None
-        for n in nodes:
-            if n["_id"] == id:
-                node = n
+    def adb_doc_to_rdf_node(self, adb_doc: dict) -> Union[URIRef, BNode, Literal]:
         # build literal
-        if "_type" in node:
-            if node["_lang"] is not None:
-                return Literal(node["_value"], lang=node["_lang"])
+        if "_type" in adb_doc:
+            if adb_doc["_lang"] is not None:
+                return Literal(adb_doc["_value"], lang=adb_doc["_lang"])
             else:
-                return Literal(node("_value"), datatype=node["_type"])
+                return Literal(adb_doc["_value"], datatype=adb_doc["_type"])
+
         # build URIRef
-        if "_iri" in node:
-            return URIRef(node["_iri"])
+        if "_iri" in adb_doc:
+            return URIRef(adb_doc["_iri"])
+
         # build BNode
-        return BNode(value=node["_key"])
+        return BNode(value=adb_doc["_key"])
 
     def save_config(self, config: dict) -> None:
         if self.db.has_collection("configurations") is False:
@@ -506,14 +377,14 @@ def save_config(self, config: dict) -> None:
             self.db.aql.execute(aql)
 
         config["latest"] = True
-        config["timestamp"] = round(time.time())
+        config["timestamp"] = time.time()
         self.db.collection("configurations").insert(config)
 
     def get_config_by_latest(self) -> dict:
         return self.get_config_by_key_value("latest", True)
 
     def get_config_by_key_value(self, key: str, val: Any) -> dict:
-        aql = f"""
+        aql = """
             FOR c IN configurations
                 FILTER c[@key] == @val
                 SORT c.timestamp DESC
diff --git a/examples/ArangoRDF.ipynb b/examples/ArangoRDF.ipynb
index d631993..7d9930c 100644
--- a/examples/ArangoRDF.ipynb
+++ b/examples/ArangoRDF.ipynb
@@ -2,65 +2,70 @@
   "cells": [
     {
       "cell_type": "markdown",
+      "metadata": {
+        "id": "VBMhu_S_A2-3"
+      },
       "source": [
         "# **ArangoRDF**\n",
         "\n",
         "<img src=\"https://github.com/ArangoDB-Community/ArangoRDF/blob/main/examples/assets/adb_logo.png?raw=1\" alt=\"rdf\" width=\"250\"/>\n",
         "<img src=\"https://github.com/ArangoDB-Community/ArangoRDF/blob/main/examples/assets/rdf_logo.png?raw=1\" alt=\"rdf\" width=\"200\"/>"
-      ],
-      "metadata": {
-        "id": "VBMhu_S_A2-3"
-      }
+      ]
     },
     {
       "cell_type": "markdown",
-      "source": [
-        "# Setup"
-      ],
       "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 "
-      ],
-      "metadata": {
-        "id": "W4WlZsOCAjgV"
-      },
-      "execution_count": null,
-      "outputs": []
+      ]
     },
     {
       "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"
-      ],
-      "metadata": {
-        "id": "wMtfVlvuApDp"
-      },
-      "execution_count": null,
-      "outputs": []
+      ]
     },
     {
       "cell_type": "markdown",
-      "source": [
-        "# Create a Temporary ArangoDB Cloud Instance"
-      ],
       "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",
@@ -68,15 +73,13 @@
         "\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)"
-      ],
-      "metadata": {
-        "id": "ETS8l_NSAv0F"
-      },
-      "execution_count": null,
-      "outputs": []
+      ]
     },
     {
       "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",
@@ -90,13 +93,13 @@
         "* [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)"
-      ],
-      "metadata": {
-        "id": "yRuJ3OIGE2Yr"
-      }
+      ]
     },
     {
       "cell_type": "markdown",
+      "metadata": {
+        "id": "gFn4w2HlGAMN"
+      },
       "source": [
         "# Get Started with ArangoRDF\n",
         "\n",
@@ -110,13 +113,15 @@
         "* 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"
-      ],
-      "metadata": {
-        "id": "gFn4w2HlGAMN"
-      }
+      ]
     },
     {
       "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",
@@ -133,15 +138,13 @@
         "# RDF Import\n",
         "adb_rdf.init_rdf_collections(bnode=\"Blank\")\n",
         "print(\"initialized collections\")"
-      ],
-      "metadata": {
-        "id": "mlq5BpN7ErhJ"
-      },
-      "execution_count": null,
-      "outputs": []
+      ]
     },
     {
       "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",
@@ -159,37 +162,37 @@
         "\n",
         "\n",
         "\n"
-      ],
-      "metadata": {
-        "id": "6UpzYTVCIMMo"
-      }
+      ]
     },
     {
       "cell_type": "code",
+      "execution_count": null,
+      "metadata": {
+        "id": "Lzv1ibrAJKe6"
+      },
+      "outputs": [],
       "source": [
         "print(\"importing ontology...\")\n",
         "print(\"... this takes ~2 minutes\")\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\")"
-      ],
-      "metadata": {
-        "id": "Lzv1ibrAJKe6"
-      },
-      "execution_count": null,
-      "outputs": []
+      ]
     },
     {
       "cell_type": "markdown",
-      "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. "
-      ],
       "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. "
+      ]
     },
     {
       "cell_type": "markdown",
+      "metadata": {
+        "id": "dkwHfYwOJlk5"
+      },
       "source": [
         "## Import RDF Data Graphs\n",
         "\n",
@@ -197,13 +200,15 @@
         "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."
-      ],
-      "metadata": {
-        "id": "dkwHfYwOJlk5"
-      }
+      ]
     },
     {
       "cell_type": "code",
+      "execution_count": null,
+      "metadata": {
+        "id": "kMBaIZDAKDIb"
+      },
+      "outputs": [],
       "source": [
         "config['Avocados_Are_Delicious'] = True\n",
         "\n",
@@ -213,15 +218,13 @@
         "# 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"
-      ],
-      "metadata": {
-        "id": "kMBaIZDAKDIb"
-      },
-      "execution_count": null,
-      "outputs": []
+      ]
     },
     {
       "cell_type": "markdown",
+      "metadata": {
+        "id": "SS6_ZE8CLAEm"
+      },
       "source": [
         "## Configuration\n",
         "\n",
@@ -232,13 +235,15 @@
         "* `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."
-      ],
-      "metadata": {
-        "id": "SS6_ZE8CLAEm"
-      }
+      ]
     },
     {
       "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",
@@ -248,67 +253,62 @@
         "# 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)"
-      ],
-      "metadata": {
-        "id": "piHGbfiCLff4"
-      },
-      "execution_count": null,
-      "outputs": []
+      ]
     },
     {
       "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()` function.\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 "
-      ],
-      "metadata": {
-        "id": "qic66nCoMA5A"
-      }
+      ]
     },
     {
       "cell_type": "code",
-      "source": [
-        "print(\"exporting data...\")\n",
-        "adb_rdf.export(f\"./ArangoRDF/examples/data/rdfExport.xml\", format=\"xml\")\n",
-        "print(\"export complete\")"
-      ],
+      "execution_count": null,
       "metadata": {
         "id": "lNaZVt0jMr8I"
       },
-      "execution_count": null,
-      "outputs": []
+      "outputs": [],
+      "source": [
+        "print(\"exporting data...\")\n",
+        "adb_rdf.export_rdf(f\"./ArangoRDF/examples/data/rdfExport.xml\", format=\"xml\")\n",
+        "print(\"export complete\")"
+      ]
     },
     {
       "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": "yx190M9ENiR9"
-      }
+      ]
     }
   ],
   "metadata": {
-    "language_info": {
-      "name": "python"
-    },
-    "orig_nbformat": 4,
     "colab": {
       "name": "ArangoRDF.ipynb",
       "provenance": []
     },
+    "gpuClass": "standard",
     "kernelspec": {
-      "name": "python3",
-      "display_name": "Python 3"
+      "display_name": "Python 3",
+      "name": "python3"
+    },
+    "language_info": {
+      "name": "python"
     },
-    "gpuClass": "standard"
+    "orig_nbformat": 4
   },
   "nbformat": 4,
   "nbformat_minor": 0
diff --git a/examples/notebook_output/ArangoRDF_output.ipynb b/examples/notebook_output/ArangoRDF_output.ipynb
index 4183178..de60f1a 100644
--- a/examples/notebook_output/ArangoRDF_output.ipynb
+++ b/examples/notebook_output/ArangoRDF_output.ipynb
@@ -2,73 +2,66 @@
   "cells": [
     {
       "cell_type": "markdown",
+      "metadata": {
+        "id": "VBMhu_S_A2-3"
+      },
       "source": [
         "# **ArangoRDF**\n",
         "\n",
         "<img src=\"https://github.com/ArangoDB-Community/ArangoRDF/blob/main/examples/assets/adb_logo.png?raw=1\" alt=\"rdf\" width=\"250\"/>\n",
         "<img src=\"https://github.com/ArangoDB-Community/ArangoRDF/blob/main/examples/assets/rdf_logo.png?raw=1\" alt=\"rdf\" width=\"200\"/>"
-      ],
-      "metadata": {
-        "id": "VBMhu_S_A2-3"
-      }
+      ]
     },
     {
       "cell_type": "markdown",
-      "source": [
-        "# Setup"
-      ],
       "metadata": {
         "id": "44mc2EvIAzDy"
-      }
+      },
+      "source": [
+        "# Setup"
+      ]
     },
     {
       "cell_type": "code",
+      "execution_count": 1,
+      "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 "
-      ],
-      "metadata": {
-        "id": "W4WlZsOCAjgV"
-      },
-      "execution_count": 1,
-      "outputs": []
+      ]
     },
     {
       "cell_type": "code",
+      "execution_count": 2,
+      "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"
-      ],
-      "metadata": {
-        "id": "wMtfVlvuApDp"
-      },
-      "execution_count": 2,
-      "outputs": []
+      ]
     },
     {
       "cell_type": "markdown",
-      "source": [
-        "# Create a Temporary ArangoDB Cloud Instance"
-      ],
       "metadata": {
         "id": "KnQifktFAxHx"
-      }
+      },
+      "source": [
+        "# Create a Temporary ArangoDB Cloud Instance"
+      ]
     },
     {
       "cell_type": "code",
-      "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)"
-      ],
+      "execution_count": 3,
       "metadata": {
         "colab": {
           "base_uri": "https://localhost:8080/"
@@ -76,11 +69,10 @@
         "id": "ETS8l_NSAv0F",
         "outputId": "599d9009-6221-4d23-b82e-e3d28abbf8fd"
       },
-      "execution_count": 3,
       "outputs": [
         {
-          "output_type": "stream",
           "name": "stdout",
+          "output_type": "stream",
           "text": [
             "Log: requesting new credentials...\n",
             "Succcess: new credentials acquired\n",
@@ -94,10 +86,21 @@
             "}\n"
           ]
         }
+      ],
+      "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",
@@ -111,13 +114,13 @@
         "* [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)"
-      ],
-      "metadata": {
-        "id": "yRuJ3OIGE2Yr"
-      }
+      ]
     },
     {
       "cell_type": "markdown",
+      "metadata": {
+        "id": "gFn4w2HlGAMN"
+      },
       "source": [
         "# Get Started with ArangoRDF\n",
         "\n",
@@ -131,13 +134,28 @@
         "* 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"
-      ],
-      "metadata": {
-        "id": "gFn4w2HlGAMN"
-      }
+      ]
     },
     {
       "cell_type": "code",
+      "execution_count": 4,
+      "metadata": {
+        "colab": {
+          "base_uri": "https://localhost:8080/"
+        },
+        "id": "mlq5BpN7ErhJ",
+        "outputId": "dfa247b5-10db-4983-d0f0-a31af81df133"
+      },
+      "outputs": [
+        {
+          "name": "stdout",
+          "output_type": "stream",
+          "text": [
+            "initialized graph\n",
+            "initialized collections\n"
+          ]
+        }
+      ],
       "source": [
         "# Clean up existing data and collections\n",
         "if db.has_graph(\"default_graph\"):\n",
@@ -154,28 +172,13 @@
         "# RDF Import\n",
         "adb_rdf.init_rdf_collections(bnode=\"Blank\")\n",
         "print(\"initialized collections\")"
-      ],
-      "metadata": {
-        "colab": {
-          "base_uri": "https://localhost:8080/"
-        },
-        "id": "mlq5BpN7ErhJ",
-        "outputId": "dfa247b5-10db-4983-d0f0-a31af81df133"
-      },
-      "execution_count": 4,
-      "outputs": [
-        {
-          "output_type": "stream",
-          "name": "stdout",
-          "text": [
-            "initialized graph\n",
-            "initialized collections\n"
-          ]
-        }
       ]
     },
     {
       "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",
@@ -193,20 +196,11 @@
         "\n",
         "\n",
         "\n"
-      ],
-      "metadata": {
-        "id": "6UpzYTVCIMMo"
-      }
+      ]
     },
     {
       "cell_type": "code",
-      "source": [
-        "print(\"importing ontology...\")\n",
-        "print(\"... this takes ~2 minutes\")\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\")"
-      ],
+      "execution_count": 5,
       "metadata": {
         "colab": {
           "base_uri": "https://localhost:8080/"
@@ -214,30 +208,39 @@
         "id": "Lzv1ibrAJKe6",
         "outputId": "5ec43eb2-5c3e-44b0-b2db-cfbd95f4b2a9"
       },
-      "execution_count": 5,
       "outputs": [
         {
-          "output_type": "stream",
           "name": "stdout",
+          "output_type": "stream",
           "text": [
             "importing ontology...\n",
             "... this takes ~2 minutes\n",
             "Ontology imported\n"
           ]
         }
+      ],
+      "source": [
+        "print(\"importing ontology...\")\n",
+        "print(\"... this takes ~2 minutes\")\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\")"
       ]
     },
     {
       "cell_type": "markdown",
-      "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. "
-      ],
       "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. "
+      ]
     },
     {
       "cell_type": "markdown",
+      "metadata": {
+        "id": "dkwHfYwOJlk5"
+      },
       "source": [
         "## Import RDF Data Graphs\n",
         "\n",
@@ -245,23 +248,11 @@
         "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."
-      ],
-      "metadata": {
-        "id": "dkwHfYwOJlk5"
-      }
+      ]
     },
     {
       "cell_type": "code",
-      "source": [
-        "config['Avocados_Are_Delicious'] = True\n",
-        "\n",
-        "print(\"importing aircraft data...\")\n",
-        "print(\"... this takes ~2 minutes\")\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"
-      ],
+      "execution_count": 6,
       "metadata": {
         "colab": {
           "base_uri": "https://localhost:8080/"
@@ -269,20 +260,32 @@
         "id": "kMBaIZDAKDIb",
         "outputId": "74c1d708-a8a0-4225-fdb4-0d7a5a21b5a7"
       },
-      "execution_count": 6,
       "outputs": [
         {
-          "output_type": "stream",
           "name": "stdout",
+          "output_type": "stream",
           "text": [
             "importing aircraft data...\n",
             "aircraft data imported\n"
           ]
         }
+      ],
+      "source": [
+        "config['Avocados_Are_Delicious'] = True\n",
+        "\n",
+        "print(\"importing aircraft data...\")\n",
+        "print(\"... this takes ~2 minutes\")\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"
       ]
     },
     {
       "cell_type": "markdown",
+      "metadata": {
+        "id": "SS6_ZE8CLAEm"
+      },
       "source": [
         "## Configuration\n",
         "\n",
@@ -293,23 +296,11 @@
         "* `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."
-      ],
-      "metadata": {
-        "id": "SS6_ZE8CLAEm"
-      }
+      ]
     },
     {
       "cell_type": "code",
-      "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)"
-      ],
+      "execution_count": 7,
       "metadata": {
         "colab": {
           "base_uri": "https://localhost:8080/"
@@ -317,40 +308,45 @@
         "id": "piHGbfiCLff4",
         "outputId": "acffb8f0-5549-4349-ff59-11d0fd94e041"
       },
-      "execution_count": 7,
       "outputs": [
         {
-          "output_type": "stream",
           "name": "stdout",
+          "output_type": "stream",
           "text": [
             "{'Avocados_Are_Delicious': True, 'default_graph': 'default_graph', 'latest': True, 'normalize_literals': False, 'sub_graph': 'http://data.sfgov.org/ontology', 'timestamp': 1657928178}\n",
             "\n",
             "{'Avocados_Are_Delicious': True, 'default_graph': 'default_graph', 'latest': True, 'normalize_literals': False, 'sub_graph': 'http://data.sfgov.org/ontology', 'timestamp': 1657928178}\n"
           ]
         }
+      ],
+      "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)"
       ]
     },
     {
       "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()` function.\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 "
-      ],
-      "metadata": {
-        "id": "qic66nCoMA5A"
-      }
+      ]
     },
     {
       "cell_type": "code",
-      "source": [
-        "print(\"exporting data...\")\n",
-        "adb_rdf.export(f\"./ArangoRDF/examples/data/rdfExport.xml\", format=\"xml\")\n",
-        "print(\"export complete\")"
-      ],
+      "execution_count": 8,
       "metadata": {
         "colab": {
           "base_uri": "https://localhost:8080/"
@@ -358,45 +354,49 @@
         "id": "lNaZVt0jMr8I",
         "outputId": "6029c5fb-2693-4c09-f4c7-70f54554328c"
       },
-      "execution_count": 8,
       "outputs": [
         {
-          "output_type": "stream",
           "name": "stdout",
+          "output_type": "stream",
           "text": [
             "exporting data...\n",
             "export complete\n"
           ]
         }
+      ],
+      "source": [
+        "print(\"exporting data...\")\n",
+        "adb_rdf.export_rdf(f\"./ArangoRDF/examples/data/rdfExport.xml\", format=\"xml\")\n",
+        "print(\"export complete\")"
       ]
     },
     {
       "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": "yx190M9ENiR9"
-      }
+      ]
     }
   ],
   "metadata": {
-    "language_info": {
-      "name": "python"
-    },
-    "orig_nbformat": 4,
     "colab": {
       "name": "ArangoRDF.ipynb",
       "provenance": []
     },
+    "gpuClass": "standard",
     "kernelspec": {
-      "name": "python3",
-      "display_name": "Python 3"
+      "display_name": "Python 3",
+      "name": "python3"
+    },
+    "language_info": {
+      "name": "python"
     },
-    "gpuClass": "standard"
+    "orig_nbformat": 4
   },
   "nbformat": 4,
   "nbformat_minor": 0
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..3417c5e
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,17 @@
+[build-system]
+requires = ["setuptools>=45", "setuptools_scm[toml]>=6.2", "wheel"]
+build-backend = "setuptools.build_meta"
+
+[tool.coverage.run]
+omit = [
+    "arango_rdf/version.py",
+    "setup.py",
+]
+
+[tool.pytest.ini_options]
+addopts = "-s -vv"
+minversion = "6.0"
+testpaths = ["tests"]
+
+[tool.setuptools_scm]
+write_to = "arango_rdf/version.py"
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
index 0000000..8417703
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,2 @@
+[isort]
+profile = black
\ No newline at end of file
diff --git a/setup.py b/setup.py
index 8434bfb..884b587 100644
--- a/setup.py
+++ b/setup.py
@@ -20,12 +20,16 @@
         "rdflib>=6.0.0",
         "python-arango>=7.4.1",
         "requests>=2.27.1",
+        "tqdm>=4.64.0",
         "setuptools>=45",
     ],
     extras_require={
         "dev": [
             "black",
             "isort>=5.0.0",
+            "pytest>=6.0.0",
+            "pytest-cov>=2.0.0",
+            "coveralls>=3.3.1",
         ],
     },
     classifiers=[


Test patch:
diff --git a/tests/__init__.py b/tests/__init__.py
index e10e072..e69de29 100644
--- a/tests/__init__.py
+++ b/tests/__init__.py
@@ -1,3 +0,0 @@
-from pathlib import Path
-
-PROJECT_DIR = Path(__file__).parent.parent
diff --git a/tests/conftest.py b/tests/conftest.py
new file mode 100644
index 0000000..234cfd4
--- /dev/null
+++ b/tests/conftest.py
@@ -0,0 +1,39 @@
+from pathlib import Path
+from typing import Any
+
+from arango import ArangoClient, DefaultHTTPClient
+from arango.database import StandardDatabase
+
+db: StandardDatabase
+PROJECT_DIR = Path(__file__).parent.parent
+
+
+def pytest_addoption(parser: Any) -> None:
+    parser.addoption("--url", action="store", default="http://localhost:8529")
+    parser.addoption("--dbName", action="store", default="_system")
+    parser.addoption("--username", action="store", default="root")
+    parser.addoption("--password", action="store", default="")
+
+
+def pytest_configure(config: Any) -> None:
+    con = {
+        "url": config.getoption("url"),
+        "username": config.getoption("username"),
+        "password": config.getoption("password"),
+        "dbName": config.getoption("dbName"),
+    }
+
+    print("----------------------------------------")
+    print("URL: " + con["url"])
+    print("Username: " + con["username"])
+    print("Password: " + con["password"])
+    print("Database: " + con["dbName"])
+    print("----------------------------------------")
+
+    class NoTimeoutHTTPClient(DefaultHTTPClient):  # type: ignore
+        REQUEST_TIMEOUT = None
+
+    global db
+    db = ArangoClient(hosts=con["url"], http_client=NoTimeoutHTTPClient()).db(
+        con["dbName"], con["username"], con["password"], verify=True
+    )
diff --git a/tests/test.py b/tests/test.py
deleted file mode 100644
index 3a2ee7b..0000000
--- a/tests/test.py
+++ /dev/null
@@ -1,64 +0,0 @@
-from arango import ArangoClient
-from arango_rdf import ArangoRDF
-
-db = ArangoClient(hosts="http://localhost:8529").db(
-    "rdf", username="root", password="openSesame"
-)
-
-# Clean up existing data and collections
-if db.has_graph("default_graph"):
-    db.delete_graph("default_graph", drop_collections=True, ignore_missing=True)
-
-# 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") 
-print("initialized graph")
-config = {"normalize_literals": False}  # default: False
-
-# RDF Import
-adb_rdf.init_rdf_collections(bnode="Blank")
-print("initialized collections")
-
-print("importing ontology...")
-# Start with importing the ontology
-adb_rdf.import_rdf("./examples/data/airport-ontology.owl", format="xml", config=config, save_config=True)
-print("Ontology imported")
-
-print("importing aircraft data...")
-# Next, let's import the actual graph data
-adb_rdf.import_rdf(f"./examples/data/sfo-aircraft-partial.ttl", format="ttl", config=config, save_config=True)
-
-print("aircraft data imported")
-
-# 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
-print("exporting data...")
-adb_rdf.export(f"./examples/data/rdfExport.xml", format="xml")
-print("export complete")
-
-# 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)
-
-# 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")
-print("re-initialized graph")
-
-adb_rdf.init_rdf_collections(bnode="Blank")
-print("re-initialized collections")
-
-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')
-
-# Re-import Exported data
-print("re-importing data...")
-adb_rdf.import_rdf(f"./examples/data/rdfExport.xml", format="xml", config=config)
-
-print("done")
diff --git a/tests/testRdfsOwlBase.py b/tests/testRdfsOwlBase.py
index 8b087e3..520ceec 100644
--- a/tests/testRdfsOwlBase.py
+++ b/tests/testRdfsOwlBase.py
@@ -1,7 +1,7 @@
 # NOTE: Temporarily Out of Service
 
-from arango_rdf import ArangoRDF
-from tests import PROJECT_DIR
+# from arango_rdf import ArangoRDF
+# from .conftest import PROJECT_DIR
 
 # Test the loading of the RDFS/OWL/SKOS metamodel
 # ToDo implement this an option on kg.init_rdf_collection
diff --git a/tests/test_main.py b/tests/test_main.py
new file mode 100644
index 0000000..8f29e30
--- /dev/null
+++ b/tests/test_main.py
@@ -0,0 +1,192 @@
+from time import time
+
+import pytest
+
+from arango_rdf import ArangoRDF
+
+from .conftest import PROJECT_DIR, db
+
+
+def test_constructor() -> None:
+    ArangoRDF(db, default_graph="temp_graph")  # create the graph
+    assert db.has_graph("temp_graph")
+    ArangoRDF(db, default_graph="temp_graph")  # re-use the graph
+
+
+def test_full_cycle_aircraft_without_normalize_literals() -> None:
+    graph_name = "default_graph"
+    ################################# Import #################################
+    # Clean up existing data and collections
+    db.delete_graph(graph_name, drop_collections=True, ignore_missing=True)
+
+    # 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, default_graph=graph_name, sub_graph="http://data.sfgov.org/ontology"
+    )
+    assert db.has_graph(graph_name)
+    print("initialized graph")
+    config = {"normalize_literals": False}  # default: False
+
+    adb_rdf.init_rdf_collections(bnode="Blank")
+    assert db.has_collection("Blank")
+    assert db.has_collection("IRI")
+    assert db.has_collection("Literal")
+    assert db.has_collection("Statement")
+    print("initialized collections")
+
+    print("importing ontology...")
+
+    timestamp = time()
+    adb_rdf.import_rdf(
+        f"{PROJECT_DIR}/examples/data/airport-ontology.owl",
+        format="xml",
+        config=config,
+        save_config=True,
+    )
+    print("Ontology imported")
+
+    assert db.collection("Blank").count() == 59
+    assert db.collection("IRI").count() == 84
+    assert db.collection("Literal").count() == 148
+    assert db.collection("Statement").count() == 492
+
+    assert db.has_collection("configurations")
+    assert adb_rdf.get_config_by_latest()["timestamp"] >= timestamp
+
+    print("importing aircraft data...")
+    # Next, let's import the actual graph data
+    adb_rdf.import_rdf(
+        f"{PROJECT_DIR}/examples/data/sfo-aircraft-partial.ttl",
+        format="ttl",
+        config=config,
+        save_config=False,
+    )
+
+    assert db.collection("Blank").count() == 59
+    assert db.collection("IRI").count() == 90
+    assert db.collection("Literal").count() == 306
+    assert db.collection("Statement").count() == 664
+
+    print("aircraft data imported")
+
+    ################################# Export #################################
+    print("exporting data...")
+    adb_rdf.export_rdf(f"{PROJECT_DIR}/examples/data/rdfExport.xml", format="xml")
+    print("export complete")
+
+    # ################################# Re-import ##############################
+    db.delete_graph(graph_name, drop_collections=True, ignore_missing=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")
+    print("re-initialized graph")
+
+    adb_rdf.init_rdf_collections(bnode="Blank")
+    print("re-initialized collections")
+
+    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')
+
+    # Re-import Exported data
+    print("re-importing data...")
+    adb_rdf.import_rdf(f"./examples/data/rdfExport.xml", format="xml", config=config)
+
+    assert db.collection("Blank").count() == 59
+    assert db.collection("IRI").count() == 90
+    assert db.collection("Literal").count() == 158
+    assert db.collection("Statement").count() == 516
+
+    print("done")
+
+
+def test_full_cycle_aircraft_with_normalize_literals() -> None:
+    graph_name = "default_graph"
+    ################################# Import #################################
+    db.delete_graph(graph_name, drop_collections=True, ignore_missing=True)
+
+    adb_rdf = ArangoRDF(
+        db, default_graph=graph_name, sub_graph="http://data.sfgov.org/ontology"
+    )
+    config = {"normalize_literals": True}
+
+    adb_rdf.init_rdf_collections(bnode="Blank")
+    print("initialized collections")
+
+    print("importing ontology...")
+    adb_rdf.import_rdf(
+        f"{PROJECT_DIR}/examples/data/airport-ontology.owl",
+        format="xml",
+        config=config,
+        save_config=True,
+    )
+
+    assert db.collection("Blank").count() == 59
+    assert db.collection("IRI").count() == 84
+    assert db.collection("Literal").count() == 78
+    assert db.collection("Statement").count() == 492
+
+    print("importing aircraft data...")
+    adb_rdf.import_rdf(
+        f"{PROJECT_DIR}/examples/data/sfo-aircraft-partial.ttl",
+        format="ttl",
+        config=config,
+        save_config=False,
+    )
+
+    assert db.collection("Blank").count() == 59
+    assert db.collection("IRI").count() == 90
+    assert db.collection("Literal").count() == 87
+    assert db.collection("Statement").count() == 516
+
+    print("aircraft data imported")
+
+    ################################# Export #################################
+    print("exporting data...")
+    adb_rdf.export_rdf(f"{PROJECT_DIR}/examples/data/rdfExport.xml", format="xml")
+    print("export complete")
+
+    ################################# Re-import ##############################
+    db.delete_graph(graph_name, drop_collections=True, ignore_missing=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")
+    print("re-initialized graph")
+
+    adb_rdf.init_rdf_collections(bnode="Blank")
+    print("re-initialized collections")
+
+    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')
+
+    # Re-import Exported data
+    print("re-importing data...")
+    adb_rdf.import_rdf(f"./examples/data/rdfExport.xml", format="xml", config=config)
+
+    assert db.collection("Blank").count() == 59
+    assert db.collection("IRI").count() == 90
+    assert db.collection("Literal").count() == 87
+    assert db.collection("Statement").count() == 516
+
+    print("done")
+
+
+def test_get_config() -> None:
+    adb_rdf = ArangoRDF(db, sub_graph="http://data.sfgov.org/ontology")
+    config_1 = adb_rdf.get_config_by_latest()
+    assert config_1["latest"] == True
+    assert "_id" not in config_1
+    assert "_key" not in config_1
+    assert "_rev" not in config_1
+
+    config_2 = adb_rdf.get_config_by_key_value("timestamp", config_1["timestamp"])
+    assert config_1 == config_2
+
+    config_3 = adb_rdf.get_config_by_key_value("normalize_literals", False)
+    assert config_3["normalize_literals"] == False