Repository: konveyor/tackle-data-gravity-insights (language: python)

Issue:
Add Stack tracing from DiVA `*.json` to the DGI graph
**As a** developer
**I want to** able to look at database transactions and how they span the regions of the code
**So that** I can track path taken by the program during a transaction

### Assumptions

- Stack traces are available to the user
- Traces are generated statically (verify?)


### Acceptance Criteria
We are able to see/query the path traced by a transaction. This can either be annotations in the graph or read from a local source. !

[DGI.png](https://images.zenhubusercontent.com/621388a0fa29d8e5e0e83a98/73dffa71-2c49-49de-8ae3-f366491a6299)


Discussion / hints:

Fix patch:
diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
index 984f505..4ea2b51 100644
--- a/.devcontainer/devcontainer.json
+++ b/.devcontainer/devcontainer.json
@@ -5,27 +5,31 @@
 	"service": "app",
 	"workspaceFolder": "/app",
 	"remoteUser": "vscode",
-	"settings": {},
-	"extensions": [
-		"VisualVisualStudioExptTeam.vscodeintellicodeStudio",
-		"ms-python.python",
-		"ms-python.vscode-pylance",
-		"ms-azuretools.vscode-docker",
-		"njpwerner.autodocstring",
-    	"wholroyd.jinja",
-		"yzhang.markdown-all-in-one",
-		"hnw.vscode-auto-open-markdown-preview",
-        "bierner.markdown-preview-github-styles",
-        "davidanson.vscode-markdownlint",
-		"donjayamanne.githistory",
-		"GitHub.vscode-pull-request-github",
-		"hbenl.vscode-test-explorer",
-		"redhat.fabric8-analytics",
-		"redhat.vscode-yaml",
-		"inercia.vscode-k3d",
-		"streetsidesoftware.code-spell-checker"
-	],
-    "postCreateCommand": "if test -f 'requirements.txt'; then sudo pip install -r requirements.txt; fi && sudo bash -c 'pip install -e .[dev]'",
+	"customizations": {
+		"vscode": {
+			"settings": {},
+			"extensions": [
+				"VisualVisualStudioExptTeam.vscodeintellicodeStudio",
+				"ms-python.python",
+				"ms-python.vscode-pylance",
+				"ms-azuretools.vscode-docker",
+				"njpwerner.autodocstring",
+				"wholroyd.jinja",
+				"yzhang.markdown-all-in-one",
+				"hnw.vscode-auto-open-markdown-preview",
+				"bierner.markdown-preview-github-styles",
+				"davidanson.vscode-markdownlint",
+				"donjayamanne.githistory",
+				"GitHub.vscode-pull-request-github",
+				"hbenl.vscode-test-explorer",
+				"redhat.fabric8-analytics",
+				"redhat.vscode-yaml",
+				"inercia.vscode-k3d",
+				"streetsidesoftware.code-spell-checker"
+			]		
+		}
+	},
+    "postCreateCommand": "sudo bash -c 'pip install -e .[dev]'",
 	"features": {
 		"docker-from-docker": "latest"
 	}
diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml
index 0faa3a2..f9736cc 100644
--- a/.devcontainer/docker-compose.yml
+++ b/.devcontainer/docker-compose.yml
@@ -1,10 +1,11 @@
-version: "3"
+version: "3.9"
 
 services:
   app:
     build:
       context: ..
       dockerfile: .devcontainer/Dockerfile
+      network: host
     hostname: tackle
     container_name: dgi-docker
     # init: true
@@ -14,7 +15,7 @@ services:
     command: "sleep infinity"
     environment:
       FLASK_APP: service:app
-      NEO4J_BOLT_URL: "bolt://neo4j:tackle@neo4j:7687"
+      NEO4J_BOLT_URL: "neo4j://neo4j:konveyor@neo4j:7687"
       GRAPH_FACTS_DIR: /app/doop-data/output
     networks:
       - dev
@@ -23,7 +24,7 @@ services:
 
   # Neo4J UI URL: http://localhost:7474/browser/
   neo4j:
-    image: neo4j
+    image: neo4j:4.4.17
     hostname: neo4j
     container_name: neo4j
     ports:
@@ -31,8 +32,8 @@ services:
       - 7687:7687
     environment:
       NEO4J_USERNAME: neo4j
-      NEO4J_PASSWORD: tackle
-      NEO4J_AUTH: neo4j/tackle
+      NEO4J_PASSWORD: konveyor
+      NEO4J_AUTH: neo4j/konveyor
       NEO4J_apoc_import_file_enabled: "true"
       NEO4J_apoc_export_file_enabled: "true"
       NEO4J_dbms_security_procedures_whitelist: gds.*, apoc.*
@@ -44,6 +45,6 @@ services:
 
 volumes:
   neo4j:
-    
+
 networks:
   dev: null
diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml
index 5247c94..e8e5b99 100644
--- a/.github/workflows/ci-build.yml
+++ b/.github/workflows/ci-build.yml
@@ -20,14 +20,14 @@ jobs:
     # use a known build environment
     container: python:3.9-slim
 
-    # Required services    
+    # Required services
     services:
       neo4j:
-        image: neo4j
+        image: neo4j:4.4.17
         env:
           NEO4J_USERNAME: neo4j
-          NEO4J_PASSWORD: test
-          NEO4J_AUTH: neo4j/test          
+          NEO4J_PASSWORD: konveyor
+          NEO4J_AUTH: neo4j/konveyor      
         ports:
           - 7474:7474
           - 7687:7687
@@ -36,7 +36,7 @@ jobs:
     steps:
       - name: Checkout
         uses: actions/checkout@v2
-      
+  
       - name: Install dependencies
         run: |
           python -m pip install --upgrade pip wheel
@@ -52,7 +52,8 @@ jobs:
       - name: Run unit tests with nose
         run: nosetests
         env:
-          NEO4J_BOLT_URL: "bolt://neo4j:test@neo4j:7687"
+          TERM: xterm-256color
+          NEO4J_BOLT_URL: "neo4j://neo4j:konveyor@neo4j:7687"
 
       # - name: Upload code coverage
       #   uses: codecov/codecov-action@v2 
diff --git a/.gitignore b/.gitignore
index 2b839f4..bbf5d13 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,7 @@
+# Ignore vscode and idea auxiliary files
+.idea
+.vscode
+
 # Ignore DB auxiliary files
 demo/**/*.dump
 demo/code2graph-samples/doop-input/*
@@ -142,3 +146,6 @@ dmypy.json
 
 # Pyre type checker
 .pyre/
+
+.idea 
+.vscode 
\ No newline at end of file
diff --git a/Makefile b/Makefile
index 1d1ba96..06d9d90 100644
--- a/Makefile
+++ b/Makefile
@@ -18,6 +18,7 @@ lint: ## Run the linter
 	$(info Running linting...)
 	flake8 dgi --count --select=E9,F63,F7,F82 --show-source --statistics
 	flake8 dgi --count --max-complexity=10 --max-line-length=127 --statistics
+	pylint dgi --disable=R0801,R0913,R0914 --max-line-length=127 --max-args=6 --ignore=sqlparse.py
 
 test: ## Run the unit tests
 	$(info Running tests...)
@@ -25,5 +26,6 @@ test: ## Run the unit tests
 
 neo4j: ## Start Neo4J in Docker
 	$(info Starting Neo4J server...)
-	export NEO4J_BOLT_URL="bolt://neo4j:tackle@localhost:7687"
-	docker run -d --name neo4j -p 7474:7474 -p 7687:7687 -e NEO4J_AUTH="neo4j/tackle" neo4j
+	export NEO4J_BOLT_URL="neo4j://neo4j:konveyor@localhost:7687"
+	docker run -d --name neo4j -p 7474:7474 -p 7687:7687 -e NEO4J_AUTH="neo4j/konveyor" neo4j:4.4.17
+	
\ No newline at end of file
diff --git a/README.md b/README.md
index 5bfbbfd..34ab458 100644
--- a/README.md
+++ b/README.md
@@ -30,44 +30,51 @@ You will need an instance of Neo4j to store the graphs that `dgi` creates. You c
 docker run -d --name neo4j \
     -p 7474:7474 \
     -p 7687:7687 \
-    -e NEO4J_AUTH="neo4j/tackle" \
-    -e NEO4J_apoc_export_file_enabled=true \
-    -e NEO4J_apoc_import_file_enabled=true \
-    -e NEO4J_apoc_import_file_use__neo4j__config=true \
-    -e NEO4JLABS_PLUGINS=\["apoc"\] \    
-  neo4j
-
-export NEO4J_BOLT_URL="bolt://neo4j:tackle@localhost:7687"    
+    -e NEO4J_AUTH="neo4j:konveyor" \
+    neo4j:4.4.17
+```
+
+Save the bolt url for further use:
+```
+export NEO4J_BOLT_URL="neo4j://neo4j:konveyor@localhost:7687"
 ```
 
 You can now use the `dgi` command to load information about your application into the graph database.
 
 ```man
-dgi --help
-
-Usage: dgi [OPTIONS] COMMAND [ARGS]...
-
-  Tackle Data Gravity Insights
-
-Options:
-  -n, --neo4j-bolt TEXT           Neo4j Bolt URL
-  -a, --abstraction TEXT          The level of abstraction to use when
-                                  building the graph. Valid options are:
-                                  class, method, or full.  [default: class]
-  -q, --quiet / -v, --verbose     Be more quiet/verbose  [default: verbose]
-  -c, --clear / -dnc, --dont-clear
-                                  Clear (or don't clear) graph before loading
-                                  [default: clear]
-  --help                          Show this message and exit.
-
-Commands:
-  c2g   This command loads Code dependencies into the graph
-  s2g   This command parses SQL schema DDL into a graph
-  tx2g  This command loads DiVA database transactions into a graph
+ Usage: dgi [OPTIONS] COMMAND [ARGS]...
+
+ Tackle Data Gravity Insights
+
+╭─ Options ────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
+│ --neo4j-bolt  -n  TEXT  Neo4j Bolt URL                                                                               │
+│ --quiet       -q        Be more quiet                                                                                │
+│ --validate    -v        Validate but don't populate graph                                                            │
+│ --clear       -c        Clear graph before loading                                                                   │
+│ --help                  Show this message and exit.                                                                  │
+╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
+╭─ Commands ───────────────────────────────────────────────────────────────────────────────────────────────────────────╮
+│ c2g         Code2Graph add various program dependencies (i.e., call return, heap, and data) into the graph           │
+│ partition   Partition is a command runs the CARGO algorithm to (re-)partition a monolith into microservices          │
+│ s2g         Schema2Graph parses SQL schema (*.DDL file) into the graph                                               │
+│ tx2g        Transaction2Graph add edges denoting CRUD operations to the graph.                                       │
+╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
 ```
 
-## Links
-Link to complete [Documentation](https://konveyor.github.io/tackle-data-gravity-insights/) of the project
+## Demo
+
+This is a demonstration of the usage of DGI
+
+1. [Demonstration](docs/demo.md)
+
+## Running DGI
+
+To run this project please refer to the steps in the getting started guide
+
+1. [Getting Started](docs/getting-started.md)
+
+## Contributing
+
+To contribute to this project you will need to set up your development environment and set up some files. The steps are in the following file:
 
-## Code of Conduct
-Refer to Konveyor's Code of Conduct [here](https://github.com/konveyor/community/blob/main/CODE_OF_CONDUCT.md).
+1. [Set up your Developer Environment](docs/development.md)
diff --git a/dgi/__init__.py b/dgi/__init__.py
index abbbbfd..8ffa394 100644
--- a/dgi/__init__.py
+++ b/dgi/__init__.py
@@ -1,2 +1,3 @@
 """ DGI Package """
-__version__ = "0.1.0"
+
+__version__ = "1.0.0"
diff --git a/dgi/cli.py b/dgi/cli.py
index 12a196e..587a2aa 100644
--- a/dgi/cli.py
+++ b/dgi/cli.py
@@ -3,8 +3,7 @@
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
+# You may obtain a copy of the License a#t˚†
 #       http://www.apache.org/licenses/LICENSE-2.0
 #
 # Unless required by applicable law or agreed to in writing, software
@@ -19,64 +18,97 @@
 
 Command Line Interface (CLI) for Tackle Data Gravity Insights
 """
-import sys
-import json
-import click
-import logging
 import importlib.resources
+import json
+import os
+import sys
+from pathlib import Path
 
+import rich_click as click
 from neomodel import config
 from simple_ddl_parser import parse_from_file
 
 # Import our packages
-from .schema2graph import schema_loader
-from .code2graph import ClassGraphBuilder, MethodGraphBuilder
-from .tx2graph import ClassTransactionLoader, MethodTransactionLoader
-from .code2graph.utils.parse_config import Config
+from dgi.code2graph import ClassGraphBuilder, MethodGraphBuilder
+from dgi.partitioning.partition import recommend_partitions
 
+from dgi.schema2graph import schema_loader
+from dgi.tx2graph import ClassTransactionLoader, MethodTransactionLoader
+from dgi.utils.parse_config import Config
+from dgi.utils.logging import Log
 
 ######################################################################
 # cli - Grouping for sub commands
 ######################################################################
+
+
 @click.group()
-@click.option("--neo4j-bolt", "-n", envvar="NEO4J_BOLT_URL",
-              default="bolt://neo4j:tackle@localhost:7687", help="Neo4j Bolt URL")
-@click.option("--quiet", "-q", required=False, help="Be more quiet", default=False, is_flag=True, show_default=True)
-@click.option("--validate", "-v", help="Validate but don't populate graph", default=False, is_flag=True)
-@click.option("--clear", "-c", help="Clear graph before loading", default=False, is_flag=True, show_default=True)
+@click.option(
+    "--neo4j-bolt",
+    "-n",
+    envvar="NEO4J_BOLT_URL",
+    default="neo4j://neo4j:konveyor@localhost:7687",
+    help="Neo4j Bolt URL",
+)
+@click.option(
+    "--quiet",
+    "-q",
+    required=False,
+    help="Be more quiet",
+    default=False,
+    is_flag=True,
+    show_default=True,
+)
+@click.option(
+    "--validate",
+    "-v",
+    help="Validate but don't populate graph",
+    default=False,
+    is_flag=True,
+)
+@click.option(
+    "--clear",
+    "-c",
+    help="Clear graph before loading",
+    default=False,
+    is_flag=True,
+    show_default=True,
+)
 @click.pass_context
 def cli(ctx, validate, quiet, clear, neo4j_bolt):
     """Tackle Data Gravity Insights"""
     ctx.ensure_object(dict)
-    ctx.obj['validate'] = validate
-    ctx.obj['verbose'] = not quiet
-    ctx.obj['clear'] = clear
-    ctx.obj['bolt'] = neo4j_bolt
+    ctx.obj["validate"] = validate
+    ctx.obj["verbose"] = not quiet
+    ctx.obj["clear"] = clear
+    ctx.obj["bolt"] = neo4j_bolt
 
     # Configure Neo4J
     config.DATABASE_URL = ctx.obj["bolt"]
     config.ENCRYPTED_CONNECTION = False
 
-    # Set logging configuration
-    loglevel = logging.WARNING
-    if (ctx.obj["verbose"]):
-        loglevel = logging.INFO
-    logging.basicConfig(level=loglevel, format="[%(levelname)s] %(message)s")
-
 
 ######################################################################
 # schema2graph - Populates the graph from an SQL schema DDL
 ######################################################################
 @cli.command()
-@click.option("--input", "-i", type=click.Path(exists=True), required=True,
-              help="The SQL/DDL file to load into the graph")
-@click.option("--output", "-o", required=False, help="The JSON file to write the schema to")
+@click.option(
+    "--input",
+    "-i",
+    type=click.Path(exists=True),
+    required=True,
+    help="The SQL/DDL file to load into the graph",
+)
+@click.option(
+    "--output", "-o", required=False, help="The JSON file to write the schema to"
+)
 @click.pass_context
-def s2g(ctx, input, output):
-    """This command parses SQL schema DDL into a graph"""
+def s2g(ctx, input, output):  # pylint: disable=redefined-builtin
+    """Schema2Graph parses SQL schema (*.DDL file) into the graph"""
 
     # Read the DDL file
-    click.echo(f"Reading: {input}")
+    input = Path(input)
+    Log.info(f"Reading: {input.absolute()}")
     result = None
     try:
         result = parse_from_file(input, group_by_type=True)
@@ -85,100 +117,138 @@ def s2g(ctx, input, output):
 
     # Optionally write it output to json
     if output:
-        click.echo(f"Writing: {output}")
-        with open(output, "w") as f:
+        Log.info(f"Writing: {output}")
+        with open(output, "w", encoding='utf-8') as file:
             contents = json.dumps(result, indent=4)
-            f.write(contents)
+            file.write(contents)
 
-    if ctx.obj['validate']:
-        click.echo(f"File [{input}] validated.")
-        exit(0)
+    if ctx.obj["validate"]:
+        Log.info(f"File [{input}] validated.")
+        sys.exit(0)
 
-    if ctx.obj['clear']:
-        click.echo("Clearing graph...")
+    if ctx.obj["clear"]:
+        Log.warn("Clear flag is turned ON. Clearing graph.")
         schema_loader.remove_all_nodes()
 
-    click.echo("Building Graph...")
+    Log.info("Building Graph..")
     schema_loader.load_graph(result)
-    click.echo("Graph build complete")
+    Log.info("Graph build complete")
 
 
 ######################################################################
 #  tx2graph - Loads output from DiVA into graph
 ######################################################################
 @cli.command()
-@click.option("--input", "-i", type=click.Path(exists=True), required=True,
-              help="DiVA Transaction JSON file")
-@click.option("--abstraction", "-a", type=click.Choice(["class", "method", "full"]),
-              default="class", help="The level of abstraction to use when building the graph", show_default=True)
+@click.option(
+    "--input",
+    "-i",
+    type=click.Path(exists=True),
+    required=True,
+    help="DiVA Transaction JSON file",
+)
+@click.option(
+    "--abstraction",
+    "-a",
+    type=click.Choice(["class", "method", "full"]),
+    default="full",
+    help="The level of abstraction to use when building the graph",
+    show_default=True,
+)
+@click.option(
+    "--force-clear",
+    "-fc",
+    help="Clear all nodes in the graph before loading. WARNING: THERE IS REASON THIS IS HIDDEN. ONLY USE "
+    "FOR TESTING!",
+    default=False,
+    is_flag=True,
+    show_default=True,
+    hidden=True,
+)
 @click.pass_context
-def tx2g(ctx, input, abstraction):
-    """This command loads DiVA database transactions into a graph"""
+def tx2g(ctx, input, abstraction, force_clear):  # pylint: disable=redefined-builtin
+    """Transaction2Graph add edges denoting CRUD operations to the graph."""
 
     if ctx.obj["verbose"]:
-        click.echo("Verbose mode: ON")
+        Log.info("Verbose mode: ON")
 
     class_transaction_loader = ClassTransactionLoader()
     method_transaction_loader = MethodTransactionLoader()
 
     if abstraction.lower() == "full":
-        if ctx.obj['validate']:
-            click.echo("Validate mode: abstraction level is {}".format(
-                abstraction.lower()))
+        if ctx.obj["validate"]:
+            Log.info(
+                f"Validate mode: abstraction level is {abstraction.lower()}")
+
             sys.exit()
 
         class_transaction_loader.load_transactions(
-            input, clear=ctx.obj['clear'])
+            input, clear=ctx.obj["clear"])
         # We don't want to clear the table node twice.
-        # Otherwise, we'll use the table nodes created above
+        # Otherwise, we'll lose the table nodes created above
         method_transaction_loader.load_transactions(input, clear=False)
 
     elif abstraction.lower() == "class":
-        if ctx.obj['validate']:
-            click.echo("Validate mode: abstraction level is {}".format(
-                abstraction.lower()))
+        if ctx.obj["validate"]:
+            Log.info(
+                f"Validate mode: abstraction level is {abstraction.lower()}"
+            )
             sys.exit()
         class_transaction_loader.load_transactions(
-            input, clear=ctx.obj['clear'])
+            input, clear=ctx.obj["clear"], force_clear=force_clear
+        )
 
     elif abstraction.lower() == "method":
-        if ctx.obj['validate']:
-            click.echo("Validate mode: abstraction level is {}".format(
-                abstraction.lower()))
+        if ctx.obj["validate"]:
+            Log.info(
+                f"Validate mode: abstraction level is {abstraction.lower()}"
+            )
             sys.exit()
 
         method_transaction_loader.load_transactions(
-            input, clear=ctx.obj['clear'])
+            input, clear=ctx.obj["clear"], force_clear=force_clear
+        )
 
     else:
         raise click.BadArgumentUsage(
-            "Not a valid abstraction level. Valid options are 'class', 'method', 'full'.")
+            "Not a valid abstraction level. Valid options are 'class', 'method', 'full'."
+        )
 
-    click.echo("Transactions populated")
+    Log.info("Transactions populated")
 
 
 ######################################################################
 #  code2graph - Imports code dependencies into the graph
 ######################################################################
 @cli.command()
-@click.option("--input", "-i", type=click.Path(exists=True, resolve_path=True,
-              file_okay=False), required=True, help="DOOP output facts directory.")
-@click.option("--abstraction", "-a", type=click.Choice(["class", "method", "full"]),
-              default="class", help="The level of abstraction to use when building the graph", show_default=True)
+@click.option(
+    "--input",
+    "-i",
+    type=click.Path(exists=True, resolve_path=True, file_okay=False),
+    required=True,
+    help="DOOP output facts directory.",
+)
+@click.option(
+    "--abstraction",
+    "-a",
+    type=click.Choice(["class", "method", "full"]),
+    default="full",
+    help="The level of abstraction to use when building the graph",
+    show_default=True,
+)
 @click.pass_context
-def c2g(ctx, input, abstraction):
-    """This command loads Code dependencies into the graph"""
+def c2g(ctx, input, abstraction):  # pylint: disable=redefined-builtin
+    """Code2Graph add various program dependencies (i.e., call return, heap, and data) into the graph"""
 
-    click.echo("code2graph generator started...")
+    Log.info("code2graph generator started.")
 
     if ctx.obj["verbose"]:
-        click.echo("Verbose mode: ON")
+        Log.info("Verbose mode: ON")
 
     # -------------------------
     # Initialize configurations
     # -------------------------
-    proj_root = importlib.resources.files('dgi.code2graph')
-    usr_cfg = Config(config_file=proj_root.joinpath("etc", "config.yml"))
+    proj_root = importlib.resources.files("dgi.code2graph")
+    usr_cfg = Config(config_file=proj_root.joinpath("etc", "config.yml"))  # pylint: disable=E1121
     usr_cfg.load_config()
 
     # Add the input dir to configuration.
@@ -188,37 +258,92 @@ def c2g(ctx, input, abstraction):
     # Build the graph
     # ---------------
 
-    click.echo("Building Graph...")
+    Log.info("Building Graph.")
 
     class_g_builder = ClassGraphBuilder(usr_cfg)
     method_g_builder = MethodGraphBuilder(usr_cfg)
 
     if abstraction.lower() == "full":
-        if ctx.obj['validate']:
-            click.echo("Validate mode: abstraction level is {}".format(
-                abstraction.lower()))
+        if ctx.obj["validate"]:
+            Log.info(
+                "Validate mode: abstraction level is {abstraction.lower()}"
+            )
             sys.exit()
-        class_g_builder.build_ddg(clear=ctx.obj['clear'])
-        # We don't want to clear the table node twice.
-        # Otherwise, we'll use the table nodes created above
-        method_g_builder.build_ddg(clear=ctx.obj['clear'])
+        Log.info("Full level abstraction adds both Class and Method nodes.")
+        class_g_builder.build_ddg(clear=ctx.obj["clear"])
+        method_g_builder.build_ddg(clear=ctx.obj["clear"])
 
     elif abstraction.lower() == "class":
-        if ctx.obj['validate']:
-            click.echo("Validate mode: abstraction level is {}".format(
-                abstraction.lower()))
+        if ctx.obj["validate"]:
+            Log.info(
+                f"Validate mode: abstraction level is {abstraction.lower()}"
+            )
             sys.exit()
-        class_g_builder.build_ddg(clear=ctx.obj['clear'])
+        Log.info("Class level abstraction.")
+        class_g_builder.build_ddg(clear=ctx.obj["clear"])
 
     elif abstraction.lower() == "method":
-        if ctx.obj['validate']:
-            click.echo("Validate mode: abstraction level is {}".format(
-                abstraction.lower()))
+        if ctx.obj["validate"]:
+            Log.info(
+                f"Validate mode: abstraction level is {abstraction.lower()}"
+            )
             sys.exit()
-        method_g_builder.build_ddg(clear=ctx.obj['clear'])
+        Log.info("Method level abstraction.")
+        method_g_builder.build_ddg(clear=ctx.obj["clear"])
 
     else:
         raise click.BadArgumentUsage(
-            "Not a valid abstraction level. Valid options are 'class', 'method', 'full'.")
+            "Not a valid abstraction level. Valid options are 'class', 'method', 'full'."
+        )
+
+    Log.info("code2graph build complete")
 
-    click.echo("code2graph build complete")
+
+#########################################################################################################
+#  PARTITION - Runs the CARGO partitioning algorithm on the DGI graph to recommend/refine µS partitioning
+#########################################################################################################
+@cli.command()
+@click.option(
+    "--seed-input",
+    "-i",
+    type=click.Path(exists=True, resolve_path=True, file_okay=True),
+    default=None,
+    help="A file of user desired seed partitions.",
+)
+@click.option(
+    "--partitions-output",
+    "-o",
+    type=click.Path(exists=True, resolve_path=True, file_okay=True),
+    default=Path(os.getcwd()),
+    help="A destination to save the final partitions.",
+)
+@click.option(
+    "--partitions",
+    "-k",
+    type=int,
+    default=None,
+    help="Number of desired partitions. If no number is provided, CARGO will interpret a sane partitioning "
+    "strategy.",
+    show_default=True,
+)
+@click.pass_context
+def partition(ctx, seed_input, partitions_output, partitions):
+    """Partition is a command runs the CARGO algorithm to (re-)partition a monolith into microservices"""
+    Log.info("Partitioning the monolith with CARGO")
+
+    # Process the bolt url to be used by CARGO
+    if "bolt://" in ctx.obj["bolt"]:
+        # Strip scheme (in case it starts with bolt://)
+        bolt_url = ctx.obj["bolt"].removeprefix("bolt://")
+    elif "neo4j://" in ctx.obj["bolt"]:
+        # Strip scheme (in case it starts with neo4j://)
+        bolt_url = ctx.obj["bolt"].removeprefix("neo4j://")
+    elif "https://" in ctx.obj["bolt"]:
+        # Strip scheme (in case it starts with https://)
+        bolt_url = ctx.obj["bolt"].removeprefix("https://")
+
+    bolt_url = ctx.obj["bolt"].removeprefix("neo4j://")  # Strip scheme
+    auth_str, netloc = bolt_url.split("@")
+    hostname, hostport = netloc.split(":")
+    recommend_partitions(hostname, hostport, auth_str,
+                         partitions_output, partitions, seed_input)
diff --git a/dgi/code2graph/__init__.py b/dgi/code2graph/__init__.py
index e0a19fe..24984b3 100644
--- a/dgi/code2graph/__init__.py
+++ b/dgi/code2graph/__init__.py
@@ -1,4 +1,7 @@
+"""
+Code 2 Graph package
+"""
 from .class_graph_builder import ClassGraphBuilder
 from .method_graph_builder import MethodGraphBuilder
 
-__all__ = ('ClassGraphBuilder', 'MethodGraphBuilder')
+__all__ = ["ClassGraphBuilder", "MethodGraphBuilder"]
diff --git a/dgi/code2graph/abstract_graph_builder.py b/dgi/code2graph/abstract_graph_builder.py
index f669b99..9b03917 100644
--- a/dgi/code2graph/abstract_graph_builder.py
+++ b/dgi/code2graph/abstract_graph_builder.py
@@ -14,12 +14,18 @@
 # limitations under the License.
 ################################################################################
 
+"""
+Abstract Graph Builder
+
+This is the abstract base class for all of the graph builders
+"""
 from abc import ABC, abstractmethod
 from typing import Dict
+
 import pandas as pd
-import logging
 
 from dgi.code2graph.process_facts import ConsumeFacts
+from dgi.utils.logging import Log
 
 # Author information
 __author__ = "Rahul Krishna"
@@ -30,14 +36,17 @@
 __status__ = "Research Prototype"
 
 
+# pylint: disable=too-few-public-methods
 class AbstractGraphBuilder(ABC):
+    """Abstract graph builder is a base class that is implemented by method and class graph builders.
+    """
     def __init__(self, opt):
         self.opt = opt
 
+    @staticmethod
     @abstractmethod
     def _clear_all_nodes():
         """Delete all nodes"""
-        pass
 
     @abstractmethod
     def _create_prev_and_next_nodes(self, prev_meth: Dict, next_meth: Dict):
@@ -47,13 +56,6 @@ def _create_prev_and_next_nodes(self, prev_meth: Dict, next_meth: Dict):
             prev_df_entry (Dict): A dictionary of method information for source method
             next_df_entry (Dict): A dictionary of method information for destination method
         """
-        pass
-
-    @abstractmethod
-    def _process_entrypoints(self):
-        """ Annotate nodes with their entrypoint data
-        """
-        pass
 
     @abstractmethod
     def _populate_heap_edges(self, heap_flows: pd.DataFrame) -> None:
@@ -61,7 +63,6 @@ def _populate_heap_edges(self, heap_flows: pd.DataFrame) -> None:
         Args:
             heap_flows (pd.DataFrame): Heap flows as a pandas dataframe
         """
-        pass
 
     @abstractmethod
     def _populate_dataflow_edges(self, data_flows: pd.DataFrame) -> None:
@@ -69,7 +70,6 @@ def _populate_dataflow_edges(self, data_flows: pd.DataFrame) -> None:
         Args:
             data_flows (pd.DataFrame): Data flows as a pandas dataframe
         """
-        pass
 
     @abstractmethod
     def _populate_callreturn_edges(self, call_ret_flows: pd.DataFrame) -> None:
@@ -77,7 +77,6 @@ def _populate_callreturn_edges(self, call_ret_flows: pd.DataFrame) -> None:
         Args:
             call_ret_flows (pd.DataFrame): Data flows as a pandas dataframe
         """
-        pass
 
     def build_ddg(self, clear: bool = True) -> None:
         """Build the data dependency graph"""
@@ -87,7 +86,6 @@ def build_ddg(self, clear: bool = True) -> None:
 
         # Remove all stray nodes in the graph
         if clear:
-            logging.info("Clear flag detected... Deleting pre-existing nodes.")
             self._clear_all_nodes()
 
         # Process heap flows
@@ -99,5 +97,4 @@ def build_ddg(self, clear: bool = True) -> None:
         # Process call return flows
         self._populate_callreturn_edges(call_return_flows)
 
-        # Process entrypoints
-        self._process_entrypoints()
+        Log.info("Populating entrypoints")
diff --git a/dgi/code2graph/class_graph_builder.py b/dgi/code2graph/class_graph_builder.py
index 666c94e..4183bc2 100644
--- a/dgi/code2graph/class_graph_builder.py
+++ b/dgi/code2graph/class_graph_builder.py
@@ -1,3 +1,4 @@
+# pylint: disable=duplicate-code
 ################################################################################
 # Copyright IBM Corporation 2021, 2022
 #
@@ -14,18 +15,26 @@
 # limitations under the License.
 ################################################################################
 
+"""
+Class Graph Builder Module
+
+This module builds a class level abstraction graph.
+It is very similar to the method graph builder but with ClassNodes
+
+"""
+
 import logging
-import pandas as pd
 from typing import Dict
 
-from pathlib import Path
-from tqdm import tqdm
-
+import pandas as pd
+from neomodel import db
 from neomodel.exceptions import DoesNotExist
 
-# Import our packages
-from dgi.models import ClassNode
 from dgi.code2graph.abstract_graph_builder import AbstractGraphBuilder
+# Import local packages
+from dgi.models import ClassNode
+from dgi.utils import ProgressBarFactory
+from dgi.utils.logging import Log
 
 # Author information
 __author__ = "Rahul Krishna"
@@ -36,71 +45,23 @@
 __status__ = "Research Prototype"
 
 
+# pylint: disable=too-few-public-methods, duplicate-code
 class ClassGraphBuilder(AbstractGraphBuilder):
-    def __init__(self, opt):
-        super().__init__(opt)
+    """ Build a class level abstraction graph
+    """
 
     @staticmethod
     def _clear_all_nodes():
         """Delete all nodes"""
-        for node in ClassNode.nodes.all():
-            node.delete()
-
-    def _process_servlets(self, facts_dir: Path) -> None:
-        """ Process Servlets
-
-        Args:
-            facts_dir (Path): Directory where the facts are stored.
-        """
-
-        for key, fact_file in self.opt.JEE.SERVLET:
-            if not fact_file or not isinstance(fact_file, str):
-                continue
-            fact_file = facts_dir.joinpath(fact_file)
-            with open(fact_file, 'r') as facts:
-                classes = facts.readlines()
-            for class_name in classes:
-                class_name = class_name.rstrip()
-                try:
-                    graph_node = ClassNode.nodes.get(node_class=class_name)
-                except DoesNotExist:
-                    continue
-                graph_node.node_is_entrypoint = True
-                graph_node.node_is_servlet = True
-                graph_node.servlet_type = key
-                graph_node.save()
-
-    def _process_beans(self, facts_dir: Path) -> None:
-        """ Annotate Beans
-
-        Args:
-            facts_dir (Path): Directory where the facts are stored.
-        """
-
-        for key, fact_file in self.opt.JEE.BEANS:
-            if not fact_file or not isinstance(fact_file, str):
-                continue
-            fact_file = facts_dir.joinpath(fact_file)
-            with open(fact_file, 'r') as facts:
-                classes = facts.readlines()
-            for class_name in classes:
-                class_name = class_name.rstrip()
-                try:
-                    graph_node = ClassNode.nodes.get(node_class=class_name)
-                except DoesNotExist:
-                    continue
-                graph_node.node_is_entrypoint = True
-                graph_node.node_is_bean = True
-                graph_node.bean_type = key
-                graph_node.save()
-
-    def _process_entrypoints(self):
-        """ Annotate nodes with their entrypoint data
-        """
-
-        facts_dir = Path(self.opt.GRAPH_FACTS_DIR)
-        self._process_servlets(facts_dir)
-        self._process_beans(facts_dir)
+        Log.warn("The option clear is turned ON. Deleting pre-existing nodes.")
+        db.cypher_query("MATCH (n:ClassNode)-[r]-(m) DELETE r")
+        db.cypher_query("MATCH (n)-[r]-(m:ClassNode) DELETE r")
+        db.cypher_query("MATCH (n:ClassNode) DELETE n")
+        # count = 0
+        # for node in ClassNode.nodes.all():
+        #     count += 1
+        #     node.delete()
+        # Log.warn(f"Deleted {count} ClassNodes")
 
     def _create_prev_and_next_nodes(self, prev_meth: Dict, next_meth: Dict):
         prev_class_name = prev_meth["class"]
@@ -132,78 +93,89 @@ def _populate_heap_edges(self, heap_flows: pd.DataFrame) -> None:
         Args:
             heap_flows (pd.DataFrame): Heap flows as a pandas dataframe
         """
-        logging.info("Populating heap carried dependencies edges")
+        Log.info("Populating heap carried dependencies edges")
         rel_id = 0
-        for _, row in tqdm(heap_flows.iterrows(), total=heap_flows.shape[0]):
-            prev_meth = row.prev
-            next_meth = row.next
-
-            prev_graph_node, next_graph_node = self._create_prev_and_next_nodes(
-                prev_meth, next_meth
-            )
-
-            if prev_graph_node != next_graph_node:
-                rel = prev_graph_node.heap_flows.relationship(next_graph_node)
-                rel_id += 1
-                if rel and (rel.pmethod, rel.nmethod, rel.context, rel.heap_object) == (
-                    prev_meth["name"],
-                    next_meth["name"],
-                    row.context,
-                    row.heap_obj,
-                ):
-                    rel.weight += 1
-                    rel.rel_id = rel_id
-                    rel.save()
-                else:
-                    relationship_property = {
-                        "weight": 1,
-                        "rel_id": rel_id,
-                        "pmethod": prev_meth["name"],
-                        "nmethod": next_meth["name"],
-                        "context": row.context,
-                        "heap_object": row.heap_obj,
-                    }
-                    prev_graph_node.heap_flows.connect(
-                        next_graph_node, relationship_property
-                    )
+        with ProgressBarFactory.get_progress_bar() as progress:
+            for (_, row) in progress.track(
+                heap_flows.iterrows(), total=heap_flows.shape[0]
+            ):
+                prev_meth = row.prev
+                next_meth = row.next
+
+                prev_graph_node, next_graph_node = self._create_prev_and_next_nodes(
+                    prev_meth, next_meth
+                )
+
+                if prev_graph_node != next_graph_node:
+                    rel = prev_graph_node.heap_flows.relationship(next_graph_node)
+                    rel_id += 1
+                    if rel and (
+                        rel.pmethod,
+                        rel.nmethod,
+                        rel.context,
+                        rel.heap_object,
+                    ) == (
+                        prev_meth["name"],
+                        next_meth["name"],
+                        row.context,
+                        row.heap_obj,
+                    ):
+                        rel.weight += 1
+                        rel.rel_id = rel_id
+                        rel.save()
+                    else:
+                        relationship_property = {
+                            "weight": 1,
+                            "rel_id": rel_id,
+                            "pmethod": prev_meth["name"],
+                            "nmethod": next_meth["name"],
+                            "context": row.context,
+                            "heap_object": row.heap_obj,
+                        }
+                        prev_graph_node.heap_flows.connect(
+                            next_graph_node, relationship_property
+                        )
 
     def _populate_dataflow_edges(self, data_flows: pd.DataFrame) -> None:
         """Populate data flow dependencies
         Args:
             data_flows (pd.DataFrame): Data flows as a pandas dataframe
         """
-        logging.info("Populating dataflow edges")
+        Log.info("Populating dataflow edges")
         rel_id = 0
-        for _, row in tqdm(data_flows.iterrows(), total=data_flows.shape[0]):
-            prev_meth = row.prev
-            next_meth = row.next
-
-            prev_graph_node, next_graph_node = self._create_prev_and_next_nodes(
-                prev_meth, next_meth
-            )
-
-            if prev_graph_node != next_graph_node:
-                rel = prev_graph_node.data_flows.relationship(next_graph_node)
-                rel_id += 1
-                if rel and (rel.pmethod, rel.nmethod, rel.context) == (
-                    prev_meth["name"],
-                    next_meth["name"],
-                    row.context,
-                ):
-                    rel.rel_id = rel_id
-                    rel.weight += 1
-                    rel.save()
-                else:
-                    next_graph_node.data_flows.connect(
-                        prev_graph_node,
-                        {
-                            "weight": 1,
-                            "rel_id": rel_id,
-                            "pmethod": prev_meth["name"],
-                            "nmethod": next_meth["name"],
-                            "context": row.context,
-                        },
-                    )
+        with ProgressBarFactory.get_progress_bar() as progress:
+            for (_, row) in progress.track(
+                data_flows.iterrows(), total=data_flows.shape[0]
+            ):
+                prev_meth = row.prev
+                next_meth = row.next
+
+                prev_graph_node, next_graph_node = self._create_prev_and_next_nodes(
+                    prev_meth, next_meth
+                )
+
+                if prev_graph_node != next_graph_node:
+                    rel = prev_graph_node.data_flows.relationship(next_graph_node)
+                    rel_id += 1
+                    if rel and (rel.pmethod, rel.nmethod, rel.context) == (
+                        prev_meth["name"],
+                        next_meth["name"],
+                        row.context,
+                    ):
+                        rel.rel_id = rel_id
+                        rel.weight += 1
+                        rel.save()
+                    else:
+                        next_graph_node.data_flows.connect(
+                            prev_graph_node,
+                            {
+                                "weight": 1,
+                                "rel_id": rel_id,
+                                "pmethod": prev_meth["name"],
+                                "nmethod": next_meth["name"],
+                                "context": row.context,
+                            },
+                        )
 
     def _populate_callreturn_edges(self, call_ret_flows: pd.DataFrame) -> None:
         """Populate data flow dependencies
@@ -212,35 +184,43 @@ def _populate_callreturn_edges(self, call_ret_flows: pd.DataFrame) -> None:
         """
         logging.info("Populating call-return dependencies edges")
         rel_id = 0
-        for _, row in tqdm(call_ret_flows.iterrows(), total=call_ret_flows.shape[0]):
-            prev_meth = row.prev
-            next_meth = row.next
-
-            prev_graph_node, next_graph_node = self._create_prev_and_next_nodes(
-                prev_meth, next_meth
-            )
-
-            if prev_graph_node.node_class != next_graph_node.node_class:
-                rel = prev_graph_node.call_ret_flows.relationship(next_graph_node)
-                rel_id += 1
-                if rel and (rel.pmethod, rel.nmethod, rel.pcontext, rel.ncontext) == (
-                    prev_meth["name"],
-                    next_meth["name"],
-                    row.prev_context,
-                    row.next_context,
-                ):
-                    rel.rel_id = rel_id
-                    rel.weight += 1
-                    rel.save()
-                else:
-                    next_graph_node.call_ret_flows.connect(
-                        prev_graph_node,
-                        {
-                            "weight": 1,
-                            "rel_id": rel_id,
-                            "pmethod": prev_meth["name"],
-                            "nmethod": next_meth["name"],
-                            "pcontext": row.prev_context,
-                            "ncontext": row.next_context,
-                        },
-                    )
+        with ProgressBarFactory.get_progress_bar() as progress:
+            for (_, row) in progress.track(
+                call_ret_flows.iterrows(), total=call_ret_flows.shape[0]
+            ):
+                prev_meth = row.prev
+                next_meth = row.next
+
+                prev_graph_node, next_graph_node = self._create_prev_and_next_nodes(
+                    prev_meth, next_meth
+                )
+
+                if prev_graph_node.node_class != next_graph_node.node_class:
+                    rel = prev_graph_node.call_ret_flows.relationship(next_graph_node)
+                    rel_id += 1
+                    if rel and (
+                        rel.pmethod,
+                        rel.nmethod,
+                        rel.pcontext,
+                        rel.ncontext,
+                    ) == (
+                        prev_meth["name"],
+                        next_meth["name"],
+                        row.prev_context,
+                        row.next_context,
+                    ):
+                        rel.rel_id = rel_id
+                        rel.weight += 1
+                        rel.save()
+                    else:
+                        next_graph_node.call_ret_flows.connect(
+                            prev_graph_node,
+                            {
+                                "weight": 1,
+                                "rel_id": rel_id,
+                                "pmethod": prev_meth["name"],
+                                "nmethod": next_meth["name"],
+                                "pcontext": row.prev_context,
+                                "ncontext": row.next_context,
+                            },
+                        )
diff --git a/dgi/code2graph/etc/config.yml b/dgi/code2graph/etc/config.yml
index 41afdf5..f39501c 100644
--- a/dgi/code2graph/etc/config.yml
+++ b/dgi/code2graph/etc/config.yml
@@ -15,7 +15,7 @@ JEE:
     GenericServlet: "GenericServlet.csv"
     WebServlet: "WebServlet.csv"
     ServletFilter: "ServletFilterClass.csv"
-  BEANS:    
+  BEANS:
     EJBTransactionBean: "EJBTransactionBean.csv"
     SessionBean: "SessionBean.csv"
     SingletonBean: "SingletonBean.csv"
diff --git a/dgi/code2graph/method_graph_builder.py b/dgi/code2graph/method_graph_builder.py
index bd705f6..ff612b8 100644
--- a/dgi/code2graph/method_graph_builder.py
+++ b/dgi/code2graph/method_graph_builder.py
@@ -1,3 +1,4 @@
+# pylint: disable=duplicate-code
 ################################################################################
 # Copyright IBM Corporation 2021, 2022
 #
@@ -14,17 +15,25 @@
 # limitations under the License.
 ################################################################################
 
+"""
+Method Graph Builder Module
+
+This module builds a method level abstraction graph.
+It is very similar to the class graph builder but with MethodNodes
+"""
+
 import logging
-import pandas as pd
 from typing import Dict
-from pathlib import Path
-from tqdm import tqdm
 
+import pandas as pd
+from neomodel import db
 from neomodel.exceptions import DoesNotExist
 
-# Import our packages
-from dgi.models import MethodNode
 from dgi.code2graph.abstract_graph_builder import AbstractGraphBuilder
+# Import local packages
+from dgi.models import MethodNode
+from dgi.utils import ProgressBarFactory
+from dgi.utils.logging import Log
 
 # Author information
 __author__ = "Rahul Krishna"
@@ -35,70 +44,18 @@
 __status__ = "Research Prototype"
 
 
+# pylint: disable=too-few-public-methods, duplicate-code
 class MethodGraphBuilder(AbstractGraphBuilder):
-    def __init__(self, opt):
-        self.opt = opt
+    """ Build a method level abstraction graph
+    """
 
     @staticmethod
     def _clear_all_nodes():
         """Delete all nodes"""
-        for node in MethodNode.nodes.all():
-            node.delete()
-
-    def _process_servlets(self, facts_dir: Path) -> None:
-        """ Process Servlets
-
-        Args:
-            facts_dir (Path): Directory where the facts are stored.
-        """
-
-        for key, fact_file in self.opt.JEE.SERVLET:
-            if not fact_file or not isinstance(fact_file, str):
-                continue
-            fact_file = facts_dir.joinpath(fact_file)
-            with open(fact_file, 'r') as facts:
-                classes = facts.readlines()
-            for class_name in classes:
-                class_name = class_name.rstrip()
-                all_method_nodes = MethodNode.nodes.all()
-                for graph_node in all_method_nodes:
-                    if graph_node.node_class != class_name:
-                        continue
-                    graph_node.node_is_entrypoint = True
-                    graph_node.node_is_servlet = True
-                    graph_node.servlet_type = key
-                    graph_node.save()
-
-    def _process_beans(self, facts_dir: Path) -> None:
-        """ Annotate Beans
-
-        Args:
-            facts_dir (Path): Directory where the facts are stored.
-        """
-
-        for key, fact_file in self.opt.JEE.BEANS:
-            if not fact_file or not isinstance(fact_file, str):
-                continue
-            fact_file = facts_dir.joinpath(fact_file)
-            with open(fact_file, 'r') as facts:
-                classes = facts.readlines()
-            for class_name in classes:
-                all_method_nodes = MethodNode.nodes.all()
-                for graph_node in all_method_nodes:
-                    if graph_node.node_class != class_name:
-                        continue
-                    graph_node.node_is_entrypoint = True
-                    graph_node.node_is_bean = True
-                    graph_node.bean_type = key
-                    graph_node.save()
-
-    def _process_entrypoints(self):
-        """ Annotate nodes with their entrypoint data
-        """
-
-        facts_dir = Path(self.opt.GRAPH_FACTS_DIR)
-        self._process_servlets(facts_dir)
-        self._process_beans(facts_dir)
+        Log.warn("The option clear is turned ON. Deleting pre-existing nodes.")
+        db.cypher_query("MATCH (n:MethodNode)-[r]-(m) DELETE r")
+        db.cypher_query("MATCH (n)-[r]-(m:MethodNode) DELETE r")
+        db.cypher_query("MATCH (n:MethodNode) DELETE n")
 
     def _create_prev_and_next_nodes(self, prev_meth: Dict, next_meth: Dict):
         """_summary_
@@ -148,34 +105,37 @@ def _populate_heap_edges(self, heap_flows: pd.DataFrame) -> None:
         logging.info("Populating heap carried dependencies edges")
 
         rel_id = 0
-        for _, row in tqdm(heap_flows.iterrows(), total=heap_flows.shape[0]):
-            prev_meth = row.prev
-            next_meth = row.next
-
-            prev_graph_node, next_graph_node = self._create_prev_and_next_nodes(
-                prev_meth, next_meth
-            )
-
-            if prev_graph_node.node_name != next_graph_node.node_name:
-                rel = prev_graph_node.heap_flows.relationship(next_graph_node)
-                rel_id += 1
-                if rel and (rel.context, rel.heap_object) == (
-                    row.context,
-                    row.heap_obj,
-                ):
-                    rel.weight += 1
-                    rel.rel_id = rel_id
-                    rel.save()
-                else:
-                    relationship_property = {
-                        "weight": 1,
-                        "rel_id": rel_id,
-                        "context": row.context,
-                        "heap_object": row.heap_obj,
-                    }
-                    prev_graph_node.heap_flows.connect(
-                        next_graph_node, relationship_property
-                    )
+        with ProgressBarFactory.get_progress_bar() as progress:
+            for (_, row) in progress.track(
+                heap_flows.iterrows(), total=heap_flows.shape[0]
+            ):
+                prev_meth = row.prev
+                next_meth = row.next
+
+                prev_graph_node, next_graph_node = self._create_prev_and_next_nodes(
+                    prev_meth, next_meth
+                )
+
+                if prev_graph_node.node_name != next_graph_node.node_name:
+                    rel = prev_graph_node.heap_flows.relationship(next_graph_node)
+                    rel_id += 1
+                    if rel and (rel.context, rel.heap_object) == (
+                        row.context,
+                        row.heap_obj,
+                    ):
+                        rel.weight += 1
+                        rel.rel_id = rel_id
+                        rel.save()
+                    else:
+                        relationship_property = {
+                            "weight": 1,
+                            "rel_id": rel_id,
+                            "context": row.context,
+                            "heap_object": row.heap_obj,
+                        }
+                        prev_graph_node.heap_flows.connect(
+                            next_graph_node, relationship_property
+                        )
 
     def _populate_dataflow_edges(self, data_flows: pd.DataFrame) -> None:
         """Populate data flow dependencies
@@ -184,24 +144,27 @@ def _populate_dataflow_edges(self, data_flows: pd.DataFrame) -> None:
         """
         logging.info("Populating dataflow edges")
         rel_id = 0
-        for _, row in tqdm(data_flows.iterrows(), total=data_flows.shape[0]):
-            prev_meth = row.prev
-            next_meth = row.next
-
-            prev_graph_node, next_graph_node = self._create_prev_and_next_nodes(
-                prev_meth, next_meth
-            )
-
-            if prev_graph_node.node_name != next_graph_node.node_name:
-                rel = prev_graph_node.data_flows.relationship(next_graph_node)
-                rel_id += 1
-                if rel and rel.context == row.context:
-                    rel.weight += 1
-                    rel.save()
-                else:
-                    next_graph_node.data_flows.connect(
-                        prev_graph_node, {"weight": 1, "context": row.context}
-                    )
+        with ProgressBarFactory.get_progress_bar() as progress:
+            for (_, row) in progress.track(
+                data_flows.iterrows(), total=data_flows.shape[0]
+            ):
+                prev_meth = row.prev
+                next_meth = row.next
+
+                prev_graph_node, next_graph_node = self._create_prev_and_next_nodes(
+                    prev_meth, next_meth
+                )
+
+                if prev_graph_node.node_name != next_graph_node.node_name:
+                    rel = prev_graph_node.data_flows.relationship(next_graph_node)
+                    rel_id += 1
+                    if rel and rel.context == row.context:
+                        rel.weight += 1
+                        rel.save()
+                    else:
+                        next_graph_node.data_flows.connect(
+                            prev_graph_node, {"weight": 1, "context": row.context}
+                        )
 
     def _populate_callreturn_edges(self, call_ret_flows: pd.DataFrame) -> None:
         """Populate data flow dependencies
@@ -210,29 +173,32 @@ def _populate_callreturn_edges(self, call_ret_flows: pd.DataFrame) -> None:
         """
         logging.info("Populating call-return dependencies edges")
         rel_id = 0
-        for _, row in tqdm(call_ret_flows.iterrows(), total=call_ret_flows.shape[0]):
-            prev_meth = row.prev
-            next_meth = row.next
-
-            prev_graph_node, next_graph_node = self._create_prev_and_next_nodes(
-                prev_meth, next_meth
-            )
-
-            if prev_graph_node.node_class != next_graph_node.node_class:
-                rel = prev_graph_node.call_ret_flows.relationship(next_graph_node)
-                rel_id += 1
-                if rel and (rel.pcontext, rel.ncontext) == (
-                    row.prev_context,
-                    row.next_context,
-                ):
-                    rel.weight += 1
-                    rel.save()
-                else:
-                    next_graph_node.call_ret_flows.connect(
-                        prev_graph_node,
-                        {
-                            "weight": 1,
-                            "pcontext": row.prev_context,
-                            "ncontext": row.next_context,
-                        },
-                    )
+        with ProgressBarFactory.get_progress_bar() as progress:
+            for (_, row) in progress.track(
+                call_ret_flows.iterrows(), total=call_ret_flows.shape[0]
+            ):
+                prev_meth = row.prev
+                next_meth = row.next
+
+                prev_graph_node, next_graph_node = self._create_prev_and_next_nodes(
+                    prev_meth, next_meth
+                )
+
+                if prev_graph_node.node_class != next_graph_node.node_class:
+                    rel = prev_graph_node.call_ret_flows.relationship(next_graph_node)
+                    rel_id += 1
+                    if rel and (rel.pcontext, rel.ncontext) == (
+                        row.prev_context,
+                        row.next_context,
+                    ):
+                        rel.weight += 1
+                        rel.save()
+                    else:
+                        next_graph_node.call_ret_flows.connect(
+                            prev_graph_node,
+                            {
+                                "weight": 1,
+                                "pcontext": row.prev_context,
+                                "ncontext": row.next_context,
+                            },
+                        )
diff --git a/dgi/code2graph/process_facts.py b/dgi/code2graph/process_facts.py
index 772b3b7..30057fc 100644
--- a/dgi/code2graph/process_facts.py
+++ b/dgi/code2graph/process_facts.py
@@ -14,20 +14,30 @@
 # limitations under the License.
 ################################################################################
 
+"""
+Process facts Module
+
+This module processes the facts from the code analysis
+"""
+
+import errno
+import json
 import os
 import re
-import json
-import errno
 from csv import reader
-import pandas as pd
 from pathlib import Path
 from typing import Dict, Tuple
-from .utils.parse_config import Config
+
+import pandas as pd
+
+from dgi.utils.parse_config import Config
 
 
 class ConsumeFacts:
+    "Synthesize DOOP facts"
+
     def __init__(self, conf: Config) -> None:
-        """Take raw datalog facts and convert them to a consumable form.
+        """Take raw datalog facts and convert them to a comsumable form.
 
         Args:
             p_root (str): Path to the project root folder.
@@ -35,13 +45,14 @@ def __init__(self, conf: Config) -> None:
             opt_cfg (Config): Other configs
         """
         self.conf = conf
+        self.method_info = {}
         self.__setup()
 
     def __setup(self) -> None:
         """Perform some setup for auxiliary paths, data structures, etc."""
         self.absolute_facts_dir = Path(self.conf.GRAPH_FACTS_DIR)
-        self.method_info = dict()
-        self.contexts = dict()
+        self.method_info = {}
+        self.contexts = {}
 
     def _jsonify_method_string(self, raw: str) -> str:
         """Convert doop style method information to a json formatted string
@@ -84,9 +95,7 @@ def _jsonify_context(self, raw: str) -> str:
             str: JSON string representation of the doop context
 
         Notes:
-            Take the context information from:
-            "[class_name_1/method_name_1/obj_name_1/id1, class_name_2/method_name_2/obj_name_2/id2]"
-            And, converts it to the following format:
+            Take the context information and convert it to:
             "{
                 {
                     "class": "class_name_1",
@@ -103,7 +112,7 @@ def _jsonify_context(self, raw: str) -> str:
             }"
         """
 
-        raw_str = re.sub("[\[\]]", "", raw)  # noqa:  W605
+        raw_str = re.sub(r"[\[\]]", "", raw)  # noqa: W601
         raw_ctx_lst = raw_str.split(", ")
 
         for i, str_el in enumerate(raw_ctx_lst):
@@ -132,7 +141,8 @@ def _jsonify_context(self, raw: str) -> str:
                 }
             else:
                 raw_substr = raw_ctx_lst[i].split("/")
-                class_name, method_signature = (*raw_substr[0][1:-1].split(": "),)
+                class_name, method_signature = (
+                    *raw_substr[0][1:-1].split(": "),)
                 try:
                     method_rtype, method_name, _ = (
                         *re.sub("[(:)]", " ", method_signature).split(),
@@ -155,13 +165,14 @@ def _jsonify_context(self, raw: str) -> str:
         ctx_json_str = json.dumps(raw_ctx_lst)
 
         # Add the found context to a dictionary.
-        # NOTE: For now, we keep the values empty.
-        # In a later method (`_update_context_transistions(...)`), we update it lazily.
+        # NOTE: For now, we keep the values empty. In a later method (`_update_context_transistions(...)`),
+        # we update it lazily.
         self.contexts.update({ctx_json_str: {"prev": [], "next": []}})
 
         return raw_ctx_lst
 
-    def _jsonify_heap_obj(self, heapobj_str: str) -> str:
+    @staticmethod
+    def _jsonify_heap_obj(heapobj_str: str) -> str:
         """Create a JSON string from raw heap object string from doop
 
         Args:
@@ -220,8 +231,7 @@ def _process_method_info(self, method_info_file: Path) -> None:
         Args:
             method_info_file (Path): Path to doop method information file
         """
-        self.method_info = dict()
-        with open(method_info_file, "r") as file_obj:
+        with open(method_info_file, "r", encoding='utf-8') as file_obj:
             csv_reader = reader(file_obj, delimiter="\t")
             for row in csv_reader:
                 key = "::".join(row[::-1][1:3])
@@ -243,10 +253,14 @@ def _process_heap_carried_dependencies(self, fact_loc: Path) -> pd.DataFrame:
         """
         heap_flows_df = pd.read_csv(fact_loc, header=None, delimiter="\t")
         heap_flows_df.columns = ["context", "heap_obj", "prev", "next"]
-        heap_flows_df.context = heap_flows_df.context.apply(self._jsonify_context)
-        heap_flows_df.prev = heap_flows_df.prev.apply(self._jsonify_method_string)
-        heap_flows_df.next = heap_flows_df.next.apply(self._jsonify_method_string)
-        heap_flows_df.heap_obj = heap_flows_df.heap_obj.apply(self._jsonify_heap_obj)
+        heap_flows_df.context = heap_flows_df.context.apply(
+            self._jsonify_context)
+        heap_flows_df.prev = heap_flows_df.prev.apply(
+            self._jsonify_method_string)
+        heap_flows_df.next = heap_flows_df.next.apply(
+            self._jsonify_method_string)
+        heap_flows_df.heap_obj = heap_flows_df.heap_obj.apply(
+            self._jsonify_heap_obj)
         return heap_flows_df
 
     def _process_call_return_dependencies(
@@ -261,22 +275,26 @@ def _process_call_return_dependencies(
         Returns:
             pd.DataFrame: The dataframe containing processed call-return dependencies
         """
-        callret_flows_df = pd.read_csv(calls_fact_loc, header=None, delimiter="\t")
+        callret_flows_df = pd.read_csv(
+            calls_fact_loc, header=None, delimiter="\t")
         pd.concat(
             [
                 callret_flows_df,
                 pd.read_csv(returns_fact_loc, header=None, delimiter="\t"),
             ]
         )
-        callret_flows_df.columns = ["prev_context", "prev", "next_context", "next"]
+        callret_flows_df.columns = [
+            "prev_context", "prev", "next_context", "next"]
         callret_flows_df.prev_context = callret_flows_df.prev_context.apply(
             self._jsonify_context
         )
         callret_flows_df.next_context = callret_flows_df.next_context.apply(
             self._jsonify_context
         )
-        callret_flows_df.prev = callret_flows_df.prev.apply(self._jsonify_method_string)
-        callret_flows_df.next = callret_flows_df.next.apply(self._jsonify_method_string)
+        callret_flows_df.prev = callret_flows_df.prev.apply(
+            self._jsonify_method_string)
+        callret_flows_df.next = callret_flows_df.next.apply(
+            self._jsonify_method_string)
         return callret_flows_df
 
     def _process_data_dependencies(self, fact_loc: Path) -> pd.DataFrame:
@@ -290,9 +308,12 @@ def _process_data_dependencies(self, fact_loc: Path) -> pd.DataFrame:
         """
         data_flows_df = pd.read_csv(fact_loc, header=None, delimiter="\t")
         data_flows_df.columns = ["context", "prev", "next"]
-        data_flows_df.context = data_flows_df.context.apply(self._jsonify_context)
-        data_flows_df.prev = data_flows_df.prev.apply(self._jsonify_method_string)
-        data_flows_df.next = data_flows_df.next.apply(self._jsonify_method_string)
+        data_flows_df.context = data_flows_df.context.apply(
+            self._jsonify_context)
+        data_flows_df.prev = data_flows_df.prev.apply(
+            self._jsonify_method_string)
+        data_flows_df.next = data_flows_df.next.apply(
+            self._jsonify_method_string)
         return data_flows_df
 
     def get_method_info(self) -> Dict:
@@ -340,7 +361,7 @@ def process_and_get_facts_data(
 
         if not method_info_file.exists():
             raise FileNotFoundError(
-                errno.ENOENT, os.strerror(errno.ENOENT), method_info_file.__str__()
+                errno.ENOENT, os.strerror(errno.ENOENT), str(method_info_file)
             )
 
         self._process_method_info(method_info_file)
@@ -354,7 +375,7 @@ def process_and_get_facts_data(
 
         if not heap_facts_file.exists():
             raise FileNotFoundError(
-                errno.ENOENT, os.strerror(errno.ENOENT), heap_facts_file.__str__()
+                errno.ENOENT, os.strerror(errno.ENOENT), str(heap_facts_file)
             )
 
         heap_flows = self._process_heap_carried_dependencies(heap_facts_file)
@@ -368,7 +389,8 @@ def process_and_get_facts_data(
 
         if not data_dep_facts_file.exists():
             raise FileNotFoundError(
-                errno.ENOENT, os.strerror(errno.ENOENT), data_dep_facts_file.__str__()
+                errno.ENOENT, os.strerror(
+                    errno.ENOENT), str(data_dep_facts_file)
             )
 
         data_flows = self._process_data_dependencies(data_dep_facts_file)
@@ -385,12 +407,14 @@ def process_and_get_facts_data(
 
         if not call_dep_facts_file.exists():
             raise FileNotFoundError(
-                errno.ENOENT, os.strerror(errno.ENOENT), call_dep_facts_file.__str__()
+                errno.ENOENT, os.strerror(
+                    errno.ENOENT), str(call_dep_facts_file)
             )
 
         if not return_dep_facts_file.exists():
             raise FileNotFoundError(
-                errno.ENOENT, os.strerror(errno.ENOENT), return_dep_facts_file.__str__()
+                errno.ENOENT, os.strerror(
+                    errno.ENOENT), str(return_dep_facts_file)
             )
 
         call_return_flows = self._process_call_return_dependencies(
diff --git a/dgi/code2graph/utils/__init__.py b/dgi/code2graph/utils/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/dgi/models/__init__.py b/dgi/models/__init__.py
index e877e0e..71ba65c 100644
--- a/dgi/models/__init__.py
+++ b/dgi/models/__init__.py
@@ -14,6 +14,10 @@
 # limitations under the License.
 ################################################################################
 
+"""
+Neo4J Models
+"""
+
 from .entities import ClassNode, MethodNode, SQLTable, SQLColumn
 
-__all__ = ('ClassNode', 'MethodNode', 'SQLTable', 'SQLColumn')
+__all__ = ["ClassNode", "MethodNode", "SQLTable", "SQLColumn"]
diff --git a/dgi/models/entities.py b/dgi/models/entities.py
index b624a4f..6307a52 100644
--- a/dgi/models/entities.py
+++ b/dgi/models/entities.py
@@ -14,27 +14,32 @@
 # limitations under the License.
 ################################################################################
 
+"""
+Entities Model
+"""
+
 from neomodel import (
     StructuredNode,
-    StringProperty,
-    IntegerProperty,
-    ArrayProperty,
     RelationshipTo,
 )
+from neomodel.properties import (
+    ArrayProperty,
+    IntegerProperty,
+    UniqueIdProperty,
+    StringProperty,
+    BooleanProperty,
+)
+from neomodel.relationship_manager import RelationshipFrom
+
 from .relationships import (
     HeapCarriedRelationship,
     DataRelationship,
     CallReturnRelationship,
     TransactionWrite,
     TransactionRead,
+    TransactionCallTrace,
 )
 
-from neomodel.properties import (
-    UniqueIdProperty,
-    BooleanProperty,
-)
-from neomodel.relationship_manager import RelationshipFrom
-
 __author__ = "Rahul Krishna"
 __license__ = "Apache 2.0"
 __version__ = "1.0"
@@ -43,6 +48,7 @@
 __status__ = "Research Prototype"
 
 
+# pylint: disable=abstract-method
 class MethodNode(StructuredNode):
     """A basic node to be inherited. Every node has 3 relationships:
     1. Heap relationship
@@ -62,6 +68,8 @@ class MethodNode(StructuredNode):
     node_class = StringProperty(required=True)
     node_class_name = StringProperty(required=True)
     node_method = StringProperty(required=True, unique_index=True)
+    node_is_tx_entry = BooleanProperty(default=False)
+    partition_id = IntegerProperty()
 
     # Relationships
     heap_flows = RelationshipTo(
@@ -73,6 +81,9 @@ class MethodNode(StructuredNode):
     call_ret_flows = RelationshipFrom(
         "MethodNode", "CALL_RETURN_DEPENDENCY", model=CallReturnRelationship
     )
+    transaction_method_call = RelationshipTo(
+        "MethodNode", "TRANSACTIONAL_TRACE", model=TransactionCallTrace
+    )
     transaction_read = RelationshipFrom(
         "SQLTable", "TRANSACTION_READ", model=TransactionRead
     )
@@ -81,6 +92,7 @@ class MethodNode(StructuredNode):
     )
 
 
+# pylint: disable=abstract-method
 class ClassNode(StructuredNode):
     """A basic node to be inherited. Every node has 3 relationships:
     1. Heap relationship
@@ -119,6 +131,7 @@ class ClassNode(StructuredNode):
     )
 
 
+# pylint: disable=abstract-method
 class SQLColumn(StructuredNode):
     """Represents a column in an SQL table"""
 
@@ -130,6 +143,7 @@ class SQLColumn(StructuredNode):
     foreign_key = RelationshipTo("SQLColumn", "FOREIGN_KEY")
 
 
+# pylint: disable=abstract-method
 class SQLTable(StructuredNode):
     """Represents a table in an SQL database"""
 
diff --git a/dgi/models/relationships.py b/dgi/models/relationships.py
index c81c894..42182b5 100644
--- a/dgi/models/relationships.py
+++ b/dgi/models/relationships.py
@@ -14,6 +14,9 @@
 # limitations under the License.
 ################################################################################
 
+"""
+Relationships Model
+"""
 from neomodel import (
     StringProperty,
     IntegerProperty,
@@ -35,7 +38,8 @@ class TransactionRead(StructuredRel):
 
     txid = IntegerProperty(required=True)
     tx_meth = StringProperty(required=True)
-    action = StringProperty(required=True)
+    action = StringProperty(required=False)
+    sql_query = StringProperty(required=False)
 
 
 class TransactionWrite(StructuredRel):
@@ -43,7 +47,15 @@ class TransactionWrite(StructuredRel):
 
     txid = IntegerProperty(required=True)
     tx_meth = StringProperty(required=True)
-    action = StringProperty(required=True)
+    action = StringProperty(required=False)
+    sql_query = StringProperty(required=False)
+
+
+class TransactionCallTrace(StructuredRel):
+    """Transaction Write"""
+
+    txid = IntegerProperty(required=True)
+    service_entry = StringProperty(required=True)
 
 
 class HeapCarriedRelationship(StructuredRel):
diff --git a/dgi/partitioning/__init__.py b/dgi/partitioning/__init__.py
new file mode 100644
index 0000000..7f98f32
--- /dev/null
+++ b/dgi/partitioning/__init__.py
@@ -0,0 +1,6 @@
+"""
+This package contains all the various partitioning schemes to partition monolith to microservices
+"""
+from dgi.partitioning.partition import recommend_partitions
+
+__all__ = ['recommend_partitions']
diff --git a/dgi/partitioning/partition.py b/dgi/partitioning/partition.py
new file mode 100644
index 0000000..31b6678
--- /dev/null
+++ b/dgi/partitioning/partition.py
@@ -0,0 +1,75 @@
+################################################################################
+# Copyright IBM Corporation 2021, 2022
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License a#t˚†
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+################################################################################
+"""
+Partition DGI Graph
+"""
+import json
+from collections import defaultdict
+from pathlib import Path
+from statistics import mode
+from typing import Union
+from neomodel.exceptions import DoesNotExist
+from cargo import Cargo
+from dgi.models.entities import ClassNode, MethodNode
+
+
+def recommend_partitions(    # noqa:  R0913,R0914
+        hostname: str, hostport, auth_str: str, output: str, partitions: int,
+        seed_input: Union[Path, None] = None, verbosity: bool = True) -> None:
+    """Recommend partitions with CARGO.
+
+    Args:
+        hostname(str): Neo4j hostname
+        hostport(str): Neo4j port
+    """
+    cargo = Cargo(
+        use_dgi=True,
+        dgi_neo4j_hostname=hostname,
+        dgi_neo4j_hostport=hostport,
+        dgi_neo4j_auth=auth_str,
+        verbose=verbosity,
+    )
+
+    if seed_input is None:
+        _, assignments = cargo.run("auto", max_part=partitions)
+    else:
+        _, assignments = cargo.run("file", labels_file=seed_input)
+
+    class_partitions = defaultdict(lambda: [])
+
+    for method_signature, partition in assignments.items():
+        try:
+            dgi_method_node = MethodNode.nodes.get(
+                node_method=method_signature)
+            dgi_method_node.partition_id = partition
+            dgi_method_node.save()
+        except DoesNotExist:
+            pass
+        class_name = method_signature.rsplit(".", 1)[0]
+        class_partitions[class_name].append(partition)
+
+    for class_name, methods_partitions in class_partitions.items():
+        try:
+            dgi_class_node = ClassNode.nodes.get(node_class=class_name)
+            dgi_class_node.partition_id = mode(methods_partitions)
+            dgi_class_node.save()
+        except DoesNotExist:
+            pass
+
+    # for class_name, methods_partitions in class_partitions.items():
+    #     class_partitions[class_name] = mode(methods_partitions)
+
+    with open(Path(output).joinpath('partitions.json'), 'w+', encoding='utf-8') as partitions_file:
+        json.dump(assignments, partitions_file, indent=4, sort_keys=True)
diff --git a/dgi/schema2graph/schema_loader.py b/dgi/schema2graph/schema_loader.py
index 4633439..4a3c327 100644
--- a/dgi/schema2graph/schema_loader.py
+++ b/dgi/schema2graph/schema_loader.py
@@ -1,3 +1,4 @@
+# pylint: disable=R0801
 ################################################################################
 # Copyright IBM Corporation 2021, 2022
 #
@@ -14,10 +15,15 @@
 # limitations under the License.
 ################################################################################
 
-# Install CLI for development with:
-#   pip install --editable .
-from tqdm import tqdm
+"""
+Schema Loader
+
+This module is responsible for loading the schema into the GraphDB
+"""
+
+from dgi.utils.progress_bar_factory import ProgressBarFactory
 from dgi.models import SQLColumn, SQLTable
+from dgi.utils.logging import Log
 
 
 def remove_all_nodes():
@@ -31,83 +37,93 @@ def remove_all_nodes():
 
 
 def process_foreign_keys(all_foreign_keys: list):
-    print("Processing foreign keys:")
-    for entry in tqdm(all_foreign_keys, total=len(all_foreign_keys)):
-        my_table_name = entry[0]
-        my_column_name = entry[1]
-        ref_table_name = entry[2]
-        ref_column_name = entry[3]
-        # print(f"Processing foreign key from {my_table_name}.{my_column_name} to {ref_table_name}.{ref_column_name}")
-        my_tab = SQLTable.nodes.get(name=my_table_name)
-        if my_tab:
-            my_col = my_tab.columns.get(name=my_column_name)
-            if my_col:
-                ref_tab = SQLTable.nodes.get(name=ref_table_name)
-                if ref_tab:
-                    ref_col = ref_tab.columns.get(name=ref_column_name)
-                    if ref_col:
-                        # print(f"Connecting {my_tab.name}.{my_col.name} to {ref_tab.name}.{ref_col.name}")
-                        my_col.foreign_key.connect(ref_col)
+    """Processes the foreign key relations into the graph
+
+    Args:
+        all_foreign_keys (list): A list of foreign keys from the schema
+    """
+    Log.info("Processing foreign keys:")
+    with ProgressBarFactory.get_progress_bar() as prog_bar:
+        for entry in prog_bar.track(all_foreign_keys, total=len(all_foreign_keys)):
+            my_table_name = entry[0]
+            my_column_name = entry[1]
+            ref_table_name = entry[2]
+            ref_column_name = entry[3]
+            # Log.info(f"Processing foreign key from {my_table_name}.{my_column_name} to {ref_table_name}.{ref_column_name}")
+            my_tab = SQLTable.nodes.get(name=my_table_name)
+            if my_tab:
+                my_col = my_tab.columns.get(name=my_column_name)
+                if my_col:
+                    ref_tab = SQLTable.nodes.get(name=ref_table_name)
+                    if ref_tab:
+                        ref_col = ref_tab.columns.get(name=ref_column_name)
+                        if ref_col:
+                            # Log.info(f"Connecting {my_tab.name}.{my_col.name} to {ref_tab.name}.{ref_col.name}")
+                            my_col.foreign_key.connect(ref_col)
+                        else:
+                            Log.info(
+                                f"*** Error: Could not find reference column: {ref_column_name}"
+                            )
                     else:
-                        print(
-                            f"*** Error: Could not find reference column: {ref_column_name}"
+                        Log.info(
+                            f"*** Error: Could not find reference table: {ref_table_name}"
                         )
                 else:
-                    print(
-                        f"*** Error: Could not find reference table: {ref_table_name}"
-                    )
+                    Log.info(f"*** Error: Could not find self column: {my_column_name}")
             else:
-                print(f"*** Error: Could not find self column: {my_column_name}")
-        else:
-            print(f"*** Error: Could not find self table: {my_table_name}")
+                Log.info(f"*** Error: Could not find self table: {my_table_name}")
 
 
 def load_graph(result):
     """Populates the graph from a dictionary"""
     all_foreign_keys = []
 
-    print("Processing schema tables:")
-    for schema in tqdm(result["tables"], total=len(result["tables"])):
-        # print(schema["table_name"])
-        table = SQLTable.nodes.get_or_none(name=schema["table_name"])
-        if table:
-            table.schema = schema["schema"]
-            table.primary_key = schema["primary_key"]
-            table.index = schema["index"]
-            table.save()
-        else:
-            table = SQLTable(
-                name=schema["table_name"],
-                schema=schema["schema"],
-                primary_key=schema["primary_key"],
-                index=schema["index"],
-            ).save()
-        for column in schema["columns"]:
-            name = column["name"]
-            # print(f" - {name}")
-            col = table.columns.get_or_none(name=name)
-            if col:
-                col.datatype = column["type"]
-                if column["name"] in schema["primary_key"]:
-                    col.is_primary = True
-                col.save()
+    Log.info("Processing schema tables:")
+    with ProgressBarFactory.get_progress_bar() as prog_bar:
+        for schema in prog_bar.track(result["tables"], total=len(result["tables"])):
+            # Log.info(schema["table_name"])
+            table = SQLTable.nodes.get_or_none(name=schema["table_name"])
+            if table:
+                table.schema = schema["schema"]
+                table.primary_key = schema["primary_key"]
+                table.index = schema["index"]
+                table.save()
             else:
-                # col = SQLColumn(name=column["name"], datatype=column["type"], size=column["size"])
-                col = SQLColumn(name=column["name"], datatype=column["type"])
-                if column["name"] in schema["primary_key"]:
-                    col.is_primary = True
-                col.save()
-                table.columns.connect(col)
+                table = SQLTable(
+                    name=schema["table_name"],
+                    schema=schema["schema"],
+                    primary_key=schema["primary_key"],
+                    index=schema["index"],
+                ).save()
+            for column in schema["columns"]:
+                name = column["name"]
+                # Log.info(f" - {name}")
+                col = table.columns.get_or_none(name=name)
+                if col:
+                    col.datatype = column["type"]
+                    if column["name"] in schema["primary_key"]:
+                        col.is_primary = True
+                    col.save()
+                else:
+                    # col = SQLColumn(name=column["name"], datatype=column["type"], size=column["size"])
+                    col = SQLColumn(name=column["name"], datatype=column["type"])
+                    if column["name"] in schema["primary_key"]:
+                        col.is_primary = True
+                    col.save()
+                    table.columns.connect(col)
 
-            # process foreign keys
-            if column["references"]:
-                all_foreign_keys.append(
-                    (
-                        schema["table_name"],
-                        name,
-                        column["references"]["table"],
-                        column["references"]["column"],
+                # process foreign keys
+                if column["references"]:
+                    all_foreign_keys.append(
+                        (
+                            schema["table_name"],
+                            name,
+                            column["references"]["table"],
+                            column["references"]["column"],
+                        )
                     )
-                )
 
-    process_foreign_keys(all_foreign_keys)
+    if len(all_foreign_keys) > 0:
+        process_foreign_keys(all_foreign_keys)
+    else:
+        Log.warn("No foreign key relationships found.")
diff --git a/dgi/tx2graph/README.md b/dgi/tx2graph/README.md
new file mode 100644
index 0000000..5dec0c8
--- /dev/null
+++ b/dgi/tx2graph/README.md
@@ -0,0 +1,92 @@
+# Tx2Graph - Transaction Dependency Extractor
+
+Transaction Dependency Extractor analyzes CRUD-based (Create, Read, Update, Delete) database dependencies across transaction and stores into a Neo4j graph database.
+
+## Prerequisites
+
+- Python 3.x
+- Neo4j server
+
+## Usage
+
+```bash
+tx2g --help
+Usage: tx2g [OPTIONS]
+
+  This command loads DiVA database transactions into a graph
+
+Options:
+  -i, --input PATH  DiVA Transaction JSON file  [required]
+  -c, --clear       Clear all Table nodes before loading
+  -v, --verbose     Enable Verbose mode
+  --help            Show this message and exit.
+```
+
+## Getting Started
+
+These instructions will take you step by step through generating and importing the output from Tackle-DiVA into Neo4j.
+
+### Step 1
+
+You must first have Java source files to scan or use a sample file. You can generate them using [Tackle-DiVA](https://github.com/konveyor/tackle-diva).
+
+```bash
+docker run --rm \
+  -v </path-to-source-code-folder>:/app \
+  -v $(PWD):/diva-distribution/output \
+  quay.io/konveyor/tackle-diva
+```
+
+This will write the output files to the current working directory. Of course you can substitute `$(PWD)` for whatever output folder you'd like. You only need the `transaction.json` file to populate the Neo4j graph.
+
+If you want to use the scan we have already produced from the [DayTrader7](https://github.com/WASdev/sample.daytrader7) or [TradingApp](https://github.com/saud-aslam/trading-app) application, look in the `samples` folder for `daytrader-transactions.json` and `trading-app-transactions.json` respectively.
+
+### Step 2
+
+Execute `tx2g` specifying the transaction JSON file using the `--input` parameter. You can optionally use `-c` to clear the graph first and `-v` for more verbose output.
+
+```bash
+tx2g --input samples/daytrader-transaction.json -c -v
+```
+
+### Step 3
+
+Open Neo4j portal at http://localhost:7474/browser/ and query nodes.
+
+- Show all nodes :
+
+  ```cypher
+  MATCH (a)-[]->(b) RETURN (a)-[]->(b)
+  ```
+
+- Focus on Write operation :
+
+  ```cypher
+  MATCH (a)-[:Write]->(b) RETURN (a)-[:Write]->(b)
+  ```
+
+- Focus on Read operation :
+
+  ```cypher
+  MATCH (a)-[:Read]->(b) RETURN (a)-[:Read]->(b)
+  ```
+  
+- Delete all nodes :
+
+  ```cypher
+  MATCH (v) DETACH DELETE v
+  ```
+  
+## Sample Queries
+
+Find instances that have heap dependencies to transactions
+
+```cypher
+Match p=(K)-[:HEAP_DEPENDENCY]-(M:ClassNode)-[]-(N:SQLTable)-[]-(Q:ClassNode) return p
+```
+
+Find instances where multiple classes partake in a database transaction
+
+```cypher
+Match p=(M:ClassNode)-[]-(N:SQLTable)-[]-(Q:ClassNode) return p
+```
diff --git a/dgi/tx2graph/__init__.py b/dgi/tx2graph/__init__.py
index ad1cb63..53e7579 100644
--- a/dgi/tx2graph/__init__.py
+++ b/dgi/tx2graph/__init__.py
@@ -1,4 +1,8 @@
+"""
+Transaction 2 Graph Module
+"""
+
 from .class_transaction_loader import ClassTransactionLoader
 from .method_transaction_loader import MethodTransactionLoader
 
-__all__ = ('ClassTransactionLoader', 'MethodTransactionLoader')
+__all__ = ["ClassTransactionLoader", "MethodTransactionLoader"]
diff --git a/dgi/tx2graph/abstract_transaction_loader.py b/dgi/tx2graph/abstract_transaction_loader.py
index 9562922..22a889b 100644
--- a/dgi/tx2graph/abstract_transaction_loader.py
+++ b/dgi/tx2graph/abstract_transaction_loader.py
@@ -14,42 +14,43 @@
 # limitations under the License.
 ################################################################################
 
+"""
+Abstract Transaction Loader Module
+"""
+
 import re
-import yaml
-from neomodel import StructuredNode
+
 import json
-import logging
 from collections import OrderedDict
 from abc import ABC, abstractmethod
-from dgi.models import SQLTable, SQLColumn
 from typing import Dict
+import yaml
+from neomodel import db
+from neomodel import StructuredNode
+from dgi.utils.logging import Log
+from dgi.utils.progress_bar_factory import ProgressBarFactory
 from dgi.tx2graph.utils import sqlexp
-from tqdm import tqdm
-
 
-class AbstactTransactionLoader(ABC):
-    def __init__(self) -> None:
-        super().__init__()
 
+class AbstractTransactionLoader(ABC):
+    """ABC for tx2graph
+    """
     @staticmethod
     def _consume_and_process_label(label: str) -> Dict:
-        """Format lable into a proper JSON string
+        """Format label into a proper JSON string
 
         Args:
-        label (str): The lable as an unformatted string
+        label (str): The label as an unformatted string
         """
-        # -- Strip newline --
-        label = re.sub("\n", "", label)
-        # -- format 'entry' key --
-        label = re.sub("entry", '"entry"', label)
-        # -- format 'action' key --
-        label = re.sub("action", '"action"', label)
-        # -- format 'methods' key --
-        label = re.sub("methods", '"methods"', label)
-        # -- format 'http-param' key --
-        label = re.sub("http-param", '"http-param"', label)
-        label = re.sub("\[", '["', label)  # noqa:  W605
-        label = re.sub("\]", '"]', label)  # noqa:  W605
+
+        # -- DiVA's JSON is malformed. Here, we fix those malformations --
+        label = re.sub(r"\n", "", label)
+        label = re.sub(r" ", "", label)
+        label = re.sub(r"{", '{"', label)
+        label = re.sub(r":", '":', label)
+        label = re.sub(r",", ',"', label)
+        label = re.sub(r"\[", '["', label)
+        label = re.sub(r"\]", '"]', label)
 
         # -- convert to json --
         label = json.loads(label)
@@ -57,55 +58,64 @@ def _consume_and_process_label(label: str) -> Dict:
         return label
 
     @staticmethod
-    def _clear_all_nodes():
+    def _clear_all_nodes(force_clear_all: bool):
         """Delete all nodes"""
-        for node in SQLTable.nodes.all():
-            node.delete()
-
-        for node in SQLColumn.nodes.all():
-            node.delete()
+        Log.warn("The CLI argument clear is turned ON. Deleting pre-existing nodes.")
+        db.cypher_query("MATCH (n:SQLTable)-[r]-(m) DELETE r")
+        db.cypher_query("MATCH (n)-[r]-(m:SQLTable) DELETE r")
+        db.cypher_query("MATCH (n:SQLTable) DELETE n")
+        db.cypher_query("MATCH (n:SQLColumn)-[r]-(m) DELETE r")
+        db.cypher_query("MATCH (n)-[r]-(m:SQLColumn) DELETE r")
+        db.cypher_query("MATCH (n:SQLColumn) DELETE n")
+        if force_clear_all:
+            Log.warn("Force clear has been turned ON. ALL nodes will be deleted.")
+            db.cypher_query("MATCH (n)-[r]-(m) DELETE r")
+            db.cypher_query("MATCH (n) DELETE n")
 
     def crud0(self, ast, write=False):
+        """Second stage CRUD"""
         if isinstance(ast, list):
             res = [set(), set()]
             for child in ast[1:]:
-                rs, ws = self.crud0(child, ast[0] != "select")
-                res[0] |= rs
-                res[1] |= ws
+                read_set, write_set = self.crud0(child, ast[0] != "select")
+                res[0] |= read_set
+                res[1] |= write_set
             return res
-        elif isinstance(ast, dict) and ":from" in ast:
-            ts = [
-                list(t.values())[0] if isinstance(t, dict) else t
-                for t in ast[":from"]
-                if not isinstance(t, tuple)
+
+        if isinstance(ast, dict) and ":from" in ast:
+            txn_set = [
+                list(txn.values())[0] if isinstance(txn, dict) else txn
+                for txn in ast[":from"]
+                if not isinstance(txn, tuple)
             ]
             res = set()
-            for t in ts:
-                if isinstance(t, list):
-                    res |= self.crud0(t, False)[0]
+            for txn in txn_set:
+                if isinstance(txn, list):
+                    res |= self.crud0(txn, False)[0]
                 else:
-                    res.add(t)
+                    res.add(txn)
             return [set(), res] if write else [res, set()]
-        else:
-            return [set(), set()]
+
+        return [set(), set()]
 
     def crud(self, sql):
-        r = sqlexp(sql.lower())
-        if r:
-            return self.crud0(r[1])
-        else:
-            return [set(), set()]
-
-    def analyze(self, txs):
-        for tx in txs:
+        """First stage CRUD"""
+        resp = sqlexp(sql.lower())  # pylint: disable=not-callable
+        if resp:
+            return self.crud0(resp[1])
+        return [set(), set()]
+
+    def analyze(self, txn_set):
+        """Analyze the transaction set"""
+        for txn in txn_set:
             stack = []
-            if tx["transaction"] and tx["transaction"][0]["sql"] != "BEGIN":
-                tx["transaction"] = [{"sql": "BEGIN"}] + tx["transaction"]
-            for op in tx["transaction"]:
-                if op["sql"] == "BEGIN":
+            if txn["transaction"] and txn["transaction"][0]["sql"] != "BEGIN":
+                txn["transaction"] = [{"sql": "BEGIN"}] + txn["transaction"]
+            for operand in txn["transaction"]:
+                if operand["sql"] == "BEGIN":
                     stack.append([set(), set()])
-                    op["rwset"] = stack[-1]
-                elif op["sql"] in ("COMMIT", "ROLLBACK"):
+                    operand["rwset"] = stack[-1]
+                elif operand["sql"] in ("COMMIT", "ROLLBACK"):
                     if len(stack) > 1:
                         stack[-2][0] |= stack[-1][0]
                         stack[-2][1] |= stack[-1][1]
@@ -113,10 +123,10 @@ def analyze(self, txs):
                     stack[-1][1] = set(stack[-1][1])
                     stack.pop()
                 else:
-                    rs, ws = self.crud(op["sql"])
-                    stack[-1][0] |= rs
-                    stack[-1][1] |= ws
-        return txs
+                    read_set, write_set = self.crud(operand["sql"])
+                    stack[-1][0] |= read_set
+                    stack[-1][1] |= write_set
+        return txn_set
 
     @abstractmethod
     def find_or_create_program_node(self, method_signature: str) -> StructuredNode:
@@ -125,19 +135,17 @@ def find_or_create_program_node(self, method_signature: str) -> StructuredNode:
         Args:
             method_signature (_type_): The full method method signature
         """
-        pass
 
     @abstractmethod
-    def find_or_create_SQL_table_node(self, table_name: str) -> StructuredNode:
+    def find_or_create_sql_table_node(self, table_name: str) -> StructuredNode:
         """Create an nodes pertaining to a SQL Table.
 
         Args:
             table_name (str): The name of the table
         """
-        pass
 
     @abstractmethod
-    def populate_transaction_read(label: dict, txid: int, table: str) -> None:
+    def populate_transaction_read(self, method_signature, txid, table, action, the_sql_query) -> None:  # noqa: R0913
         """Add transaction read edges to the database
 
         Args:
@@ -146,10 +154,9 @@ def populate_transaction_read(label: dict, txid: int, table: str) -> None:
             txid (int):   This is the ID assigned to the transaction.
             table (str):  The is the name of the table.
         """
-        pass
 
     @abstractmethod
-    def populate_transaction_write(label: dict, txid: int, table: str) -> None:
+    def populate_transaction_write(self, method_signature, txid, table, action, the_sql_query) -> None:
         """Add transaction write edges to the database
 
         Args:
@@ -158,46 +165,91 @@ def populate_transaction_write(label: dict, txid: int, table: str) -> None:
             txid (int):   This is the ID assigned to the transaction.
             table (str):  The is the name of the table.
         """
-        pass
+
+    # pylint: disable=too-many-arguments
+    @abstractmethod
+    def populate_transaction(
+        self,
+        label: dict,
+        txid: int,
+        read: str,
+        write: str,
+        transaction: list,
+        action: str,
+    ):
+        """Add transaction write edges to the database
+
+        Args:
+            label (dict):        This is a dictionary of the attribute information for the edge. It contains information
+                                 such as the entrypoint class, method, etc.
+            txid (int):          This is the ID assigned to the transaction.
+            read (str):          The name of table that has read operations performed on it.
+            write (str):         The name of the table that has the write operations performed on it.
+            transactions (str):  A list of all the transactions.
+            action (str):        The action that initiated the transaction
+        """
+
+    @abstractmethod
+    def populate_transaction_callgraph(
+        self, callstack: dict, tx_id: int, entrypoint: str
+    ) -> None:
+        """Add transaction write edges to the database
+
+        Args:
+            callstack (dict): The callstack from the entrypoint to the transaction.
+            tx_id (int)      : This is the ID assigned to the transaction.
+            entrypoint (str): The entrypoint that initiated this transaction.
+        """
 
     def tx2neo4j(self, transactions, label):
+        """Load the graphDB with transaction data"""
+
         # If there are no transactions to process, nothing to do here.
-        if not transactions:
+        if len(transactions) == 0:
             return
 
         label = self._consume_and_process_label(label)
+        entrypoint = label["entry"]["methods"][0]
+        action = label.get("action")
+        if action is not None:
+            action = action[tuple(label["action"].keys())[0]][0]
 
         for transaction_dict in transactions:
             txid = transaction_dict["txid"]
             read, write = transaction_dict["transaction"][0]["rwset"]
-            for t in read:
-                self.populate_transaction_read(label, txid, t)
-            for t in write:
-                self.populate_transaction_write(label, txid, t)
+            for each_transaction in transaction_dict["transaction"][
+                1:-1
+            ]:  # [0] -> BEGIN, [-1] -> COMMIT
+                self.populate_transaction_callgraph(
+                    each_transaction["stacktrace"], txid, entrypoint
+                )
+                self.populate_transaction(
+                    label, txid, read, write, each_transaction, action
+                )
+
+    def load_transactions(self, input_file, clear, force_clear=False):
+        """Load transactions data"""
 
-    def load_transactions(self, input, clear):
-
-        # ----------------------
-        # Load transactions data
-        # ----------------------
         yaml.add_representer(
             OrderedDict,
             lambda dumper, data: dumper.represent_mapping(
                 "tag:yaml.org,2002:map", list(data.items())
             ),
         )
-        data = json.load(open(input), object_pairs_hook=OrderedDict)
+        # pylint: disable=consider-using-with,unspecified-encoding
+        data = json.load(open(input_file), object_pairs_hook=OrderedDict)
 
         # --------------------------
         # Remove all existing nodes?
         # --------------------------
         if clear:
-            logging.info("Clear flag detected... Deleting pre-existing SQLTable nodes.")
-            self._clear_all_nodes()
-
-        logging.info("{}: Populating transactions".format(type(self).__name__))
-        for c, entry in tqdm(enumerate(data), total=len(data)):
-            txs = self.analyze(entry["transactions"])
-            del entry["transactions"]
-            label = yaml.dump(entry, default_flow_style=True).strip()
-            self.tx2neo4j(txs, label)
+            self._clear_all_nodes(force_clear)
+
+        Log.info(f"{type(self).__name__}: Populating transactions")
+
+        with ProgressBarFactory.get_progress_bar() as prog_bar:
+            for (_, entry) in prog_bar.track(enumerate(data), total=len(data)):
+                txn_set = self.analyze(entry["transactions"])
+                del entry["transactions"]
+                label = yaml.dump(entry, default_flow_style=True).strip()
+                self.tx2neo4j(txn_set, label)
diff --git a/dgi/tx2graph/class_transaction_loader.py b/dgi/tx2graph/class_transaction_loader.py
index 0934dae..182c0b4 100644
--- a/dgi/tx2graph/class_transaction_loader.py
+++ b/dgi/tx2graph/class_transaction_loader.py
@@ -14,20 +14,23 @@
 # limitations under the License.
 ################################################################################
 
+"""
+Class Transaction Loader Module
+"""
+
+import re
 from neomodel import DoesNotExist
 
-from dgi.tx2graph.abstract_transaction_loader import AbstactTransactionLoader
+from dgi.tx2graph.abstract_transaction_loader import AbstractTransactionLoader
 
 # Import our modules
 from dgi.models import ClassNode, SQLTable
 
 
-class ClassTransactionLoader(AbstactTransactionLoader):
-    def __init__(self) -> None:
-        super().__init__()
-
+class ClassTransactionLoader(AbstractTransactionLoader):
+    """Transaction edges between classes and DBTables
+    """
     def find_or_create_program_node(self, method_signature):
-        # method_name = method_signature.split(".")[-1]
         class_short_name = method_signature.split(".")[-2]
         class_name = ".".join(method_signature.split(".")[:-1])
         try:
@@ -38,7 +41,7 @@ def find_or_create_program_node(self, method_signature):
             ).save()
         return node
 
-    def find_or_create_SQL_table_node(self, table_name):
+    def find_or_create_sql_table_node(self, table_name):
         try:
             node = SQLTable.nodes.get(name=table_name)
         except DoesNotExist:
@@ -46,42 +49,99 @@ def find_or_create_SQL_table_node(self, table_name):
 
         return node
 
-    def populate_transaction_read(self, label, txid, table):
-        entry_method_signature = label["entry"]["methods"][0]
-        class_node = self.find_or_create_program_node(entry_method_signature)
-        table_node = self.find_or_create_SQL_table_node(table)
+    def populate_transaction_read(
+        self, method_signature, txid, table, action, the_sql_query
+    ) -> None:
+        class_node = self.find_or_create_program_node(method_signature)
+        table_node = self.find_or_create_sql_table_node(table)
         rel = class_node.transaction_read.relationship(table_node)
         if not rel:
-            action = (
-                "null"
-                if label.get("http-param") is None
-                else label.get("http-param").get("action")[0]
-            )
             class_node.transaction_read.connect(
                 table_node,
                 {
                     "txid": txid,
-                    "tx_meth": entry_method_signature.split(".")[-1],
+                    "tx_meth": method_signature.split(".")[-1],
                     "action": action,
+                    "sql_query": the_sql_query,
                 },
             )
 
-    def populate_transaction_write(self, label, txid, table):
-        entry_method_signature = label["entry"]["methods"][0]
-        class_node = self.find_or_create_program_node(entry_method_signature)
-        table_node = self.find_or_create_SQL_table_node(table)
+    def populate_transaction_write(
+        self, method_signature, txid, table, action, the_sql_query
+    ):
+        class_node = self.find_or_create_program_node(method_signature)
+        table_node = self.find_or_create_sql_table_node(table)
         rel = class_node.transaction_write.relationship(table_node)
         if not rel:
-            action = (
-                "null"
-                if label.get("http-param") is None
-                else label.get("http-param").get("action")[0]
-            )
             class_node.transaction_write.connect(
                 table_node,
                 {
                     "txid": txid,
-                    "tx_meth": entry_method_signature.split(".")[-1],
+                    "tx_meth": method_signature.split(".")[-1],
                     "action": action,
+                    "sql_query": the_sql_query,
                 },
             )
+
+    def populate_transaction_callgraph(
+        self, callstack: dict, tx_id: int, entrypoint: str
+    ) -> None:
+        """Add transaction write edges to the database
+
+        Args:
+            label (dict): This is a dictionary of the attribute information for the edge. It contains information such
+                          as the entrypoint class, method, etc.
+            txid (int):   This is the ID assigned to the transaction.
+            table (str):  The is the name of the table.
+        """
+        return
+
+    def populate_transaction(
+        self,
+        label: dict,
+        txid: int,
+        read: str,
+        write: str,
+        transaction: list,
+        action: str,
+    ):
+        """Add transaction write edges to the database
+
+        Args:
+            label (dict):        This is a dictionary of the attribute information for the edge. It contains information
+                                 such as the entrypoint class, method, etc.
+            txid (int):          This is the ID assigned to the transaction.
+            read (str):          The name of table that has read operations performed on it.
+            write (str):         The name of the table that has the write operations performed on it.
+            transaction (str):   The transaction under processing.
+            action (str):        The action that initiated the transaction
+        """
+        method_that_access_table = transaction["stacktrace"][-1]
+        class_name_next = re.sub(
+            "/", ".", method_that_access_table["method"].split(", ")[1][1:]
+        )
+        method_name_next = (
+            method_that_access_table["method"].split(", ")[2].split("(")[0]
+        )
+        method_signature = ".".join([class_name_next, method_name_next])
+
+        the_sql_query = transaction["sql"]
+
+        for tx_read in read:
+            if tx_read.casefold() in the_sql_query.casefold():
+                self.populate_transaction_read(
+                    method_signature,
+                    txid,
+                    tx_read.casefold(),
+                    action,
+                    the_sql_query.casefold(),
+                )
+        for tx_write in write:
+            if tx_write.casefold() in the_sql_query.casefold():
+                self.populate_transaction_write(
+                    method_signature,
+                    txid,
+                    tx_write.casefold(),
+                    action,
+                    the_sql_query.casefold(),
+                )
diff --git a/dgi/tx2graph/method_transaction_loader.py b/dgi/tx2graph/method_transaction_loader.py
index 9cc668d..10cab03 100644
--- a/dgi/tx2graph/method_transaction_loader.py
+++ b/dgi/tx2graph/method_transaction_loader.py
@@ -14,18 +14,24 @@
 # limitations under the License.
 ################################################################################
 
+"""
+Method Transaction Loader Module
+"""
+
+import re
+
 from neomodel import DoesNotExist
 
 # Import our modules
 from dgi.models import SQLTable, MethodNode
-from dgi.tx2graph.abstract_transaction_loader import AbstactTransactionLoader
+from dgi.tx2graph.abstract_transaction_loader import AbstractTransactionLoader
 
 
-class MethodTransactionLoader(AbstactTransactionLoader):
-    def __init__(self) -> None:
-        super().__init__()
+class MethodTransactionLoader(AbstractTransactionLoader):
+    """CRUD operation at a method level.
+    """
 
-    def find_or_create_program_node(self, method_signature):
+    def find_or_create_program_node(self, method_signature: str, is_entrypoint=False) -> MethodNode:
         method_name = method_signature.split(".")[-1]
         class_short_name = method_signature.split(".")[-2]
         class_name = ".".join(method_signature.split(".")[:-1])
@@ -39,11 +45,12 @@ def find_or_create_program_node(self, method_signature):
                 node_class_name=class_short_name,
                 node_name=method_name,
                 node_short_name=class_short_name,
+                node_is_tx_entry=is_entrypoint,
             ).save()
 
         return node
 
-    def find_or_create_SQL_table_node(self, table_name):
+    def find_or_create_sql_table_node(self, table_name) -> SQLTable:
         try:
             node = SQLTable.nodes.get(name=table_name)
         except DoesNotExist:
@@ -51,42 +58,119 @@ def find_or_create_SQL_table_node(self, table_name):
 
         return node
 
-    def populate_transaction_read(self, label, txid, table):
-        entry_method_signature = label["entry"]["methods"][0]
-        method_node = self.find_or_create_program_node(entry_method_signature)
-        table_node = self.find_or_create_SQL_table_node(table)
+    def populate_transaction_read(
+        self, method_signature, txid, table, action, the_sql_query
+    ) -> None:
+        method_node = self.find_or_create_program_node(method_signature)
+        table_node = self.find_or_create_sql_table_node(table)
         rel = method_node.transaction_read.relationship(table_node)
         if not rel:
-            action = (
-                "null"
-                if label.get("http-param") is None
-                else label.get("http-param").get("action")[0]
-            )
             method_node.transaction_read.connect(
                 table_node,
                 {
                     "txid": txid,
-                    "tx_meth": entry_method_signature.split(".")[-1],
+                    "tx_meth": method_signature.split(".")[-1],
                     "action": action,
+                    "sql_query": the_sql_query,
                 },
             )
 
-    def populate_transaction_write(self, label, txid, table):
-        entry_method_signature = label["entry"]["methods"][0]
-        method_node = self.find_or_create_program_node(entry_method_signature)
-        table_node = self.find_or_create_SQL_table_node(table)
+    def populate_transaction_write(
+        self, method_signature, txid, table, action, the_sql_query
+    ):
+        method_node = self.find_or_create_program_node(method_signature)
+        table_node = self.find_or_create_sql_table_node(table)
         rel = method_node.transaction_write.relationship(table_node)
         if not rel:
-            action = (
-                "null"
-                if label.get("http-param") is None
-                else label.get("http-param").get("action")[0]
-            )
             method_node.transaction_write.connect(
                 table_node,
                 {
                     "txid": txid,
-                    "tx_meth": entry_method_signature.split(".")[-1],
+                    "tx_meth": method_signature.split(".")[-1],
                     "action": action,
+                    "sql_query": the_sql_query,
                 },
             )
+
+    def populate_transaction_callgraph(
+            self, callstack: dict, tx_id: int, entrypoint: str) -> None:
+        """Add transaction write edges to the database
+
+        Args:
+            callstack (dict): The callstack from the entrypoint to the transaction.
+            tx_id (int)      : This is the ID assigned to the transaction.
+            entrypoint (str): The entrypoint that initiated this transaction.
+        """
+        # Create a method node for this entrypoint
+        self.find_or_create_program_node(entrypoint, is_entrypoint=True)
+
+        # Iterate over all the calls in the callstack and build the callgraph.
+        for prev_call, next_call in zip(callstack[:-1], callstack[1:]):
+            # We strip the class signature (which is in the JNI type signature format) to use a dot notation
+            # E.g., Lcom/abc/class --> com.abc.class.
+            class_name_prev = re.sub("/", ".", prev_call["method"].split(", ")[1][1:])
+            # Likewise, we process the method signature as well.
+            method_name_prev = prev_call["method"].split(", ")[2].split("(")[0]
+            method_signature_prev = ".".join([class_name_prev, method_name_prev])
+            prev_node = self.find_or_create_program_node(method_signature_prev)
+
+            class_name_next = re.sub("/", ".", next_call["method"].split(", ")[1][1:])
+            method_name_next = next_call["method"].split(", ")[2].split("(")[0]
+            method_signature_next = ".".join([class_name_next, method_name_next])
+            next_node = self.find_or_create_program_node(method_signature_next)
+
+            rel = prev_node.transaction_method_call.relationship(next_node)
+            if not rel:
+                prev_node.transaction_method_call.connect(
+                    next_node, {"txid": tx_id, "service_entry": entrypoint}
+                )
+
+    def populate_transaction(
+        self,
+        label: dict,
+        txid: int,
+        read: str,
+        write: str,
+        transaction: list,
+        action: str,
+    ):
+        """Add transaction write edges to the database
+
+        Args:
+            label (dict):        This is a dictionary of the attribute information for the edge. It contains information
+                                 such as the entrypoint class, method, etc.
+            txid (int):          This is the ID assigned to the transaction.
+            read (str):          The name of table that has read operations performed on it.
+            write (str):         The name of the table that has the write operations performed on it.
+            transaction (str):   The transaction under processing.
+            action (str):        The action that initiated the transaction
+        """
+        method_that_access_table = transaction["stacktrace"][-1]
+        class_name_next = re.sub(
+            "/", ".", method_that_access_table["method"].split(", ")[1][1:]
+        )
+        method_name_next = (
+            method_that_access_table["method"].split(", ")[2].split("(")[0]
+        )
+        method_signature = ".".join([class_name_next, method_name_next])
+
+        the_sql_query = transaction["sql"]
+
+        for tx_read in read:
+            if tx_read.casefold() in the_sql_query.casefold():
+                self.populate_transaction_read(
+                    method_signature,
+                    txid,
+                    tx_read.casefold(),
+                    action,
+                    the_sql_query.casefold(),
+                )
+        for tx_write in write:
+            if tx_write.casefold() in the_sql_query.casefold():
+                self.populate_transaction_write(
+                    method_signature,
+                    txid,
+                    tx_write.casefold(),
+                    action,
+                    the_sql_query.casefold(),
+                )
diff --git a/dgi/tx2graph/utils/__init__.py b/dgi/tx2graph/utils/__init__.py
index 8bec099..1592612 100644
--- a/dgi/tx2graph/utils/__init__.py
+++ b/dgi/tx2graph/utils/__init__.py
@@ -1,3 +1,6 @@
-from .sqlparse import sqlexp  # noqa: F401
+"""
+Utilities Package
+"""
+from .sqlparse import sqlexp
 
-__all__ = ('sqlexp')
+__all__ = ['sqlexp']
diff --git a/dgi/tx2graph/utils/peg.py b/dgi/tx2graph/utils/peg.py
index 9d5dc43..f962597 100644
--- a/dgi/tx2graph/utils/peg.py
+++ b/dgi/tx2graph/utils/peg.py
@@ -1,3 +1,4 @@
+# pylint: disable=invalid-name,missing-function-docstring,not-callable,unnecessary-lambda
 ################################################################################
 # Copyright IBM Corporation 2021, 2022
 #
@@ -14,6 +15,14 @@
 # limitations under the License.
 ################################################################################
 
+"""
+Peg Module
+"""
+
+# This code needs major cleanup! I am disabling the PyLint messages
+# until we can get someone to fix them properly. This code is an example
+# of how critical it is to use a good linter at the start of a project!
+
 
 def pegop(f):
     def g(*args):
@@ -23,10 +32,10 @@ def h(s):
             i = hash(s) % 64
             if memo[i] and memo[i][0] == s:
                 return memo[i][1]
-            else:
-                v = f(s, *args)
-                memo[i] = (s, v)
-                return v
+
+            v = f(s, *args)
+            memo[i] = (s, v)
+            return v
 
         return h
 
@@ -67,8 +76,7 @@ def seq(s, *args):
 def val(s, x):
     if s.startswith(x):
         return s[len(x):], []
-    else:
-        return ()
+    return ()
 
 
 @pegop
@@ -81,8 +89,7 @@ def before(s, *args):
             t = s[s.index(a):]
     if t is None:
         return ()
-    else:
-        return t, []
+    return t, []
 
 
 @pegop
@@ -90,10 +97,9 @@ def match(s, e, r=None):
     a = e(s)
     if a == ():
         return ()
-    elif r is None:
+    if r is None:
         return a[0], a[1] + [s[: len(s) - len(a[0])]]
-    else:
-        return a[0], r(a[1], s[: len(s) - len(a[0])])
+    return a[0], r(a[1], s[: len(s) - len(a[0])])
 
 
 @pegop
@@ -109,21 +115,3 @@ def nil(s):
 def star(e):
     f = choice(seq(e, lambda s: f(s)), nil)
     return f
-
-
-if __name__ == "__main__":
-    peg0 = seq(
-        val("{"),
-        star(seq(before("{", "}"), lambda s: peg0(s))),
-        before("{", "}"),
-        val("}"),
-    )
-    peg1 = star(seq(before("{", "}"), match(peg0)))
-    print(star(val("a"))("aaa"))
-    print(peg0("{aaa}"))
-    print(
-        peg1(
-            "akihiko.plugins.HttpRequestAnalyzer@3af87954={method=[POST, GET]"
-            ", path=/app, action=[login, {home}], passwd=xxx}, b={}"
-        )
-    )
diff --git a/dgi/tx2graph/utils/sqlparse.py b/dgi/tx2graph/utils/sqlparse.py
index 31323ce..b48c54a 100644
--- a/dgi/tx2graph/utils/sqlparse.py
+++ b/dgi/tx2graph/utils/sqlparse.py
@@ -1,3 +1,4 @@
+# pylint: disable=invalid-name,missing-function-docstring,not-callable,no-value-for-parameter,unnecessary-lambda
 ################################################################################
 # Copyright IBM Corporation 2021, 2022
 #
@@ -14,6 +15,14 @@
 # limitations under the License.
 ################################################################################
 
+"""
+SQL Parser Module
+"""
+
+# This code needs major cleanup! I am disabling the PyLint messages
+# until we can get someone to fix them properly. This code is an example
+# of how critical it is to use a good linter at the start of a project!
+
 from .peg import seq, choice, star, nil, pegop, pegcxt
 from .peg import match as match0
 
@@ -115,7 +124,8 @@ def option(v):
     "partition",
 ]
 
-what_only_follow_names = [")", ",", "as", "in", "on", "where", "then", "else", "inner"]
+what_only_follow_names = [")", ",", "as", "in",
+                          "on", "where", "then", "else", "inner"]
 
 operator_tokens = [
     "||",
@@ -129,7 +139,7 @@ def option(v):
 ]
 
 
-def token0(s):  # noqa: C901
+def token0(s):  # noqa: C901 pylint: disable=too-many-branches
     k0 = 0
     while k0 < len(s):
         if not s[k0].isspace():
@@ -152,7 +162,7 @@ def token0(s):  # noqa: C901
         while k < len(s):
             if s[k] == "." and p:
                 break
-            elif s[k] == ".":
+            if s[k] == ".":
                 p = True
             elif not s[k].isdigit():
                 break
@@ -166,6 +176,7 @@ def token0(s):  # noqa: C901
     return (s[k:], [s[k0:k]]) if k > k0 else ()
 
 
+# pylint: disable=consider-using-f-string
 @pegop
 def token1(s):
     r = token0(s)
@@ -232,16 +243,14 @@ def until(s, *ws):
 def match(e, r=None):
     if r is None:
         return match0(e, lambda s, v: s + [v.strip()])
-    else:
-        return match0(e, lambda s, v: r(s, v.strip()))
+    return match0(e, lambda s, v: r(s, v.strip()))
 
 
 def split(n):
     if "." not in n:
         return ("", n)
-    else:
-        p = n.rindex(".")
-        return (n[:p], n[p + 1:])
+    p = n.rindex(".")
+    return (n[:p], n[p + 1:])
 
 
 qname = seq(name, star(seq(op("."), name)))
@@ -251,29 +260,35 @@ def split(n):
 )
 
 # skip until one of strs occurs, ignoring anything inside parenthesis
-until_ignoring_paren = lambda *strs: seq(   # noqa: E731
-    star(seq(until("(", ")", *strs), paren)), until("(", ")", *strs), until(*strs)
+until_ignoring_paren = lambda *strs: seq(   # noqa: E0012,E731
+    star(seq(until("(", ")", *strs), paren)  # noqa: E0012,E731
+         ), until("(", ")", *strs), until(*strs)
 )
 
-comma_sep = lambda e: seq(e, star(seq(op(","), e)))     # noqa: E731
-plus = lambda e: seq(e, star(e))    # noqa: E731
+
+def comma_sep(e): return seq(e, star(seq(op(","), e)))     # noqa: E0012,E731 pylint: disable=unnecessary-lambda-assignment
+def plus(e): return seq(e, star(e))    # noqa: E0012,E731 pylint: disable=unnecessary-lambda-assignment
 
 # [{ s[-1][None] : s[0] }] if len(s) == 2 and seq(qname, option(op('as')), name)(v) == ('', []) else
 
-ascxt = lambda e: match(    # noqa: E731
-    seq(e, option(seq(option(op("as")), match(name, lambda s, v: [{None: v}])))),
+
+def ascxt(e): return match(    # noqa: E0012,E731 pylint: disable=unnecessary-lambda-assignment
+    seq(e, option(seq(option(op("as")), match(
+        name, lambda s, v: [{None: v}])))),
     lambda s, v: [{s[-1][None]: s[0] if len(s) == 2 else s[:-1]}]
     if s and isinstance(s[-1], dict) and None in s[-1]
     else s,
 )
 
+
 func = choice(qname, variable, op("left"), op("right"), op("concat"))
 
 callargs = choice(
     comma_sep(
         seq(
             option(choice(op("distinct"), op("all"))),
-            choice(lambda s: colexp(s), match(op("*"), lambda s, v: s + [(v,)])),
+            choice(lambda s: colexp(s), match(
+                op("*"), lambda s, v: s + [(v,)])),
         )
     ),
     nil,
@@ -302,7 +317,8 @@ def split(n):
     op("case"),
     option(lambda s: colexp(s)),
     plus(
-        seq(op("when"), until_ignoring_paren("then"), op("then"), lambda s: colexp(s))
+        seq(op("when"), until_ignoring_paren(
+            "then"), op("then"), lambda s: colexp(s))
     ),
     option(seq(op("else"), lambda s: colexp(s))),
     op("end"),
@@ -349,7 +365,8 @@ def split(n):
 colsexp = choice(comma_sep(colexp1), match(op("*"), lambda s, v: s + [(v,)]))
 
 jointype = choice(
-    seq(choice(op("left"), op("right"), op("full")), option(op("outer")), op("join")),
+    seq(choice(op("left"), op("right"), op("full")),
+        option(op("outer")), op("join")),
     seq(op("inner"), op("join")),
 )
 
@@ -361,7 +378,9 @@ def split(n):
     seq(op("("), lambda s: tblsexp(s), op(")")),
 )
 
-frmcxt = lambda e: match(e, lambda s, _: [{":from": s}])    # noqa: E731
+
+def frmcxt(e): return match(e, lambda s, _: [{":from": s}])  # noqa: E0012,E731 pylint: disable=unnecessary-lambda-assignment
+
 
 tblsexp = frmcxt(
     seq(
@@ -385,7 +404,8 @@ def unioncxt(e, fix):
             seq(
                 choice(seq(op("union"), option(op("all"))), op("except")),
                 match(
-                    choice(e, seq(op("("), choice(e, fix), op(")"))), lambda s, v: [s]
+                    choice(e, seq(op("("), choice(e, fix), op(")"))
+                           ), lambda s, v: [s]
                 ),
             )
         ),
@@ -395,7 +415,7 @@ def unioncxt(e, fix):
 
 
 @pegcxt
-def withcxt(e, fix):
+def withcxt(e, fix):  # pylint: disable=unused-argument
     return choice(
         seq(
             op("with"),
@@ -442,7 +462,8 @@ def withcxt(e, fix):
                     option(op("not")),
                     choice(
                         seq(op("in"), choice(colexp, lambda s: nestedexp(s))),
-                        seq(op("like"), colexp, option(seq(op("escape"), colexp))),
+                        seq(op("like"), colexp, option(
+                            seq(op("escape"), colexp))),
                         seq(op("between"), colexp, op("and"), colexp),
                     ),
                 ),
@@ -483,13 +504,15 @@ def withcxt(e, fix):
 
 grpexp = seq(op("group"), op("by"), comma_sep(choice(qname, variable)))
 orderexp = seq(
-    op("order"), op("by"), comma_sep(seq(colexp, option(choice(op("asc"), op("desc")))))
+    op("order"), op("by"), comma_sep(
+        seq(colexp, option(choice(op("asc"), op("desc")))))
 )
 
 assignexp = seq(
     choice(
         match(qname, lambda s, v: s + [(v,)]),
-        seq(op("("), comma_sep(match(qname, lambda s, v: s + [(v,)])), op(")")),
+        seq(op("("), comma_sep(
+            match(qname, lambda s, v: s + [(v,)])), op(")")),
     ),
     op("="),
     colexp,
@@ -517,7 +540,8 @@ def withcxt(e, fix):
             op("for"),
             op("update"),
             choice(
-                op("nowait"), seq(op("wait"), literal), seq(op("with"), op("rs")), nil
+                op("nowait"), seq(op("wait"), literal), seq(
+                    op("with"), op("rs")), nil
             ),
         )
     ),
@@ -562,7 +586,8 @@ def withcxt(e, fix):
             )
         ),
         op("values"),
-        choice(lambda s: nestedexp(s), seq(op("("), colsexp, op(")")), colsexp),
+        choice(lambda s: nestedexp(s), seq(
+            op("("), colsexp, op(")")), colsexp),
     )
 )
 delexp = withcxt(
@@ -586,7 +611,8 @@ def withcxt(e, fix):
     op(")"),
 )
 
-sqlexp = seq(choice(selexp, updexp, insexp, delexp, valuesexp), option(op(";")))
+sqlexp = seq(choice(selexp, updexp, insexp,
+             delexp, valuesexp), option(op(";")))
 
 if __name__ == "__main__":
     print(selexp("select a from b t,c where (a = b)"))
@@ -609,7 +635,8 @@ def withcxt(e, fix):
     print(
         sqlexp(
             "INSERT INTO holdingejb (HOLDINGID, PURCHASEDATE, PURCHASEPRICE, QUANTITY, "
-            "ACCOUNT_ACCOUNTID, QUOTE_SYMBOL) VALUES (?, ?, ?, ?, ?, ?)".lower()
+            "ACCOUNT_ACCOUNTID, QUOTE_SYMBOL) VALUES (?, ?, ?, ?, ?, ?)".lower(
+            )
         )
     )
     print(sqlexp("DELETE FROM orderejb WHERE (ORDERID = ?".lower()))
diff --git a/dgi/utils/__init__.py b/dgi/utils/__init__.py
new file mode 100644
index 0000000..dcfe180
--- /dev/null
+++ b/dgi/utils/__init__.py
@@ -0,0 +1,6 @@
+"""
+Utility functions
+"""
+from .progress_bar_factory import ProgressBarFactory
+
+__all__ = ["ProgressBarFactory"]
diff --git a/dgi/utils/logging.py b/dgi/utils/logging.py
new file mode 100644
index 0000000..7d8ad9c
--- /dev/null
+++ b/dgi/utils/logging.py
@@ -0,0 +1,52 @@
+################################################################################
+# Copyright IBM Corporation 2021, 2022
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License a#t˚†
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+################################################################################
+
+"""
+Logging functions
+"""
+
+import logging
+from rich.logging import RichHandler
+
+FORMAT = "%(message)s"
+logging.basicConfig(
+    level="INFO", format=FORMAT, datefmt="[%X]", handlers=[RichHandler(show_path=False)]
+)
+
+
+class Log:
+    """Common Logger that uses Rich"""
+
+    log = logging.getLogger("rich")
+
+    @staticmethod
+    def info(msg: str):
+        """Log level INFO"""
+        Log.log.info(msg, extra={"markup": True})
+
+    @staticmethod
+    def warn(msg: str):
+        """Log level WARNING"""
+        Log.log.warning(msg, extra={"markup": True})
+
+    @staticmethod
+    def debug(msg: str):
+        """Log level DEBUG"""
+        Log.log.debug(msg, extra={"markup": True})
+
+    @staticmethod
+    def error(msg: str):
+        """Log level ERROR"""
+        Log.log.error(msg, extra={"markup": True})
diff --git a/dgi/code2graph/utils/parse_config.py b/dgi/utils/parse_config.py
similarity index 88%
rename from dgi/code2graph/utils/parse_config.py
rename to dgi/utils/parse_config.py
index 061bc12..4bdc4f0 100644
--- a/dgi/code2graph/utils/parse_config.py
+++ b/dgi/utils/parse_config.py
@@ -42,8 +42,8 @@
 
 import os
 import re
-import yaml
 from typing import Any
+import yaml
 
 __author__ = "Rahul Krishna"
 __license__ = "Apache 2.0"
@@ -54,6 +54,8 @@
 
 
 class Config:
+    """Holds the configurations
+    """
     def __init__(self, config_file: str = None) -> None:
         self.config_file = config_file
         self._num_attributes = 0
@@ -94,9 +96,9 @@ def set_config(self, key: str, val: Any):
         self._num_attributes += 1
 
         # If the value has environment variables, replace them with the correct values or the defaults
-        reg = re.compile("\${[^\}]*}")  # noqa: W605
+        reg = re.compile("\${[^\}]*}")  # noqa: W601,W605 pylint: disable=W1401
         if isinstance(val, str) and reg.match(val):
-            raw_str = re.sub("[${\ }]", "", val)  # noqa: W605
+            raw_str = re.sub("[${\ }]", "", val)  # noqa: W601,W605 pylint: disable=W1401
             sub_str = raw_str.split("|")
             env_val = sub_str[0]
             default = None
@@ -106,11 +108,9 @@ def set_config(self, key: str, val: Any):
             val = os.getenv(env_val)
 
             if not val:
-                assert (
-                    default
-                ), "Enviroment variable {val} not set, and default value is not set. Please set {val}".format(
-                    val=env_val
-                )
+                assert (default), \
+                    f"Environment variable {val} not set, and default value is not set. Please set {val}".format(
+                        val=env_val)
                 val = default
 
         setattr(self, key, val)
@@ -130,7 +130,7 @@ def load_config(self):
         self: self
             A reference to self
         """
-        with open(self.config_file, "r") as cfg:
+        with open(self.config_file, "r", encoding='utf-8') as cfg:
             yaml_loader = yaml.load(cfg, Loader=yaml.FullLoader)
             for attr_name, attr_val in list(yaml_loader.items()):
                 self.set_config(attr_name, attr_val)
diff --git a/dgi/utils/progress_bar_factory.py b/dgi/utils/progress_bar_factory.py
new file mode 100644
index 0000000..fc8e09b
--- /dev/null
+++ b/dgi/utils/progress_bar_factory.py
@@ -0,0 +1,50 @@
+################################################################################
+# Copyright IBM Corporation 2021, 2022
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+################################################################################
+
+"""
+Fancy Progress Bar
+"""
+from rich.progress import (
+    Progress,
+    BarColumn,
+    TextColumn,
+    SpinnerColumn,
+    TimeElapsedColumn,
+    MofNCompleteColumn,
+    TimeRemainingColumn,
+)
+
+
+# pylint: disable=too-few-public-methods
+class ProgressBarFactory:
+    """Produces a fancy progress bar"""
+
+    @classmethod
+    def get_progress_bar(cls):
+        """Returns the progress bar"""
+        progress_bar = Progress(
+            SpinnerColumn(),
+            TextColumn("•"),
+            TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
+            BarColumn(),
+            TextColumn("• Completed/Total:"),
+            MofNCompleteColumn(),
+            TextColumn("• Elapsed:"),
+            TimeElapsedColumn(),
+            TextColumn("• Remaining:"),
+            TimeRemainingColumn(),
+        )
+        return progress_bar
diff --git a/docs/development.md b/docs/development.md
index d8da568..04954bf 100644
--- a/docs/development.md
+++ b/docs/development.md
@@ -28,4 +28,4 @@ If it does not automatically prompt you to open the project in a container, you
 
 Once the environment is loaded you should be placed at a `bash` prompt in the `/app` folder inside of the development container. This folder is mounted to the current working directory of your repository on your computer. This means that any file you edit while inside of the `/app` folder in the container is actually being edited on your computer. You can then commit your changes to `git` from either inside or outside of the container.
 
-This project uses **Neo4j** which will also be added to your development environment running in a separate container and accessible at `neo4j:7474` from inside the development environment and outside from your web browser at: http://localhost:7474. The default development username is `neo4j` and the default password is `tackle`.
+This project uses **Neo4j** which will also be added to your development environment running in a separate container and accessible at `neo4j:7474` from inside the development environment and outside from your web browser at: http://localhost:7474. The default development username is `neo4j` and the default password is `konveyor`.
diff --git a/docs/getting-started.md b/docs/getting-started.md
index ac9aca4..52cf32c 100644
--- a/docs/getting-started.md
+++ b/docs/getting-started.md
@@ -40,20 +40,17 @@ You will need an instance of Neo4j to store the graphs that `dgi` creates. You c
 
   ```bash
   docker run -d --name neo4j \
-    -p 7474:7474 \
-    -p 7687:7687 \
-    -e NEO4J_AUTH="neo4j/tackle" \
-    -e NEO4J_apoc_export_file_enabled=true \
-    -e NEO4J_apoc_import_file_enabled=true \
-    -e NEO4J_apoc_import_file_use__neo4j__config=true \
-    -e NEO4JLABS_PLUGINS=\[\"apoc\"\] \    
-  neo4j
+      -p 7474:7474 \
+      -p 7687:7687 \
+      -v neo4j:/data \
+      -e NEO4J_AUTH="neo4j:konveyor" \
+      docker.io/neo4j:4.2.0
   ```
 
 You must set an environment variable to let `dgi` know where to find this neo4j container.
 
 ```bash
-export NEO4J_BOLT_URL="bolt://neo4j:tackle@localhost:7687"    
+export NEO4J_BOLT_URL="neo4j://neo4j:konveyor@localhost:7687"
 ```
 
 ### Installation complete
@@ -149,7 +146,7 @@ Code2graph uses the output from a tool called [DOOP](https://bitbucket.org/yanni
 
     ```sh
     $ dgi c2g
-    
+
     Usage: dgi c2g [OPTIONS]
 
       This command loads Code dependencies into the graph
@@ -167,7 +164,7 @@ Code2graph uses the output from a tool called [DOOP](https://bitbucket.org/yanni
     ```sh
     dgi --clear c2g --abstraction=class --input=doop-output
     ```
-    
+
     Note that we could have passed in [class|method|full] as the abstraction. If you decide to run with the `method` or `full` level of abstraction, make sure you use the same abstraction level when running with `tx2g` as well.
 
     After successful completion, you should see:
@@ -269,6 +266,6 @@ In order to explore the neo4j graph, visit [http://localhost:7474/browser/](http
   
 * Under username, enter: `neo4j`
 
-* Under password, enter: `tackle`
+* Under password, enter: `konveyor`
   
 This should bring you to the browser page where you can explore the DGI graph.
diff --git a/docs/neo4j-guides/http-server.py b/docs/neo4j-guides/http-server.py
index 8acc156..5be3823 100755
--- a/docs/neo4j-guides/http-server.py
+++ b/docs/neo4j-guides/http-server.py
@@ -1,23 +1,28 @@
 #! /usr/bin/env python
 import os
+
 try:
     from http.server import HTTPServer, SimpleHTTPRequestHandler
 except ImportError:
     from BaseHTTPServer import HTTPServer
     from SimpleHTTPServer import SimpleHTTPRequestHandler
 
-class CORSRequestHandler(SimpleHTTPRequestHandler):
 
-    def end_headers (self):
-        self.send_header('Access-Control-Allow-Origin', '*')
-        self.send_header('Access-Control-Allow-Methods', 'GET')
-        self.send_header('Access-Control-Allow-Headers', '*')
-        self.send_header('Access-Control-Allow-Headers', 'Pragma,Cache-Control,If-Modified-Since,Content-Type,X-Requested-With,X-stream,X-Ajax-Browser-Auth')
+class CORSRequestHandler(SimpleHTTPRequestHandler):
+    def end_headers(self):
+        self.send_header("Access-Control-Allow-Origin", "*")
+        self.send_header("Access-Control-Allow-Methods", "GET")
+        self.send_header("Access-Control-Allow-Headers", "*")
+        self.send_header(
+            "Access-Control-Allow-Headers",
+            "Pragma,Cache-Control,If-Modified-Since,Content-Type,X-Requested-With,X-stream,X-Ajax-Browser-Auth",
+        )
         SimpleHTTPRequestHandler.end_headers(self)
 
     def do_OPTIONS(self):
         self.send_response(200)
         self.end_headers()
 
-if __name__ == '__main__':
-    HTTPServer(('0.0.0.0', 8001), CORSRequestHandler).serve_forever()
+
+if __name__ == "__main__":
+    HTTPServer(("0.0.0.0", 8001), CORSRequestHandler).serve_forever()
diff --git a/docs/neo4j-guides/walkthrough.html b/docs/neo4j-guides/walkthrough.html
index 04ef1fd..7338bfc 100644
--- a/docs/neo4j-guides/walkthrough.html
+++ b/docs/neo4j-guides/walkthrough.html
@@ -41,7 +41,7 @@ <h3>DGI Demo Guide</h3>
         </figure>
       </div>
     </slide-->
-    
+
 
 
    <h4>DGI Demo Guide</h4>
@@ -120,7 +120,7 @@ <h3>Getting Started</h3>
     <h3>Interesting Information</h3>
     <br/>
     <div>
-      
+  
 
 
    <h4>How many classes are contained in the project?</h4>
@@ -157,7 +157,7 @@ <h4>How many database tables are contained in the project?</h4>
     <h3>Relationships</h3>
     <br/>
     <div>
-      
+  
 
 
    <h4>A summary of relationship types</h4>
@@ -202,7 +202,7 @@ <h4>A summary of relationship types</h4>
     <h3>Immutability</h3>
     <br/>
     <div>
-      
+  
 
 
    <h4>Labels all classes with an immutable state as "Immutable"</h4>
@@ -229,7 +229,7 @@ <h4>Labels all classes with an immutable state as "Immutable"</h4>
     <h3>Coupling</h3>
     <br/>
     <div>
-      
+  
 
 
    <h4>Mark types to investigate</h4>
@@ -396,7 +396,7 @@ <h4>Add type-in-package coupling</h4>
     <h3>Unit Tests: Validate Assertions</h3>
     <br/>
     <div>
-      
+  
 
 
    <h4>Label Assert Methods</h4>
@@ -450,7 +450,7 @@ <h4>Label Assert Methods</h4>
     <h3>(Unit) Test Coverage</h3>
     <br/>
     <div>
-      
+  
 
 
    <h4>Test Coverage</h4>
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..23790f7
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,19 @@
+Click~=8.0.4
+neomodel~=4.0.8
+simple-ddl-parser~=0.25.0
+PyYAML~=6.0
+ipdb~=0.13.9
+pandas~=1.4.1
+tqdm~=4.63.0
+rich~=12.6.0
+py2neo
+minerva-cargo==1.1.0
+rich-click
+nose~=1.3.7
+pinocchio~=0.4.3
+coverage~=6.3.2
+pylint~=2.13
+py2neo~=2021.2.3
+flake8~=4.0.1
+black~=22.3.0
+tox~=3.24.5
\ No newline at end of file
diff --git a/setup.cfg b/setup.cfg
index e377196..1b52a86 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -8,7 +8,4 @@ cover-package=dgi
 # with-xunit=1
 # xunit-file=./unittests.xml
 # cover-xml=1
-# cover-xml-file=./coverage.xml
-
-[coverage:report]
-show_missing = True
+# cover-xml-file=./coverage.xml
\ No newline at end of file
diff --git a/setup.py b/setup.py
index 6cf58b6..c5911f4 100644
--- a/setup.py
+++ b/setup.py
@@ -1,11 +1,11 @@
 from setuptools import setup, find_packages
 
-with open("README.md", "r") as fh:
+with open("README.md", "r", encoding='utf-8') as fh:
     long_description = fh.read()
 
 setup(
-    name='tackle-dgi',
-    version='0.1.0',
+    name="tackle-dgi",
+    version="1.0.0",
     url="https://github.com/konveyor/tackle-data-gravity-insights",
     author="IBM",
     description="Konveyor Tackle Data Gravity Insights",
@@ -14,39 +14,41 @@
     packages=find_packages(exclude=["tests"]),
     include_package_data=True,
     install_requires=[
-        'Click>=8.0.4',
-        'neomodel>=4.0.8',
-        'simple-ddl-parser==0.25.0',
-        'PyYAML>=6.0',
-        'ipdb>=0.13.9',
-        'pandas>=1.4.1',
-        'tqdm>=4.63.0'
+        "Click==8.1.3",
+        "neomodel==4.0.10",
+        "simple-ddl-parser==0.25.0",
+        "PyYAML==6.0",
+        "ipdb==0.13.11",
+        "pandas==1.5.3",
+        "tqdm==4.65.0",
+        "rich==13.3.2",
+        "rich-click==1.6.1",
+        "py2neo==2021.2.3",
+        "minerva-cargo==1.1.0",
     ],
     classifiers=[
-        'Development Status :: 2 - Pre-Alpha',
-        'Environment :: Console',
-        'Intended Audience :: Developers',
-        'License :: OSI Approved :: Apache Software License',
-        'Operating System :: OS Independent',
-        'Programming Language :: Python',
-        'Programming Language :: Python :: 3',
-        'Programming Language :: Python :: 3.9',
+        "Development Status :: 4 - Beta"
+        "Environment :: Console",
+        "Intended Audience :: Developers",
+        "License :: OSI Approved :: Apache Software License",
+        "Operating System :: OS Independent",
+        "Programming Language :: Python",
+        "Programming Language :: Python :: 3",
+        "Programming Language :: Python :: 3.9",
     ],
-    extras_require = {
+    extras_require={
         "dev": [
             "nose==1.3.7",
             "pinocchio==0.4.3",
-            "coverage==6.3.2",
-            "pylint==2.13",
+            "coverage==7.1.0",
+            "pylint==2.16.2",
             "py2neo==2021.2.3",
-            "flake8==4.0.1",
-            "black==22.3.0",
-            "tox==3.24.5"
+            "flake8==6.0.0",
+            "black==23.1.0",
+            "tox==3.24.5",
         ],
     },
     entry_points={
-        'console_scripts': [
-            'dgi = dgi.cli:cli'
-        ],
+        "console_scripts": ["dgi = dgi.cli:cli"],
     },
 )


Test patch:
diff --git a/test.json b/test.json
new file mode 100644
index 0000000..704d4b9
--- /dev/null
+++ b/test.json
@@ -0,0 +1,579 @@
+{
+    "tables": [
+        {
+            "columns": [
+                {
+                    "name": "PURCHASEPRICE",
+                    "type": "DECIMAL",
+                    "size": [
+                        14,
+                        2
+                    ],
+                    "references": null,
+                    "unique": false,
+                    "nullable": true,
+                    "default": null,
+                    "check": null
+                },
+                {
+                    "name": "HOLDINGID",
+                    "type": "INTEGER",
+                    "size": null,
+                    "references": null,
+                    "unique": false,
+                    "nullable": false,
+                    "default": null,
+                    "check": null
+                },
+                {
+                    "name": "QUANTITY",
+                    "type": "DOUBLE",
+                    "size": null,
+                    "references": null,
+                    "unique": false,
+                    "nullable": false,
+                    "default": null,
+                    "check": null
+                },
+                {
+                    "name": "PURCHASEDATE",
+                    "type": "TIMESTAMP",
+                    "size": null,
+                    "references": null,
+                    "unique": false,
+                    "nullable": true,
+                    "default": null,
+                    "check": null
+                },
+                {
+                    "name": "ACCOUNT_ACCOUNTID",
+                    "type": "INTEGER",
+                    "size": null,
+                    "references": null,
+                    "unique": false,
+                    "nullable": true,
+                    "default": null,
+                    "check": null
+                },
+                {
+                    "name": "QUOTE_SYMBOL",
+                    "type": "VARCHAR",
+                    "size": 250,
+                    "references": null,
+                    "unique": false,
+                    "nullable": true,
+                    "default": null,
+                    "check": null
+                }
+            ],
+            "primary_key": [
+                "HOLDINGID"
+            ],
+            "alter": {},
+            "checks": [],
+            "index": [
+                {
+                    "index_name": "HOLDING_ACCOUNTID",
+                    "unique": false,
+                    "detailed_columns": [
+                        {
+                            "name": "ACCOUNT_ACCOUNTID",
+                            "order": "ASC",
+                            "nulls": "LAST"
+                        }
+                    ],
+                    "columns": [
+                        "ACCOUNT_ACCOUNTID"
+                    ]
+                }
+            ],
+            "partitioned_by": [],
+            "tablespace": null,
+            "schema": null,
+            "table_name": "HOLDINGEJB"
+        },
+        {
+            "columns": [
+                {
+                    "name": "ADDRESS",
+                    "type": "VARCHAR",
+                    "size": 250,
+                    "references": null,
+                    "unique": false,
+                    "nullable": true,
+                    "default": null,
+                    "check": null
+                },
+                {
+                    "name": "PASSWD",
+                    "type": "VARCHAR",
+                    "size": 250,
+                    "references": null,
+                    "unique": false,
+                    "nullable": true,
+                    "default": null,
+                    "check": null
+                }
+            ],
+            "primary_key": [],
+            "alter": {},
+            "checks": [],
+            "index": [],
+            "partitioned_by": [],
+            "tablespace": null,
+            "schema": null,
+            "table_name": "ACCOUNTPROFILEEJB"
+        },
+        {
+            "columns": [
+                {
+                    "name": "LOW",
+                    "type": "DECIMAL",
+                    "size": [
+                        14,
+                        2
+                    ],
+                    "references": null,
+                    "unique": false,
+                    "nullable": true,
+                    "default": null,
+                    "check": null
+                },
+                {
+                    "name": "OPEN1",
+                    "type": "DECIMAL",
+                    "size": [
+                        14,
+                        2
+                    ],
+                    "references": null,
+                    "unique": false,
+                    "nullable": true,
+                    "default": null,
+                    "check": null
+                },
+                {
+                    "name": "VOLUME",
+                    "type": "DOUBLE",
+                    "size": null,
+                    "references": null,
+                    "unique": false,
+                    "nullable": false,
+                    "default": null,
+                    "check": null
+                },
+                {
+                    "name": "PRICE",
+                    "type": "DECIMAL",
+                    "size": [
+                        14,
+                        2
+                    ],
+                    "references": null,
+                    "unique": false,
+                    "nullable": true,
+                    "default": null,
+                    "check": null
+                },
+                {
+                    "name": "HIGH",
+                    "type": "DECIMAL",
+                    "size": [
+                        14,
+                        2
+                    ],
+                    "references": null,
+                    "unique": false,
+                    "nullable": true,
+                    "default": null,
+                    "check": null
+                },
+                {
+                    "name": "COMPANYNAME",
+                    "type": "VARCHAR",
+                    "size": 250,
+                    "references": null,
+                    "unique": false,
+                    "nullable": true,
+                    "default": null,
+                    "check": null
+                },
+                {
+                    "name": "SYMBOL",
+                    "type": "VARCHAR",
+                    "size": 250,
+                    "references": null,
+                    "unique": false,
+                    "nullable": false,
+                    "default": null,
+                    "check": null
+                }
+            ],
+            "primary_key": [
+                "SYMBOL"
+            ],
+            "alter": {},
+            "checks": [],
+            "index": [
+                {
+                    "index_name": "QUOTE_SYM",
+                    "unique": true,
+                    "detailed_columns": [
+                        {
+                            "name": "SYMBOL",
+                            "order": "ASC",
+                            "nulls": "LAST"
+                        }
+                    ],
+                    "columns": [
+                        "SYMBOL"
+                    ]
+                }
+            ],
+            "partitioned_by": [],
+            "tablespace": null,
+            "schema": null,
+            "table_name": "QUOTEEJB"
+        },
+        {
+            "columns": [
+                {
+                    "name": "KEYVAL",
+                    "type": "INTEGER",
+                    "size": null,
+                    "references": null,
+                    "unique": false,
+                    "nullable": false,
+                    "default": null,
+                    "check": null
+                },
+                {
+                    "name": "KEYNAME",
+                    "type": "VARCHAR",
+                    "size": 250,
+                    "references": null,
+                    "unique": false,
+                    "nullable": false,
+                    "default": null,
+                    "check": null
+                }
+            ],
+            "primary_key": [
+                "KEYNAME"
+            ],
+            "alter": {},
+            "checks": [],
+            "index": [],
+            "partitioned_by": [],
+            "tablespace": null,
+            "schema": null,
+            "table_name": "KEYGENEJB"
+        },
+        {
+            "columns": [
+                {
+                    "name": "CREATIONDATE",
+                    "type": "TIMESTAMP",
+                    "size": null,
+                    "references": null,
+                    "unique": false,
+                    "nullable": true,
+                    "default": null,
+                    "check": null
+                },
+                {
+                    "name": "OPENBALANCE",
+                    "type": "DECIMAL",
+                    "size": [
+                        14,
+                        2
+                    ],
+                    "references": null,
+                    "unique": false,
+                    "nullable": true,
+                    "default": null,
+                    "check": null
+                },
+                {
+                    "name": "LOGOUTCOUNT",
+                    "type": "INTEGER",
+                    "size": null,
+                    "references": null,
+                    "unique": false,
+                    "nullable": false,
+                    "default": null,
+                    "check": null
+                },
+                {
+                    "name": "BALANCE",
+                    "type": "DECIMAL",
+                    "size": [
+                        14,
+                        2
+                    ],
+                    "references": null,
+                    "unique": false,
+                    "nullable": true,
+                    "default": null,
+                    "check": null
+                },
+                {
+                    "name": "ACCOUNTID",
+                    "type": "INTEGER",
+                    "size": null,
+                    "references": null,
+                    "unique": false,
+                    "nullable": false,
+                    "default": null,
+                    "check": null
+                },
+                {
+                    "name": "LASTLOGIN",
+                    "type": "TIMESTAMP",
+                    "size": null,
+                    "references": null,
+                    "unique": false,
+                    "nullable": true,
+                    "default": null,
+                    "check": null
+                },
+                {
+                    "name": "LOGINCOUNT",
+                    "type": "INTEGER",
+                    "size": null,
+                    "references": null,
+                    "unique": false,
+                    "nullable": false,
+                    "default": null,
+                    "check": null
+                },
+                {
+                    "name": "PROFILE_USERID",
+                    "type": "VARCHAR",
+                    "size": 250,
+                    "references": null,
+                    "unique": false,
+                    "nullable": true,
+                    "default": null,
+                    "check": null
+                }
+            ],
+            "primary_key": [
+                "ACCOUNTID"
+            ],
+            "alter": {},
+            "checks": [],
+            "index": [
+                {
+                    "index_name": "ACCOUNT_USERID",
+                    "unique": false,
+                    "detailed_columns": [
+                        {
+                            "name": "PROFILE_USERID",
+                            "order": "ASC",
+                            "nulls": "LAST"
+                        }
+                    ],
+                    "columns": [
+                        "PROFILE_USERID"
+                    ]
+                }
+            ],
+            "partitioned_by": [],
+            "tablespace": null,
+            "schema": null,
+            "table_name": "ACCOUNTEJB"
+        },
+        {
+            "columns": [
+                {
+                    "name": "ORDERFEE",
+                    "type": "DECIMAL",
+                    "size": [
+                        14,
+                        2
+                    ],
+                    "references": null,
+                    "unique": false,
+                    "nullable": true,
+                    "default": null,
+                    "check": null
+                },
+                {
+                    "name": "COMPLETIONDATE",
+                    "type": "TIMESTAMP",
+                    "size": null,
+                    "references": null,
+                    "unique": false,
+                    "nullable": true,
+                    "default": null,
+                    "check": null
+                },
+                {
+                    "name": "ORDERTYPE",
+                    "type": "VARCHAR",
+                    "size": 250,
+                    "references": null,
+                    "unique": false,
+                    "nullable": true,
+                    "default": null,
+                    "check": null
+                },
+                {
+                    "name": "ORDERSTATUS",
+                    "type": "VARCHAR",
+                    "size": 250,
+                    "references": null,
+                    "unique": false,
+                    "nullable": true,
+                    "default": null,
+                    "check": null
+                },
+                {
+                    "name": "PRICE",
+                    "type": "DECIMAL",
+                    "size": [
+                        14,
+                        2
+                    ],
+                    "references": null,
+                    "unique": false,
+                    "nullable": true,
+                    "default": null,
+                    "check": null
+                },
+                {
+                    "name": "QUANTITY",
+                    "type": "DOUBLE",
+                    "size": null,
+                    "references": null,
+                    "unique": false,
+                    "nullable": false,
+                    "default": null,
+                    "check": null
+                },
+                {
+                    "name": "OPENDATE",
+                    "type": "TIMESTAMP",
+                    "size": null,
+                    "references": null,
+                    "unique": false,
+                    "nullable": true,
+                    "default": null,
+                    "check": null
+                },
+                {
+                    "name": "ORDERID",
+                    "type": "INTEGER",
+                    "size": null,
+                    "references": null,
+                    "unique": false,
+                    "nullable": false,
+                    "default": null,
+                    "check": null
+                },
+                {
+                    "name": "ACCOUNT_ACCOUNTID",
+                    "type": "INTEGER",
+                    "size": null,
+                    "references": null,
+                    "unique": false,
+                    "nullable": true,
+                    "default": null,
+                    "check": null
+                },
+                {
+                    "name": "QUOTE_SYMBOL",
+                    "type": "VARCHAR",
+                    "size": 250,
+                    "references": {
+                        "table": "QUOTEEJB",
+                        "schema": null,
+                        "on_delete": null,
+                        "on_update": null,
+                        "deferrable_initially": null,
+                        "column": "SYMBOL"
+                    },
+                    "unique": false,
+                    "nullable": true,
+                    "default": null,
+                    "check": null
+                },
+                {
+                    "name": "HOLDING_HOLDINGID",
+                    "type": "INTEGER",
+                    "size": null,
+                    "references": null,
+                    "unique": false,
+                    "nullable": true,
+                    "default": null,
+                    "check": null
+                }
+            ],
+            "primary_key": [
+                "ORDERID"
+            ],
+            "alter": {},
+            "checks": [],
+            "index": [
+                {
+                    "index_name": "ORDER_ACCOUNTID",
+                    "unique": false,
+                    "detailed_columns": [
+                        {
+                            "name": "ACCOUNT_ACCOUNTID",
+                            "order": "ASC",
+                            "nulls": "LAST"
+                        }
+                    ],
+                    "columns": [
+                        "ACCOUNT_ACCOUNTID"
+                    ]
+                },
+                {
+                    "index_name": "ORDER_HOLDINGID",
+                    "unique": false,
+                    "detailed_columns": [
+                        {
+                            "name": "HOLDING_HOLDINGID",
+                            "order": "ASC",
+                            "nulls": "LAST"
+                        }
+                    ],
+                    "columns": [
+                        "HOLDING_HOLDINGID"
+                    ]
+                },
+                {
+                    "index_name": "CLOSED_ORDERS",
+                    "unique": false,
+                    "detailed_columns": [
+                        {
+                            "name": "ACCOUNT_ACCOUNTID",
+                            "order": "ASC",
+                            "nulls": "LAST"
+                        },
+                        {
+                            "name": "ORDERSTATUS",
+                            "order": "ASC",
+                            "nulls": "LAST"
+                        }
+                    ],
+                    "columns": [
+                        "ACCOUNT_ACCOUNTID",
+                        "ORDERSTATUS"
+                    ]
+                }
+            ],
+            "partitioned_by": [],
+            "tablespace": null,
+            "schema": null,
+            "table_name": "ORDEREJB"
+        }
+    ],
+    "types": [],
+    "sequences": [],
+    "domains": [],
+    "schemas": [],
+    "ddl_properties": []
+}
\ No newline at end of file
diff --git a/tests/fixtures/test-schema.ddl b/tests/fixtures/test-schema.ddl
index 459933e..3e42f8f 100644
--- a/tests/fixtures/test-schema.ddl
+++ b/tests/fixtures/test-schema.ddl
@@ -28,7 +28,7 @@ CREATE TABLE QUOTEEJB (
 );
 
 CREATE UNIQUE INDEX QUOTE_SYM ON QUOTEEJB(SYMBOL);   
-    
+
 CREATE TABLE KEYGENEJB (
   KEYVAL INTEGER NOT NULL,
   KEYNAME VARCHAR(250) NOT NULL PRIMARY KEY
diff --git a/tests/fixtures/test_app/build.gradle b/tests/fixtures/test_app/build.gradle
index 3b8507f..7676948 100644
--- a/tests/fixtures/test_app/build.gradle
+++ b/tests/fixtures/test_app/build.gradle
@@ -15,5 +15,5 @@ jar {
     manifest {
         attributes "Main-Class": "com.ibm.dgitest.DGIMain"
     }
-        
+    
 }
diff --git a/tests/test_c2g.py b/tests/test_c2g.py
index 51288ce..9141ce2 100644
--- a/tests/test_c2g.py
+++ b/tests/test_c2g.py
@@ -29,7 +29,9 @@
 ######################################################################
 
 
-NEO4J_BOLT_URL = os.getenv("NEO4J_BOLT_URL", "bolt://neo4j:tackle@neo4j:7687")
+NEO4J_BOLT_URL = os.getenv(
+    "NEO4J_BOLT_URL", "neo4j://neo4j:konveyor@neo4j:7687")
+
 
 class TestS2GCLI(unittest.TestCase):
     """Test Cases for c2g command"""
@@ -38,52 +40,80 @@ def setUp(self):
         self.runner = CliRunner()
 
     def tearDown(self) -> None:
-        g = Graph(NEO4J_BOLT_URL)
-        g.delete_all()
+        pass
 
     def test_help(self):
-        """Test help command """
+        """Test help command"""
         result = self.runner.invoke(cli, ["c2g", "--help"])
         self.assertEqual(result.exit_code, 0)
 
     def test_missing_input(self):
-        """Test --input with no filename """
+        """Test --input with no filename"""
         result = self.runner.invoke(cli, ["c2g", "--input"])
         self.assertNotEqual(result.exit_code, 0)
-        assert (
-            "Error: Option '--input' requires an argument." in result.output
-        )
+        self.assertIn("Option '--input' requires an argument.", result.output)
 
     def test_not_found_output(self):
-        """Test --input directory not found """
+        """Test --input directory not found"""
         result = self.runner.invoke(cli, ["c2g", "--input", "foo"])
         self.assertEqual(result.exit_code, 2)
-        assert "Directory 'foo' does not exist." in result.output
+        self.assertIn("Directory 'foo' does not exist.", result.output)
 
     def test_abstraction_level_is_class(self):
         """Test --abstraction set to 'class'"""
         result = self.runner.invoke(
-            cli, ["c2g", "--abstraction=class",  "--input=tests/fixtures/doop_out"])
-        assert "code2graph build complete" in result.output
+            cli,
+            [
+                "--validate",
+                "c2g",
+                "--abstraction=class",
+                "--input=tests/fixtures/doop_out",
+            ],
+        )
+        self.assertIn("abstraction level is class", result.output)
         self.assertEqual(result.exit_code, 0)
 
     def test_abstraction_level_is_method(self):
         """Test --abstraction set to 'method'"""
         result = self.runner.invoke(
-            cli, ["--validate", "c2g", "--abstraction=method",  "--input=tests/fixtures/doop_out"])
-        assert "abstraction level is method" in result.output
+            cli,
+            [
+                "--validate",
+                "c2g",
+                "--abstraction=method",
+                "--input=tests/fixtures/doop_out",
+            ],
+        )
+        self.assertIn("abstraction level is method", result.output)
         self.assertEqual(result.exit_code, 0)
 
     def test_abstraction_level_is_full(self):
         """Test --abstraction set to 'full'"""
         result = self.runner.invoke(
-            cli, ["--validate", "c2g", "--abstraction=full", "--input=tests/fixtures/doop_out"])
-        assert "abstraction level is full" in result.output
+            cli,
+            [
+                "--validate",
+                "c2g",
+                "--abstraction=full",
+                "--input=tests/fixtures/doop_out",
+            ],
+        )
+        self.assertIn("abstraction level is full", result.output)
         self.assertEqual(result.exit_code, 0)
 
     def test_abstraction_level_is_wrong(self):
-        """Test raise exception when --abstraction set to 'gobbledygook'"""
+        """Test raise exception when --abstraction set to 'unknown'"""
         result = self.runner.invoke(
-            cli, ["--validate", "c2g", "--abstraction=gobbledygook", "--input=tests/fixtures/doop_out"])
-        assert "Invalid value for '--abstraction' / '-a': 'gobbledygook' is not one of 'class', 'method', 'full'." in result.output
+            cli,
+            [
+                "--validate",
+                "c2g",
+                "--abstraction=unknown",
+                "--input=tests/fixtures/doop_out",
+            ],
+        )
+        self.assertIn(
+            "Invalid value for '--abstraction' / '-a': 'unknown' is not one of 'class', 'method', 'full'.",
+            result.output
+        )
         self.assertEqual(result.exit_code, 2)
diff --git a/tests/test_s2g.py b/tests/test_s2g.py
index edbf958..697af0d 100644
--- a/tests/test_s2g.py
+++ b/tests/test_s2g.py
@@ -20,6 +20,7 @@
 from cmath import log
 import os
 import logging
+from pathlib import Path
 import unittest
 from py2neo import Graph
 from click.testing import CliRunner
@@ -27,11 +28,14 @@
 
 logging.disable(logging.CRITICAL)
 
-NEO4J_BOLT_URL = os.getenv("NEO4J_BOLT_URL", "bolt://neo4j:tackle@neo4j:7687")
+NEO4J_BOLT_URL = os.getenv(
+    "NEO4J_BOLT_URL", "neo4j://neo4j:konveyor@neo4j:7687")
 
 ######################################################################
 #  S 2 G   C L I   T E S T   C A S E S
 ######################################################################
+
+
 class TestS2GCLI(unittest.TestCase):
     """Test Cases for S2G command"""
 
@@ -46,64 +50,79 @@ def setUp(self):
         self.runner = CliRunner()
 
     def test_help(self):
-        """Test help command """
+        """Test help command"""
         result = self.runner.invoke(cli, ["s2g", "--help"])
         self.assertEqual(result.exit_code, 0)
 
     def test_no_args(self):
-        """ Call with to arguments """
+        """Call with to arguments"""
         result = self.runner.invoke(cli, ["s2g"])
         self.assertNotEqual(result.exit_code, 0)
-        assert (
-            "Error: Missing option '--input' / '-i'." in result.output
-        )
+        self.assertIn("Missing option '--input' / '-i'.", result.output)
 
     def test_missing_input(self):
-        """Test --input with no filename """
+        """Test --input with no filename"""
         result = self.runner.invoke(cli, ["s2g", "--input"])
         self.assertNotEqual(result.exit_code, 0)
-        assert (
-            "Error: Option '--input' requires an argument." in result.output
-        )
+        self.assertIn("Option '--input' requires an argument.", result.output)
 
     def test_not_found_output(self):
-        """Test --input with file not found """
+        """Test --input with file not found"""
         result = self.runner.invoke(cli, ["s2g", "--input", "foo"])
         self.assertEqual(result.exit_code, 2)
-        assert "Path 'foo' does not exist." in result.output
+        self.assertIn("Path 'foo' does not exist.", result.output)
 
     def test_good_output(self):
-        """Test with good output """
-        result = self.runner.invoke(cli, ["--clear", "s2g", "--input", "tests/fixtures/test-schema.ddl"])
+        """Test with good output"""
+        result = self.runner.invoke(
+            cli, ["--clear", "s2g", "--input",
+                  "tests/fixtures/test-schema.ddl"]
+        )
         self.assertEqual(result.exit_code, 0)
 
     def test_validate_only(self):
-        """Test validating the a good file """
-        result = self.runner.invoke(cli, ["--validate", "s2g", "--input", "tests/fixtures/test-schema.ddl"])
+        """Test validating the a good file"""
+        result = self.runner.invoke(
+            cli, ["--validate", "s2g", "--input",
+                  "tests/fixtures/test-schema.ddl"]
+        )
         self.assertEqual(result.exit_code, 0)
-        assert "File [tests/fixtures/test-schema.ddl] validated." in result.output
+        # TODO: Fix this. Does not work with logging, i.e., result.output is an empty string.
+        # assert "File [tests/fixtures/test-schema.ddl] validated." in result.output
 
     def test_output_file(self):
-        """Test writing output file """
-        result = self.runner.invoke(cli, ["--validate","s2g","--input", "tests/fixtures/test-schema.ddl", "--output", "test.json"])
+        """Test writing output file"""
+        result = self.runner.invoke(
+            cli,
+            [
+                "--validate",
+                "s2g",
+                "--input",
+                "tests/fixtures/test-schema.ddl",
+                "--output",
+                "test.json",
+            ],
+        )
         self.assertEqual(result.exit_code, 0)
-        assert "Writing: test.json" in result.output
-        os.remove("test.json")
 
     def test_neo_graph_count(self):
-        """Test the number of nodes were populated """
-        result = self.runner.invoke(cli, ["--clear", "s2g", "--input", "tests/fixtures/test-schema.ddl"])
+        """Test the number of nodes were populated"""
+        result = self.runner.invoke(
+            cli, ["--clear", "s2g", "--input",
+                  "tests/fixtures/test-schema.ddl"]
+        )
         self.assertEqual(result.exit_code, 0)
-
         cypher = "MATCH (n:SQLTable) RETURN COUNT(n)"
         cursor = TestS2GCLI.graph.run(cypher)
         self.assertEqual(cursor.evaluate(), 6)
 
     def test_neo_graph_sqltables(self):
-        """Test the SQLTables were populated """
-        result = self.runner.invoke(cli, ["--clear", "s2g", "--input", "tests/fixtures/test-schema.ddl"])
+        """Test the SQLTables were populated"""
+        result = self.runner.invoke(
+            cli, ["--clear", "s2g", "--input",
+                  "tests/fixtures/test-schema.ddl"]
+        )
         self.assertEqual(result.exit_code, 0)
-
         cypher = "MATCH (n:SQLTable) RETURN n.name as name"
         cursor = TestS2GCLI.graph.run(cypher).data()
         data = [n["name"] for n in cursor]
@@ -115,20 +134,23 @@ def test_neo_graph_sqltables(self):
         self.assertIn("HOLDINGEJB", data)
 
     def test_neo_graph_sqlcolumns(self):
-        """Test the SQLColumns were populated """
-        result = self.runner.invoke(cli, ["--clear", "s2g", "--input", "tests/fixtures/test-schema.ddl"])
+        """Test the SQLColumns were populated"""
+        result = self.runner.invoke(
+            cli, ["--clear", "s2g", "--input",
+                  "tests/fixtures/test-schema.ddl"]
+        )
         self.assertEqual(result.exit_code, 0)
-        cypher1 = "MATCH (:SQLTable{name:\"ACCOUNTPROFILEEJB\"})-[:CONTAINS]->(m:SQLColumn) RETURN m.name as name"
+        cypher1 = 'MATCH (:SQLTable{name:"ACCOUNTPROFILEEJB"})-[:CONTAINS]->(m:SQLColumn) RETURN m.name as name'
         cursor1 = TestS2GCLI.graph.run(cypher1).data()
         data1 = [n["name"] for n in cursor1]
         self.assertIn("PASSWD", data1)
-        cypher2 = "MATCH (:SQLTable{name:\"QUOTEEJB\"})-[:CONTAINS]->(m:SQLColumn) RETURN m.name as name"
+        cypher2 = 'MATCH (:SQLTable{name:"QUOTEEJB"})-[:CONTAINS]->(m:SQLColumn) RETURN m.name as name'
         cursor2 = TestS2GCLI.graph.run(cypher2).data()
         data2 = [n["name"] for n in cursor2]
         self.assertEqual(len(data2), 7)
         self.assertIn("COMPANYNAME", data2)
         self.assertIn("VOLUME", data2)
-        cypher3 = "MATCH (:SQLTable{name:\"ORDEREJB\"})-[:CONTAINS]->(m:SQLColumn) RETURN m.name as name"
+        cypher3 = 'MATCH (:SQLTable{name:"ORDEREJB"})-[:CONTAINS]->(m:SQLColumn) RETURN m.name as name'
         cursor3 = TestS2GCLI.graph.run(cypher3).data()
         data3 = [n["name"] for n in cursor3]
         self.assertIn("ACCOUNT_ACCOUNTID", data3)
@@ -136,19 +158,25 @@ def test_neo_graph_sqlcolumns(self):
         self.assertIn("COMPLETIONDATE", data3)
 
     def test_neo_graph_primarykey(self):
-        """Test if the primary key(s) were populated """
-        result = self.runner.invoke(cli, ["--clear", "s2g", "--input", "tests/fixtures/test-schema.ddl"])
+        """Test if the primary key(s) were populated"""
+        result = self.runner.invoke(
+            cli, ["--clear", "s2g", "--input",
+                  "tests/fixtures/test-schema.ddl"]
+        )
         self.assertEqual(result.exit_code, 0)
-        cypher4 = "MATCH (:SQLTable{name:\"QUOTEEJB\"})-[:CONTAINS]->(m:SQLColumn) RETURN m.is_primary as name"
+        cypher4 = 'MATCH (:SQLTable{name:"QUOTEEJB"})-[:CONTAINS]->(m:SQLColumn) RETURN m.is_primary as name'
         cursor4 = TestS2GCLI.graph.run(cypher4).data()
         data4 = [n["name"] for n in cursor4]
         self.assertEqual(True, data4[0])
 
     def test_neo_graph_foreignkey(self):
-        """Test if the foreign key(s) were populated """
-        result = self.runner.invoke(cli, ["--clear", "s2g", "--input", "tests/fixtures/test-schema.ddl"])
+        """Test if the foreign key(s) were populated"""
+        result = self.runner.invoke(
+            cli, ["--clear", "s2g", "--input",
+                  "tests/fixtures/test-schema.ddl"]
+        )
         self.assertEqual(result.exit_code, 0)
-        cypherForeign = "MATCH (:SQLColumn{name:\"QUOTE_SYMBOL\"})-[:FOREIGN_KEY]->(m:SQLColumn) RETURN m.name as name"
-        cursorForeign = TestS2GCLI.graph.run(cypherForeign).data()
-        dataForeign = [n["name"] for n in cursorForeign]
-        self.assertIn("SYMBOL", dataForeign)
+        cypher_foreign = 'MATCH (:SQLColumn{name:"QUOTE_SYMBOL"})-[:FOREIGN_KEY]->(m:SQLColumn) RETURN m.name as name'
+        cursor_foreign = TestS2GCLI.graph.run(cypher_foreign).data()
+        data_foreign = [n["name"] for n in cursor_foreign]
+        self.assertIn("SYMBOL", data_foreign)
diff --git a/tests/test_tx2g.py b/tests/test_tx2g.py
index fb8d229..e45e650 100644
--- a/tests/test_tx2g.py
+++ b/tests/test_tx2g.py
@@ -21,7 +21,6 @@
 import logging
 import unittest
 from unittest.mock import patch
-from wsgiref import validate
 from click.testing import CliRunner
 from dgi.cli import cli
 from py2neo import Graph
@@ -32,7 +31,8 @@
 ######################################################################
 
 
-NEO4J_BOLT_URL = os.getenv("NEO4J_BOLT_URL", "bolt://neo4j:tackle@neo4j:7687")
+NEO4J_BOLT_URL = os.getenv(
+    "NEO4J_BOLT_URL", "neo4j://neo4j:konveyor@neo4j:7687")
 
 loglevel = logging.CRITICAL
 logging.basicConfig(level=loglevel)
@@ -45,81 +45,126 @@ def setUp(self):
         self.runner = CliRunner()
 
     def tearDown(self) -> None:
-        g = Graph(NEO4J_BOLT_URL)
-        g.delete_all()
-        nodes = len(g.nodes)
-        self.assertEquals(nodes, 0)
+        pass
 
     def test_help(self):
-        """Test help command """
+        """Test help command"""
         result = self.runner.invoke(cli, ["tx2g", "--help"])
         self.assertEqual(result.exit_code, 0)
 
     def test_no_args(self):
-        """ Call with no arguments """
+        """Call with no arguments"""
         result = self.runner.invoke(cli, ["tx2g"])
         self.assertNotEqual(result.exit_code, 0)
-        assert (
-            "Error: Missing option '--input' / '-i'." in result.output
-        )
+        self.assertIn("Missing option '--input' / '-i'.", result.output)
 
     def test_missing_input(self):
-        """Test --input with no filename """
+        """Test --input with no filename"""
         result = self.runner.invoke(cli, ["tx2g", "--input"])
         self.assertNotEqual(result.exit_code, 0)
-        assert (
-            "Error: Option '--input' requires an argument." in result.output
-        )
+        self.assertIn("Option '--input' requires an argument.", result.output)
 
     def test_not_found_input(self):
-        """Test --input with file not found """
+        """Test --input with file not found"""
         result = self.runner.invoke(cli, ["tx2g", "--input", "foo"])
         self.assertEqual(result.exit_code, 2)
-        assert "Error: Invalid value for '--input' / '-i': Path 'foo' does not exist." in result.output
+        self.assertIn(
+            "Invalid value for '--input' / '-i': Path 'foo' does not exist.",
+            result.output
+        )
 
     def test_verbose_mode(self):
-        """Test verbose mode on """
+        """Test verbose mode on"""
         result = self.runner.invoke(
-            cli, ["--validate", "tx2g", "--input", "tests/fixtures/daytrader_transaction.json"])
+            cli,
+            [
+                "--validate",
+                "tx2g",
+                "--input",
+                "tests/fixtures/daytrader_transaction.json",
+            ],
+        )
         self.assertEqual(result.exit_code, 0)
-        assert "Verbose mode: ON" in result.output
+        # TODO: result.output is empty because we use logging instead of console print.
+        # assert "Verbose mode: ON" in result.output
 
     def test_good_output_with_http_param(self):
-        """Test with good output *with* HTTP param """
+        """Test with good output *with* HTTP param"""
         result = self.runner.invoke(
-            cli, ["--clear", "tx2g", "--input", "tests/fixtures/daytrader_transaction.json"])
+            cli,
+            ["--clear", "tx2g", "--input",
+                "tests/fixtures/daytrader_transaction.json"],
+        )
         self.assertEqual(result.exit_code, 0)
 
     def test_good_output_without_http_param(self):
-        """Test with good output *without* HTTP param """
+        """Test with good output *without* HTTP param"""
         result = self.runner.invoke(
-            cli, ["--clear", "tx2g", "--input", "tests/fixtures/trading_app_transactions.json"])
+            cli,
+            [
+                "--clear",
+                "tx2g",
+                "--input",
+                "tests/fixtures/trading_app_transactions.json",
+            ],
+        )
         self.assertEqual(result.exit_code, 0)
 
     def test_abstraction_level_is_class(self):
         """Test --abstraction set to 'class'"""
         result = self.runner.invoke(
-            cli, ["--validate", "tx2g", "--abstraction=class", "--input=tests/fixtures/daytrader_transaction.json"])
-        assert "abstraction level is class" in result.output
+            cli,
+            [
+                "--validate",
+                "tx2g",
+                "--abstraction=class",
+                "--input=tests/fixtures/daytrader_transaction.json",
+            ],
+        )
+        self.assertIn("abstraction level is class", result.output)
         self.assertEqual(result.exit_code, 0)
 
     def test_abstraction_level_is_method(self):
         """Test --abstraction set to 'method'"""
         result = self.runner.invoke(
-            cli, ["--validate", "tx2g", "--abstraction=method", "--input=tests/fixtures/daytrader_transaction.json"])
-        assert "abstraction level is method" in result.output
+            cli,
+            [
+                "--validate",
+                "tx2g",
+                "--abstraction=method",
+                "--input=tests/fixtures/daytrader_transaction.json",
+            ],
+        )
+        self.assertIn("abstraction level is method", result.output)
         self.assertEqual(result.exit_code, 0)
 
     def test_abstraction_level_is_full(self):
         """Test --abstraction set to 'full'"""
         result = self.runner.invoke(
-            cli, ["--validate", "tx2g", "--abstraction=full", "--input=tests/fixtures/daytrader_transaction.json"])
-        assert "abstraction level is full" in result.output
+            cli,
+            [
+                "--validate",
+                "tx2g",
+                "--abstraction=full",
+                "--input=tests/fixtures/daytrader_transaction.json",
+            ],
+        )
+        self.assertIn("abstraction level is full", result.output)
         self.assertEqual(result.exit_code, 0)
 
     def test_abstraction_level_is_wrong(self):
-        """Test raise exception when --abstraction set to 'gobbledygook'"""
+        """Test raise exception when --abstraction set to 'unknown'"""
         result = self.runner.invoke(
-            cli, ["--validate", "tx2g", "--abstraction=gobbledygook", "--input=tests/fixtures/daytrader_transaction.json"])
-        assert "Invalid value for '--abstraction' / '-a': 'gobbledygook' is not one of 'class', 'method', 'full'." in result.output
+            cli,
+            [
+                "--validate",
+                "tx2g",
+                "--abstraction=unknown",
+                "--input=tests/fixtures/daytrader_transaction.json",
+            ],
+        )
+        self.assertIn(
+            "Invalid value for '--abstraction' / '-a': 'unknown' is not one of 'class', 'method', 'full'.",
+            result.output
+        )
         self.assertEqual(result.exit_code, 2)