Repository: callahantiff/PheKnowLator (language: python)

Issue:
Set-up SPARQL Endpoint
### TASK  
**Task Type:** [`PKT DATA DELIVERY`](https://github.com/callahantiff/PheKnowLator/projects/8#card-51636129)

Select and set-up a SPARQL endpoint for exploring KG build data  

#### TODO

- [x] Pick an endpoint. [Here](https://medium.com/wallscope/comparing-linked-data-triplestores-ebfac8c3ad4f) is a Medium article that compares and contrasts existing triplestores. Considering what we care about (Docker compatibility, RDF and Querying speed), I have selected a few and ordered them from best to worst:
  - [GraphDB](https://graphdb.ontotext.com/)   
    - Concurrent queries not free  
  - [BlazeGraph](https://blazegraph.com/)   
    - Having working example via BB
  - [Virtuoso](http://vos.openlinksw.com/dataspace/owiki/wiki/VOS/VOSSparqlProtocol)  
- [ ] Configure with CI/CD  
- [x] Figure out where to host it   
  - Through Google Cloud Run?  

**Questions:**  
- @bill-baumgartner - Which one do you think we should use?
- Two versions needed, 1 for our production build and 1 for those who want to build their own

TODO: Move print statements to logging
### TASK  
**Task Type:** [`CODEBASE`](https://github.com/callahantiff/PheKnowLator/projects/8#card-51636107)

There are a lot of print statements created during the KG build that provide useful information as well track build progress. Most of these should be moved to logging.  

#### TODO  
- [ ] Move all print statements and processing to logging  


Discussion / hints:
**Follow-up**  
- How many concurrent queries can be run from each service on free plan? This matters if we are making this available to the public or just using it for the challenge  
@bill-baumgartner - I successfully brought down the endpoint tonight 😄. It's running again, I restarted the container and it came back. The query I ran is shown below. It went down because I did not include `LIMIT`. I wonder if we should add something to protect from others doing this, or if there is something we can add to help it restart itself in these situations. Just something for us to discuss tomorrow!

```SPARQL
PREFIX obo: <http://purl.obolibrary.org/obo/>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>

SELECT ?s ?p ?o 
WHERE { 
  VALUES ?p {
    obo:RO_0000087
    obo:RO_0002434
    rdfs:subClassOf
  }
  ?s ?p ?o 
} 
```

Note when adding `LIMIT n` the query executes totally fine. This query format is the template RH provided me so that's why I was testing it ot.
Good to know. From grepping our input n-triples file, we would expect the following numbers of responses:
* `subClassOf`: 1,340,072
* `RO_0000087 (has_role)`: 40,500
* `RO_0002434 (interacts_with)`: 4,042,408

So, in total, this query would have eclipsed the 5M triple limit we had originally set. It may be the case that for queries with many results, users will need to request results in batches using `ORDER BY` + `LIMIT` + `OFFSET`.

I agree. I did some experimenting using the SPARQL Proxy settings (i.e. `ENABLE_QUERY_SPLITTING` and `MAX_CHUNK_LIMIT`) in `docker-compose.yml` and have some interesting insight to share in our meeting this afternoon. In a nutshell, I can get it to return all of the results, but then generate a different error when trying to return the results (which when using the `ENABLE_QUERY_SPLITTING` setting returns `JSON`). See below:

```
buffer.js:799
api_1      |     return this.utf8Slice(start, end);
api_1      |                 ^
api_1      | 
api_1      | Error: Cannot create a string longer than 0x1fffffe8 characters
api_1      |     at Buffer.toString (buffer.js:799:17)
api_1      |     at Request.<anonymous> (/app/node_modules/request/request.js:1128:39)
api_1      |     at Request.emit (events.js:315:20)
api_1      |     at IncomingMessage.<anonymous> (/app/node_modules/request/request.js:1076:12)
api_1      |     at Object.onceWrapper (events.js:421:28)
api_1      |     at IncomingMessage.emit (events.js:327:22)
api_1      |     at endReadableNT (internal/streams/readable.js:1327:12)
api_1      |     at processTicksAndRejections (internal/process/task_queues.js:80:21) {
api_1      |   code: 'ERR_STRING_TOO_LONG'
api_1      | }
api_1      | npm ERR! code ELIFECYCLE
api_1      | npm ERR! errno 1
api_1      | npm ERR! sparql-proxy@0.0.0 start: `node --experimental-modules src/server.mjs`
api_1      | npm ERR! Exit status 1
api_1      | npm ERR! 
api_1      | npm ERR! Failed at the sparql-proxy@0.0.0 start script.
api_1      | npm ERR! This is probably not a problem with npm. There is likely additional logging output above.
api_1      | 
api_1      | npm ERR! A complete log of this run can be found in:
api_1      | npm ERR!     /app/.npm/_logs/2021-01-08T18_52_49_474Z-debug.log
```
@bill-baumgartner so we have a record, here are the two queries we ran against the Endpoint via the command line:

**wget Simple:** 
```
wget -qO- "http://35.233.212.30/blazegraph/sparql?query=select * where { ?s ?p ?o } " > filename.xml
```

<br>

**wget Relation Template:**. 
```
wget -qO- "http://35.233.212.30/blazegraph/sparql?query=PREFIX%20obo%3A%20%3Chttp%3A%2F%2Fpurl.obolibrary.org%2Fobo%2F%3E%20PREFIX%20rdfs%3A%20%3Chttp%3A%2F%2Fwww.w3.org%2F2000%2F01%2Frdf-schema%23%3E%20SELECT%20%3Fs%20%3Fp%20%3Fo%20WHERE%20%7B%20VALUES%20%3Fp%20%7B%20obo%3ARO_0000087%20obo%3ARO_0002434%20rdfs%3AsubClassOf%20%7D%20%3Fs%20%3Fp%20%3Fo%20%7D" > filename.xml
```
@bill-baumgartner - given the time crunch I'm pulling this off of the TODO for the ISMB deadline. I have lots of exceptions in place currently and also have implemented some other types of manual logging. That will sufficient for now. I'm hopeful some of these less pressing "enhancements" can be implemented during rotations or as part of summer internships. 

Fix patch:
diff --git a/.github/workflows/build-qa.yml b/.github/workflows/build-qa.yml
index 723365029..a048b3ca0 100644
--- a/.github/workflows/build-qa.yml
+++ b/.github/workflows/build-qa.yml
@@ -54,7 +54,7 @@ jobs:
             GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
 
   publish_docker_container:
-    if: github.event_name == 'release' && startsWith(github.ref, 'refs/tags')
+    if: github.event_name == 'push'
     needs: build
     name: Push Docker Image to Docker Hub
     runs-on: ubuntu-latest
@@ -78,13 +78,23 @@ jobs:
         run: echo ${{ steps.docker_build.outputs.digest }}
 
   publish_pypi_library:
-    if: github.event_name == 'release' && startsWith(github.ref, 'refs/tags')
+    if: github.event_name == 'release'
     needs: build
     name: Publishes pkt_kg to PyPI
     runs-on: ubuntu-latest
     steps:
-      - name: Publish package
-        if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
+      - uses: actions/checkout@v2
+        with:
+            fetch-depth: 0
+      - name: Setup Python
+        uses: actions/setup-python@v2
+        with:
+          python-version: 3.6
+      - name: Install Requirements and Dependencies
+        run: pip install --upgrade pip setuptools wheel
+      - name: Create Package Distribution
+        run: python setup.py sdist bdist_wheel
+      - name: Publish Package tp PyPi
         uses: pypa/gh-action-pypi-publish@master
         with:
           user: __token__
diff --git a/.github/workflows/kg-build.yml b/.github/workflows/kg-build.yml
index b70184178..c81a06a36 100644
--- a/.github/workflows/kg-build.yml
+++ b/.github/workflows/kg-build.yml
@@ -1,44 +1,499 @@
 name: Build and Deploy to Google Compute Engine
-on: release
+#on: push
+on:
+  schedule:
+    - cron: '0 0 1 * *'  # runs at midnight on the first day of each month
 env:
   PROJECT_ID: ${{ secrets.GCE_PROJECT }}
-  GCE_INSTANCE: pkt-builder
-  GCE_INSTANCE_ZONE: us-central1-a
+  GCS_SERVICE_ACCOUNT: ${{ secrets.GCE_SA_KEY }}
 
 jobs:
-  setup-build-publish-deploy:
-    name: Setup, Build, Publish, and Deploy
+  pkt-build-phase-1-2:
+    name: KG Build Phases 1-2
     runs-on: ubuntu-latest
-
+    env:
+      DOCKERFILE: builds/Dockerfile.phases12
+      GCE_MACHINE_TYPE: n1-highmem-16
+      GCE_REGION: us-central1
     steps:
     - name: Checkout
       uses: actions/checkout@v2
       with:
-       ref: scripting_prebuild_notebooks
-
-    # Setup gcloud CLI
+       ref: ci_cd_build # UPDATE TO MASTER WHEN DONE TESTNG
+    - name: Setup Python
+      uses: actions/setup-python@v2
+      with:
+        python-version: 3.6
+    - name: Install Requirements and Dependencies
+      run: pip install -r builds/build_requirements.txt pkt_kg
     - uses: google-github-actions/setup-gcloud@master
       with:
         version: '290.0.1'
         service_account_key: ${{ secrets.GCE_SA_KEY }}
         project_id: ${{ secrets.GCE_PROJECT }}
-
-    # Configure Docker to use the gcloud command-line tool as a credential
-    # helper for authentication
-    - run: |-
-        gcloud --quiet auth configure-docker
-      # Build the Docker image
-    - name: Build
+    - name: Configure Docker Authentication
+      run: gcloud --quiet auth configure-docker
+    - name: Build Docker Image
+      run: docker build --tag "gcr.io/$PROJECT_ID/pkt_builder_phases_1_2_$GITHUB_SHA-image:$GITHUB_SHA" -f "$DOCKERFILE" ./builds
+    - name: Publish Docker Image to Google Container Registry
+      run: docker push "gcr.io/$PROJECT_ID/pkt_builder_phases_1_2_$GITHUB_SHA-image:$GITHUB_SHA"
+    - name: Clean-Up Google Cloud Storage Environment
+      run: gsutil rm -f gs://pheknowlator/current_build/*.log 2> /dev/null || true
+    - name: Submit Build Job to AI-Platform
       run: |-
-        docker build --tag "gcr.io/$PROJECT_ID/$GCE_INSTANCE-image:$GITHUB_SHA" .
-      # Push the Docker image to Google Container Registry
-    - name: Publish
-      run: |-
-        docker push "gcr.io/$PROJECT_ID/$GCE_INSTANCE-image:$GITHUB_SHA"
-    - name: Deploy
-      run: |-
-        gcloud compute instances update-container "$GCE_INSTANCE" \
-          --zone "$GCE_INSTANCE_ZONE" \
-          --container-image "gcr.io/$PROJECT_ID/$GCE_INSTANCE-image:$GITHUB_SHA"
+        gcloud ai-platform jobs submit training "pkt_builder_phases_1_2_$GITHUB_SHA" \
+          --scale-tier custom --master-machine-type "$GCE_MACHINE_TYPE" --region "$GCE_REGION" \
+          --master-image-uri "gcr.io/$PROJECT_ID/pkt_builder_phases_1_2_$GITHUB_SHA-image:$GITHUB_SHA"
+    - name: Monitor Build
+      run: python builds/job_monitoring.py --gce_type reg --phase 1 --sleep 60 --gcs_dir '' --project '' --job ''
+
+  pkt-build-phase3-job1:
+    needs: pkt-build-phase-1-2
+    name: KG Build Phase 3 - Job 1 (Subclass + RelationsOnly + OWL)
+    runs-on: ubuntu-latest
+    timeout-minutes: 3000
+    env:
+      GCE_ZONE: us-central1-a
+      GCE_CPU: 1
+      GCE_MEMORY: 250GB
+      GCE_BOOT_DISC_TYPE: pd-standard
+      GCE_BOOT_DISC_SIZE: 500GB
+      DOCKERFILE: builds/Dockerfile.phase3
+      GCE_JOB_NAME: pkt-builder-phase3-job1
+      GCS_LOCATION: 'subclass_builds/relations_only/owl'
+      KG_CONSTRUCT_APPROACH: 'subclass'
+      INV_RELATIONS: 'no'
+      DECODE_OWL: 'no'
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v2
+        with:
+          ref: ci_cd_build # UPDATE TO MASTER WHEN DONE TESTNG
+      - name: Setup Python
+        uses: actions/setup-python@v2
+        with:
+          python-version: 3.6
+      - name: Install Requirements and Dependencies
+        run: pip install -r builds/build_requirements.txt pkt_kg
+      - uses: google-github-actions/setup-gcloud@master
+        with:
+          version: '290.0.1'
+          service_account_key: ${{ secrets.GCE_SA_KEY }}
+          project_id: ${{ secrets.GCE_PROJECT }}
+      - name: Configure Docker Authentication
+        run: gcloud --quiet auth configure-docker
+      - name: Build Docker Image
+        run: |-
+          docker build --tag "gcr.io/$PROJECT_ID/$GCE_JOB_NAME-image:$GITHUB_SHA" -f "$DOCKERFILE" ./builds \
+            --build-arg "app_arg=$KG_CONSTRUCT_APPROACH" --build-arg "rel_arg=$INV_RELATIONS" --build-arg "owl_arg=$DECODE_OWL"
+      - name: Publish Docker Image to Google Container Registry
+        run: docker push "gcr.io/$PROJECT_ID/$GCE_JOB_NAME-image:$GITHUB_SHA"
+      - name: Clean-Up Google Cloud Storage Environment
+        run: gsutil rm -f "gs://pheknowlator/current_build/knowledge_graphs/$GCS_LOCATION/*.log" 2> /dev/null || true
+      - name: Create Virtual Machine Instance
+        run: |-
+          gcloud compute instances create-with-container "$GCE_JOB_NAME" --zone="$GCE_ZONE" \
+            --container-image "gcr.io/$PROJECT_ID/$GCE_JOB_NAME-image:$GITHUB_SHA" --container-restart-policy=never \
+            --boot-disk-type="$GCE_BOOT_DISC_TYPE" --boot-disk-size="$GCE_BOOT_DISC_SIZE" \
+            --custom-cpu="$GCE_CPU" --custom-memory="$GCE_MEMORY" --custom-extensions --scopes=storage-rw
+      - name: Monitor Build
+        run: python builds/job_monitoring.py --gce_type reg --phase 3 --sleep 60 --gcs_dir "$GCS_LOCATION" --project '' --job ''
+      - name: Stop Virtual Machine Instance
+        run: gcloud compute instances stop "$GCE_JOB_NAME" --zone="$GCE_ZONE"
+      - name: Delete Virtual Machine Instance
+        run: gcloud compute instances delete "$GCE_JOB_NAME" --zone="$GCE_ZONE"
+
+  pkt-build-phase3-job2:
+    needs: pkt-build-phase-1-2
+    name: KG Build Phase 3 - Job 2 (Subclass + RelationsOnly + OWL-NETS)
+    runs-on: ubuntu-latest
+    timeout-minutes: 3000
+    env:
+      GCE_ZONE: us-central1-a
+      GCE_CPU: 1
+      GCE_MEMORY: 250GB
+      GCE_BOOT_DISC_TYPE: pd-standard
+      GCE_BOOT_DISC_SIZE: 500GB
+      DOCKERFILE: builds/Dockerfile.phase3
+      GCE_JOB_NAME: pkt-builder-phase3-job2
+      GCS_LOCATION: 'subclass_builds/relations_only/owlnets'
+      KG_CONSTRUCT_APPROACH: 'subclass'
+      INV_RELATIONS: 'no'
+      DECODE_OWL: 'yes'
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v2
+        with:
+          ref: ci_cd_build # UPDATE TO MASTER WHEN DONE TESTNG
+      - name: Setup Python
+        uses: actions/setup-python@v2
+        with:
+          python-version: 3.6
+      - name: Install Requirements and Dependencies
+        run: pip install -r builds/build_requirements.txt pkt_kg
+      - uses: google-github-actions/setup-gcloud@master
+        with:
+          version: '290.0.1'
+          service_account_key: ${{ secrets.GCE_SA_KEY }}
+          project_id: ${{ secrets.GCE_PROJECT }}
+      - name: Configure Docker Authentication
+        run: gcloud --quiet auth configure-docker
+      - name: Build Docker Image
+        run: |-
+          docker build --tag "gcr.io/$PROJECT_ID/$GCE_JOB_NAME-image:$GITHUB_SHA" -f "$DOCKERFILE" ./builds \
+            --build-arg "app_arg=$KG_CONSTRUCT_APPROACH" --build-arg "rel_arg=$INV_RELATIONS" --build-arg "owl_arg=$DECODE_OWL"
+      - name: Publish Docker Image to Google Container Registry
+        run: docker push "gcr.io/$PROJECT_ID/$GCE_JOB_NAME-image:$GITHUB_SHA"
+      - name: Clean-Up Google Cloud Storage Environment
+        run: gsutil rm -f "gs://pheknowlator/current_build/knowledge_graphs/$GCS_LOCATION/*.log" 2> /dev/null || true
+      - name: Create Virtual Machine Instance
+        run: |-
+          gcloud compute instances create-with-container "$GCE_JOB_NAME" --zone="$GCE_ZONE" \
+            --container-image "gcr.io/$PROJECT_ID/$GCE_JOB_NAME-image:$GITHUB_SHA" --container-restart-policy=never \
+            --boot-disk-type="$GCE_BOOT_DISC_TYPE" --boot-disk-size="$GCE_BOOT_DISC_SIZE" \
+            --custom-cpu="$GCE_CPU" --custom-memory="$GCE_MEMORY" --custom-extensions --scopes=storage-rw
+      - name: Monitor Build
+        run: python builds/job_monitoring.py --gce_type reg --phase 3 --sleep 60 --gcs_dir "$GCS_LOCATION" --project '' --job ''
+      - name: Stop Virtual Machine Instance
+        run: gcloud compute instances stop "$GCE_JOB_NAME" --zone="$GCE_ZONE"
+      - name: Delete Virtual Machine Instance
+        run: gcloud compute instances delete "$GCE_JOB_NAME" --zone="$GCE_ZONE"
+
+  pkt-build-phase3-job3:
+    needs: pkt-build-phase-1-2
+    name: KG Build Phase 3 - Job 3 (Subclass + InverseRelations + OWL)
+    runs-on: ubuntu-latest
+    timeout-minutes: 3000
+    env:
+      GCE_ZONE: us-central1-a
+      GCE_CPU: 1
+      GCE_MEMORY: 250GB
+      GCE_BOOT_DISC_TYPE: pd-standard
+      GCE_BOOT_DISC_SIZE: 500GB
+      DOCKERFILE: builds/Dockerfile.phase3
+      GCE_JOB_NAME: pkt-builder-phase3-job3
+      GCS_LOCATION: 'subclass_builds/inverse_relations/owl'
+      KG_CONSTRUCT_APPROACH: 'subclass'
+      INV_RELATIONS: 'yes'
+      DECODE_OWL: 'no'
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v2
+        with:
+          ref: ci_cd_build # UPDATE TO MASTER WHEN DONE TESTNG
+      - name: Setup Python
+        uses: actions/setup-python@v2
+        with:
+          python-version: 3.6
+      - name: Install Requirements and Dependencies
+        run: pip install -r builds/build_requirements.txt pkt_kg
+      - uses: google-github-actions/setup-gcloud@master
+        with:
+          version: '290.0.1'
+          service_account_key: ${{ secrets.GCE_SA_KEY }}
+          project_id: ${{ secrets.GCE_PROJECT }}
+      - name: Configure Docker Authentication
+        run: gcloud --quiet auth configure-docker
+      - name: Build Docker Image
+        run: |-
+          docker build --tag "gcr.io/$PROJECT_ID/$GCE_JOB_NAME-image:$GITHUB_SHA" -f "$DOCKERFILE" ./builds \
+            --build-arg "app_arg=$KG_CONSTRUCT_APPROACH" --build-arg "rel_arg=$INV_RELATIONS" --build-arg "owl_arg=$DECODE_OWL"
+      - name: Publish Docker Image to Google Container Registry
+        run: docker push "gcr.io/$PROJECT_ID/$GCE_JOB_NAME-image:$GITHUB_SHA"
+      - name: Clean-Up Google Cloud Storage Environment
+        run: gsutil rm -f "gs://pheknowlator/current_build/knowledge_graphs/$GCS_LOCATION/*.log" 2> /dev/null || true
+      - name: Create Virtual Machine Instance
+        run: |-
+          gcloud compute instances create-with-container "$GCE_JOB_NAME" --zone="$GCE_ZONE" \
+            --container-image "gcr.io/$PROJECT_ID/$GCE_JOB_NAME-image:$GITHUB_SHA" --container-restart-policy=never \
+            --boot-disk-type="$GCE_BOOT_DISC_TYPE" --boot-disk-size="$GCE_BOOT_DISC_SIZE" \
+            --custom-cpu="$GCE_CPU" --custom-memory="$GCE_MEMORY" --custom-extensions --scopes=storage-rw
+      - name: Monitor Build
+        run: python builds/job_monitoring.py --gce_type reg --phase 3 --sleep 60 --gcs_dir "$GCS_LOCATION" --project '' --job ''
+      - name: Stop Virtual Machine Instance
+        run: gcloud compute instances stop "$GCE_JOB_NAME" --zone="$GCE_ZONE"
+      - name: Delete Virtual Machine Instance
+        run: gcloud compute instances delete "$GCE_JOB_NAME" --zone="$GCE_ZONE"
+
+  pkt-build-phase3-job4:
+      needs: pkt-build-phase-1-2
+      name: KG Build Phase 3 - Job 4 (Subclass + InverseRelations + OWL-NETS)
+      runs-on: ubuntu-latest
+      timeout-minutes: 3000
+      env:
+        GCE_ZONE: us-central1-a
+        GCE_CPU: 1
+        GCE_MEMORY: 250GB
+        GCE_BOOT_DISC_TYPE: pd-standard
+        GCE_BOOT_DISC_SIZE: 500GB
+        DOCKERFILE: builds/Dockerfile.phase3
+        GCE_JOB_NAME: pkt-builder-phase3-job4
+        GCS_LOCATION: 'subclass_builds/inverse_relations/owlnets'
+        KG_CONSTRUCT_APPROACH: 'subclass'
+        INV_RELATIONS: 'yes'
+        DECODE_OWL: 'yes'
+      steps:
+        - name: Checkout
+          uses: actions/checkout@v2
+          with:
+            ref: ci_cd_build # UPDATE TO MASTER WHEN DONE TESTNG
+        - name: Setup Python
+          uses: actions/setup-python@v2
+          with:
+            python-version: 3.6
+        - name: Install Requirements and Dependencies
+          run: pip install -r builds/build_requirements.txt pkt_kg
+        - uses: google-github-actions/setup-gcloud@master
+          with:
+            version: '290.0.1'
+            service_account_key: ${{ secrets.GCE_SA_KEY }}
+            project_id: ${{ secrets.GCE_PROJECT }}
+        - name: Configure Docker Authentication
+          run: gcloud --quiet auth configure-docker
+        - name: Build Docker Image
+          run: |-
+            docker build --tag "gcr.io/$PROJECT_ID/$GCE_JOB_NAME-image:$GITHUB_SHA" -f "$DOCKERFILE" ./builds \
+              --build-arg "app_arg=$KG_CONSTRUCT_APPROACH" --build-arg "rel_arg=$INV_RELATIONS" --build-arg "owl_arg=$DECODE_OWL"
+        - name: Publish Docker Image to Google Container Registry
+          run: docker push "gcr.io/$PROJECT_ID/$GCE_JOB_NAME-image:$GITHUB_SHA"
+        - name: Clean-Up Google Cloud Storage Environment
+          run: gsutil rm -f "gs://pheknowlator/current_build/knowledge_graphs/$GCS_LOCATION/*.log" 2> /dev/null || true
+        - name: Create Virtual Machine Instance
+          run: |-
+            gcloud compute instances create-with-container "$GCE_JOB_NAME" --zone="$GCE_ZONE" \
+              --container-image "gcr.io/$PROJECT_ID/$GCE_JOB_NAME-image:$GITHUB_SHA" --container-restart-policy=never \
+              --boot-disk-type="$GCE_BOOT_DISC_TYPE" --boot-disk-size="$GCE_BOOT_DISC_SIZE" \
+              --custom-cpu="$GCE_CPU" --custom-memory="$GCE_MEMORY" --custom-extensions --scopes=storage-rw
+        - name: Monitor Build
+          run: python builds/job_monitoring.py --gce_type reg --phase 3 --sleep 60 --gcs_dir "$GCS_LOCATION" --project '' --job ''
+        - name: Stop Virtual Machine Instance
+          run: gcloud compute instances stop "$GCE_JOB_NAME" --zone="$GCE_ZONE"
+        - name: Delete Virtual Machine Instance
+          run: gcloud compute instances delete "$GCE_JOB_NAME" --zone="$GCE_ZONE"
+
+  pkt-build-phase3-job5:
+    needs: pkt-build-phase-1-2
+    name: KG Build Phase 3 - Job 5 (Instance + RelationsOnly + OWL)
+    runs-on: ubuntu-latest
+    timeout-minutes: 1200
+    env:
+      GCE_ZONE: us-central1-a
+      GCE_CPU: 1
+      GCE_MEMORY: 250GB
+      GCE_BOOT_DISC_TYPE: pd-standard
+      GCE_BOOT_DISC_SIZE: 500GB
+      DOCKERFILE: builds/Dockerfile.phase3
+      GCE_JOB_NAME: pkt-builder-phase3-job5
+      GCS_LOCATION: 'instance_builds/relations_only/owl'
+      KG_CONSTRUCT_APPROACH: 'instance'
+      INV_RELATIONS: 'no'
+      DECODE_OWL: 'no'
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v2
+        with:
+          ref: ci_cd_build # UPDATE TO MASTER WHEN DONE TESTNG
+      - name: Setup Python
+        uses: actions/setup-python@v2
+        with:
+          python-version: 3.6
+      - name: Install Requirements and Dependencies
+        run: pip install -r builds/build_requirements.txt pkt_kg
+      - uses: google-github-actions/setup-gcloud@master
+        with:
+          version: '290.0.1'
+          service_account_key: ${{ secrets.GCE_SA_KEY }}
+          project_id: ${{ secrets.GCE_PROJECT }}
+      - name: Configure Docker Authentication
+        run: gcloud --quiet auth configure-docker
+      - name: Build Docker Image
+        run: |-
+          docker build --tag "gcr.io/$PROJECT_ID/$GCE_JOB_NAME-image:$GITHUB_SHA" -f "$DOCKERFILE" ./builds \
+            --build-arg "app_arg=$KG_CONSTRUCT_APPROACH" --build-arg "rel_arg=$INV_RELATIONS" --build-arg "owl_arg=$DECODE_OWL"
+      - name: Publish Docker Image to Google Container Registry
+        run: docker push "gcr.io/$PROJECT_ID/$GCE_JOB_NAME-image:$GITHUB_SHA"
+      - name: Clean-Up Google Cloud Storage Environment
+        run: gsutil rm -f "gs://pheknowlator/current_build/knowledge_graphs/$GCS_LOCATION/*.log" 2> /dev/null || true
+      - name: Create Virtual Machine Instance
+        run: |-
+          gcloud compute instances create-with-container "$GCE_JOB_NAME" --zone="$GCE_ZONE" \
+            --container-image "gcr.io/$PROJECT_ID/$GCE_JOB_NAME-image:$GITHUB_SHA" --container-restart-policy=never \
+            --boot-disk-type="$GCE_BOOT_DISC_TYPE" --boot-disk-size="$GCE_BOOT_DISC_SIZE" \
+            --custom-cpu="$GCE_CPU" --custom-memory="$GCE_MEMORY" --custom-extensions --scopes=storage-rw
+      - name: Monitor Build
+        run: python builds/job_monitoring.py --gce_type reg --phase 3 --sleep 60 --gcs_dir "$GCS_LOCATION" --project '' --job ''
+      - name: Stop Virtual Machine Instance
+        run: gcloud compute instances stop "$GCE_JOB_NAME" --zone="$GCE_ZONE"
+      - name: Delete Virtual Machine Instance
+        run: gcloud compute instances delete "$GCE_JOB_NAME" --zone="$GCE_ZONE"
+
+  pkt-build-phase3-job6:
+    needs: pkt-build-phase-1-2
+    name: KG Build Phase 3 - Job 6 (Instance + RelationsOnly + OWL-NETS)
+    runs-on: ubuntu-latest
+    timeout-minutes: 3000
+    env:
+      GCE_ZONE: us-central1-a
+      GCE_CPU: 1
+      GCE_MEMORY: 250GB
+      GCE_BOOT_DISC_TYPE: pd-standard
+      GCE_BOOT_DISC_SIZE: 500GB
+      DOCKERFILE: builds/Dockerfile.phase3
+      GCE_JOB_NAME: pkt-builder-phase3-job6
+      GCS_LOCATION: 'instance_builds/relations_only/owlnets'
+      KG_CONSTRUCT_APPROACH: 'instance'
+      INV_RELATIONS: 'no'
+      DECODE_OWL: 'yes'
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v2
+        with:
+          ref: ci_cd_build # UPDATE TO MASTER WHEN DONE TESTNG
+      - name: Setup Python
+        uses: actions/setup-python@v2
+        with:
+          python-version: 3.6
+      - name: Install Requirements and Dependencies
+        run: pip install -r builds/build_requirements.txt pkt_kg
+      - uses: google-github-actions/setup-gcloud@master
+        with:
+          version: '290.0.1'
+          service_account_key: ${{ secrets.GCE_SA_KEY }}
+          project_id: ${{ secrets.GCE_PROJECT }}
+      - name: Configure Docker Authentication
+        run: gcloud --quiet auth configure-docker
+      - name: Build Docker Image
+        run: |-
+          docker build --tag "gcr.io/$PROJECT_ID/$GCE_JOB_NAME-image:$GITHUB_SHA" -f "$DOCKERFILE" ./builds \
+            --build-arg "app_arg=$KG_CONSTRUCT_APPROACH" --build-arg "rel_arg=$INV_RELATIONS" --build-arg "owl_arg=$DECODE_OWL"
+      - name: Publish Docker Image to Google Container Registry
+        run: docker push "gcr.io/$PROJECT_ID/$GCE_JOB_NAME-image:$GITHUB_SHA"
+      - name: Clean-Up Google Cloud Storage Environment
+        run: gsutil rm -f "gs://pheknowlator/current_build/knowledge_graphs/$GCS_LOCATION/*.log" 2> /dev/null || true
+      - name: Create Virtual Machine Instance
+        run: |-
+          gcloud compute instances create-with-container "$GCE_JOB_NAME" --zone="$GCE_ZONE" \
+            --container-image "gcr.io/$PROJECT_ID/$GCE_JOB_NAME-image:$GITHUB_SHA" --container-restart-policy=never \
+            --boot-disk-type="$GCE_BOOT_DISC_TYPE" --boot-disk-size="$GCE_BOOT_DISC_SIZE" \
+            --custom-cpu="$GCE_CPU" --custom-memory="$GCE_MEMORY" --custom-extensions --scopes=storage-rw
+      - name: Monitor Build
+        run: python builds/job_monitoring.py --gce_type reg --phase 3 --sleep 60 --gcs_dir "$GCS_LOCATION" --project '' --job ''
+      - name: Stop Virtual Machine Instance
+        run: gcloud compute instances stop "$GCE_JOB_NAME" --zone="$GCE_ZONE"
+      - name: Delete Virtual Machine Instance
+        run: gcloud compute instances delete "$GCE_JOB_NAME" --zone="$GCE_ZONE"
+
+  pkt-build-phase3-job7:
+    needs: pkt-build-phase-1-2
+    name: KG Build Phase 3 - Job 7 (Instance + InverseRelations + OWL)
+    runs-on: ubuntu-latest
+    timeout-minutes: 3000
+    env:
+      GCE_ZONE: us-central1-a
+      GCE_CPU: 1
+      GCE_MEMORY: 250GB
+      GCE_BOOT_DISC_TYPE: pd-standard
+      GCE_BOOT_DISC_SIZE: 500GB
+      DOCKERFILE: builds/Dockerfile.phase3
+      GCE_JOB_NAME: pkt-builder-phase3-job7
+      GCS_LOCATION: 'instance_builds/inverse_relations/owl'
+      KG_CONSTRUCT_APPROACH: 'instance'
+      INV_RELATIONS: 'yes'
+      DECODE_OWL: 'no'
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v2
+        with:
+          ref: ci_cd_build # UPDATE TO MASTER WHEN DONE TESTNG
+      - name: Setup Python
+        uses: actions/setup-python@v2
+        with:
+          python-version: 3.6
+      - name: Install Requirements and Dependencies
+        run: pip install -r builds/build_requirements.txt pkt_kg
+      - uses: google-github-actions/setup-gcloud@master
+        with:
+          version: '290.0.1'
+          service_account_key: ${{ secrets.GCE_SA_KEY }}
+          project_id: ${{ secrets.GCE_PROJECT }}
+      - name: Configure Docker Authentication
+        run: gcloud --quiet auth configure-docker
+      - name: Build Docker Image
+        run: |-
+          docker build --tag "gcr.io/$PROJECT_ID/$GCE_JOB_NAME-image:$GITHUB_SHA" -f "$DOCKERFILE" ./builds \
+            --build-arg "app_arg=$KG_CONSTRUCT_APPROACH" --build-arg "rel_arg=$INV_RELATIONS" --build-arg "owl_arg=$DECODE_OWL"
+      - name: Publish Docker Image to Google Container Registry
+        run: docker push "gcr.io/$PROJECT_ID/$GCE_JOB_NAME-image:$GITHUB_SHA"
+      - name: Clean-Up Google Cloud Storage Environment
+        run: gsutil rm -f "gs://pheknowlator/current_build/knowledge_graphs/$GCS_LOCATION/*.log" 2> /dev/null || true
+      - name: Create Virtual Machine Instance
+        run: |-
+          gcloud compute instances create-with-container "$GCE_JOB_NAME" --zone="$GCE_ZONE" \
+            --container-image "gcr.io/$PROJECT_ID/$GCE_JOB_NAME-image:$GITHUB_SHA" --container-restart-policy=never \
+            --boot-disk-type="$GCE_BOOT_DISC_TYPE" --boot-disk-size="$GCE_BOOT_DISC_SIZE" \
+            --custom-cpu="$GCE_CPU" --custom-memory="$GCE_MEMORY" --custom-extensions --scopes=storage-rw
+      - name: Monitor Build
+        run: python builds/job_monitoring.py --gce_type reg --phase 3 --sleep 60 --gcs_dir "$GCS_LOCATION" --project '' --job ''
+      - name: Stop Virtual Machine Instance
+        run: gcloud compute instances stop "$GCE_JOB_NAME" --zone="$GCE_ZONE"
+      - name: Delete Virtual Machine Instance
+        run: gcloud compute instances delete "$GCE_JOB_NAME" --zone="$GCE_ZONE"
+
+  pkt-build-phase3-job8:
+    needs: pkt-build-phase-1-2
+    name: KG Build Phase 3 - Job 8 (Instance + InverseRelations + OWL-NETS)
+    runs-on: ubuntu-latest
+    timeout-minutes: 3000
+    env:
+      GCE_ZONE: us-central1-a
+      GCE_CPU: 1
+      GCE_MEMORY: 250GB
+      GCE_BOOT_DISC_TYPE: pd-standard
+      GCE_BOOT_DISC_SIZE: 500GB
+      DOCKERFILE: builds/Dockerfile.phase3
+      GCE_JOB_NAME: pkt-builder-phase3-job8
+      GCS_LOCATION: 'instance_builds/inverse_relations/owlnets'
+      KG_CONSTRUCT_APPROACH: 'instance'
+      INV_RELATIONS: 'yes'
+      DECODE_OWL: 'yes'
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v2
+        with:
+          ref: ci_cd_build # UPDATE TO MASTER WHEN DONE TESTNG
+      - name: Setup Python
+        uses: actions/setup-python@v2
+        with:
+          python-version: 3.6
+      - name: Install Requirements and Dependencies
+        run: pip install -r builds/build_requirements.txt pkt_kg
+      - uses: google-github-actions/setup-gcloud@master
+        with:
+          version: '290.0.1'
+          service_account_key: ${{ secrets.GCE_SA_KEY }}
+          project_id: ${{ secrets.GCE_PROJECT }}
+      - name: Configure Docker Authentication
+        run: gcloud --quiet auth configure-docker
+      - name: Build Docker Image
+        run: |-
+          docker build --tag "gcr.io/$PROJECT_ID/$GCE_JOB_NAME-image:$GITHUB_SHA" -f "$DOCKERFILE" ./builds \
+            --build-arg "app_arg=$KG_CONSTRUCT_APPROACH" --build-arg "rel_arg=$INV_RELATIONS" --build-arg "owl_arg=$DECODE_OWL"
+      - name: Publish Docker Image to Google Container Registry
+        run: docker push "gcr.io/$PROJECT_ID/$GCE_JOB_NAME-image:$GITHUB_SHA"
+      - name: Clean-Up Google Cloud Storage Environment
+        run: gsutil rm -f "gs://pheknowlator/current_build/knowledge_graphs/$GCS_LOCATION/*.log" 2> /dev/null || true
+      - name: Create Virtual Machine Instance
+        run: |-
+          gcloud compute instances create-with-container "$GCE_JOB_NAME" --zone="$GCE_ZONE" \
+            --container-image "gcr.io/$PROJECT_ID/$GCE_JOB_NAME-image:$GITHUB_SHA" --container-restart-policy=never \
+            --boot-disk-type="$GCE_BOOT_DISC_TYPE" --boot-disk-size="$GCE_BOOT_DISC_SIZE" \
+            --custom-cpu="$GCE_CPU" --custom-memory="$GCE_MEMORY" --custom-extensions --scopes=storage-rw
+      - name: Monitor Build
+        run: python builds/job_monitoring.py --gce_type reg --phase 3 --sleep 60 --gcs_dir "$GCS_LOCATION" --project '' --job ''
+      - name: Stop Virtual Machine Instance
+        run: gcloud compute instances stop "$GCE_JOB_NAME" --zone="$GCE_ZONE"
+      - name: Delete Virtual Machine Instance
+        run: gcloud compute instances delete "$GCE_JOB_NAME" --zone="$GCE_ZONE"
 
-## Need to add instance start and stop commands
\ No newline at end of file
+### ADD BILL'S CODE FOR UPDATING TO SPARQL ENDPOINT
+### ADD SELF-HOSTED RUNNER: https://github.com/terraform-google-modules/terraform-google-github-actions-runners/blob/master/examples/gh-runner-gke-dind/README.md
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index e82697146..55878858e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,9 +8,9 @@
 __pycache__
 __pycache__/
 **/__pycache__/*
-build/**
+pkt_kg.egg*
+build/*
 dist/*
-pkt_kg.egg-info/*
 
 #### Testing
 .single_run
@@ -20,10 +20,13 @@ coverage.xml
 \.vscode/
 /tests/archive/
 
+#### Logs
+logs/*.log
+**/logs/*.log
+
 #### CI/CD Builds
 builds/temp/*
-builds/GitHub Action Workflow Build Jobs.xlsx
-
+/builds/GitHub Action Workflow Build Jobs.xlsx
 
 #### API Keys/Project Passwords
 /resources/project_keys/*
@@ -63,4 +66,4 @@ scratch*.py
 !/resources/relations_data/README.md
 
 ## Releases
-/releases/*
\ No newline at end of file
+/releases/*
diff --git a/Data_Preparation.ipynb b/Data_Preparation.ipynb
index 1d14032f2..2cb526671 100644
--- a/Data_Preparation.ipynb
+++ b/Data_Preparation.ipynb
@@ -944,7 +944,7 @@
    "outputs": [],
    "source": [
     "# download data\n",
-    "url = 'https://storage.googleapis.com/pheknowlator/release_v2.0.0/curated_data/genomic_typing_dict.pkl'\n",
+    "url = 'https://storage.googleapis.com/pheknowlator/curated_data/genomic_typing_dict.pkl'\n",
     "if not os.path.exists(unprocessed_data_location + 'genomic_typing_dict.pkl'):\n",
     "    data_downloader(url, unprocessed_data_location)\n",
     "\n",
@@ -2547,7 +2547,7 @@
    "outputs": [],
    "source": [
     "# download data\n",
-    "url='https://storage.googleapis.com/pheknowlator/release_v2.0.0/curated_data/zooma_tissue_cell_mapping_04JAN2020.xlsx'\n",
+    "url='https://storage.googleapis.com/pheknowlator/curated_data/zooma_tissue_cell_mapping_04JAN2020.xlsx'\n",
     "if not os.path.exists(unprocessed_data_location + 'zooma_tissue_cell_mapping_04JAN2020.xlsx'):\n",
     "    data_downloader(url, unprocessed_data_location)\n",
     "    \n",
@@ -3055,7 +3055,7 @@
    "outputs": [],
    "source": [
     "# download data\n",
-    "url='https://storage.googleapis.com/pheknowlator/release_v2.0.0/curated_data/genomic_sequence_ontology_mappings.xlsx'\n",
+    "url='https://storage.googleapis.com/pheknowlator/curated_data/genomic_sequence_ontology_mappings.xlsx'\n",
     "if not os.path.exists(unprocessed_data_location + 'genomic_sequence_ontology_mappings.xlsx'):\n",
     "    data_downloader(url, unprocessed_data_location)\n",
     "\n",
diff --git a/Dockerfile b/Dockerfile
index e7e66ef78..2406afb61 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,5 +1,5 @@
 #!/usr/local/bin/docker
-# -*- version: 19.03.8, build afacb8b -*-
+# -*- version: 20.10.2 -*-
 
 ############################################
 ## MULTI-STAGE CONTAINER CONFIGURATION ##
@@ -19,25 +19,44 @@ RUN wget -O- https://apt.corretto.aws/corretto.key | apt-key add - && \
 ## PHEKNOWLATOR (PKT_KG) PROJECT SETTINGS ##
 # create needed project directories
 WORKDIR /PheKnowLator
-# I THINK WE NEED LINE 23 -- which I had to delete to get it to run past this point
-#RUN mkdir /PheKnowLator
-RUN mkdir /PheKnowLator/resources
-RUN mkdir /PheKnowLator/resources/edge_data
-RUN mkdir /PheKnowLator/resources/knowledge_graphs
-RUN mkdir /PheKnowLator/resources/ontologies
-
-# copy pkt_kg scripts
-COPY pkt_kg /PheKnowLator/pkt_kg
+RUN mkdir -p /PheKnowLator
+RUN mkdir -p /PheKnowLator/resources
+RUN mkdir -p /PheKnowLator/resources/construction_approach
+RUN mkdir -p /PheKnowLator/resources/edge_data
+RUN mkdir -p /PheKnowLator/resources/knowledge_graphs
+RUN mkdir -p /PheKnowLator/resources/node_data
+RUN mkdir -p /PheKnowLator/resources/ontologies
+RUN mkdir -p /PheKnowLator/resources/processed_data
+RUN mkdir -p /PheKnowLator/resources/relations_data
 
 # copy scripts/files needed to run pkt_kg
+COPY pkt_kg /PheKnowLator/pkt_kg
 COPY Main.py /PheKnowLator
 COPY setup.py /PheKnowLator
 COPY README.rst /PheKnowLator
 COPY resources /PheKnowLator/resources
 
+# download and copy needed data
+RUN curl -O https://storage.googleapis.com/pheknowlator/current_build/dependencies/edge_source_list.txt && mv edge_source_list.txt resources/
+RUN curl -O https://storage.googleapis.com/pheknowlator/current_build/dependencies/ontology_source_list.txt && mv ontology_source_list.txt resources/
+RUN curl -O https://storage.googleapis.com/pheknowlator/current_build/dependencies/resource_info.txt && mv resource_info.txt resources/
+RUN curl -O https://storage.googleapis.com/pheknowlator/current_build/dependencies/subclass_construction_map.pkl && mv subclass_construction_map.pkl resources/construction_approach/
+RUN curl -O https://storage.googleapis.com/pheknowlator/current_build/dependencies/PheKnowLator_MergedOntologies.owl && mv PheKnowLator_MergedOntologies.owl resources/knowledge_graphs/
+RUN curl -O https://storage.googleapis.com/pheknowlator/current_build/dependencies/node_metadata_dict.pkl && mv node_metadata_dict.pkl resources/node_data/
+RUN curl -O https://storage.googleapis.com/pheknowlator/current_build/data/processed_data/DISEASE_MONDO_MAP.txt && mv DISEASE_MONDO_MAP.txt resources/processed_data/
+RUN curl -O https://storage.googleapis.com/pheknowlator/current_build/data/processed_data/ENSEMBL_GENE_ENTREZ_GENE_MAP.txt && mv ENSEMBL_GENE_ENTREZ_GENE_MAP.txt resources/processed_data/
+RUN curl -O https://storage.googleapis.com/pheknowlator/current_build/data/processed_data/ENTREZ_GENE_PRO_ONTOLOGY_MAP.txt && mv ENTREZ_GENE_PRO_ONTOLOGY_MAP.txt resources/processed_data/
+RUN curl -O https://storage.googleapis.com/pheknowlator/current_build/data/processed_data/GENE_SYMBOL_ENSEMBL_TRANSCRIPT_MAP.txt && mv GENE_SYMBOL_ENSEMBL_TRANSCRIPT_MAP.txt resources/processed_data/
+RUN curl -O https://storage.googleapis.com/pheknowlator/current_build/data/processed_data/HPA_GTEx_TISSUE_CELL_MAP.txt && mv HPA_GTEx_TISSUE_CELL_MAP.txt resources/processed_data/
+RUN curl -O https://storage.googleapis.com/pheknowlator/current_build/data/processed_data/MESH_CHEBI_MAP.txt && mv MESH_CHEBI_MAP.txt resources/processed_data/
+RUN curl -O https://storage.googleapis.com/pheknowlator/current_build/data/processed_data/PHENOTYPE_HPO_MAP.txt && mv PHENOTYPE_HPO_MAP.txt resources/processed_data/
+RUN curl -O https://storage.googleapis.com/pheknowlator/current_build/data/processed_data/STRING_PRO_ONTOLOGY_MAP.txt && mv STRING_PRO_ONTOLOGY_MAP.txt resources/processed_data/
+RUN curl -O https://storage.googleapis.com/pheknowlator/current_build/data/processed_data/UNIPROT_ACCESSION_PRO_ONTOLOGY_MAP.txt && mv UNIPROT_ACCESSION_PRO_ONTOLOGY_MAP.txt resources/processed_data/
+RUN curl -O https://storage.googleapis.com/pheknowlator/current_build/dependencies/INVERSE_RELATIONS.txt && mv INVERSE_RELATIONS.txt resources/relations_data/
+RUN curl -O https://storage.googleapis.com/pheknowlator/current_build/dependencies/RELATIONS_LABELS.txt && mv RELATIONS_LABELS.txt resources/relations_data/
+
 # install needed python libraries
 RUN pip install --upgrade pip setuptools
-# BILL -- CAN WE REMOVE LINE  41 if we had to add line 21 to get the container to run
 WORKDIR /PheKnowLator
 RUN pip install .
 
diff --git a/MANIFEST.in b/MANIFEST.in
index e69de29bb..15dc66f26 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -0,0 +1,1 @@
+include pkt_kg/libs/owltools
\ No newline at end of file
diff --git a/Main.py b/Main.py
index 1f7b7d382..0066d58e3 100644
--- a/Main.py
+++ b/Main.py
@@ -21,10 +21,9 @@ def main():
     parser.add_argument('-a', '--app', help='construction approach to use (i.e. instance or subclass)', required=True)
     parser.add_argument('-t', '--res', help='name/path to text file containing resource_info', required=True)
     parser.add_argument('-b', '--kg', help='build type: "partial", "full", or "post-closure"', required=True)
-    parser.add_argument('-n', '--nde', help='yes/no - non-ontology node metadata directory location', required=True)
     parser.add_argument('-r', '--rel', help='yes/no - adding inverse relations to knowledge graph', required=True)
     parser.add_argument('-s', '--owl', help='yes/no - removing OWL Semantics from knowledge graph', required=True)
-    parser.add_argument('-m', '--kgm', help='yes/no - adding node metadata to knowledge graph', required=True)
+    parser.add_argument('-m', '--nde', help='yes/no - adding node metadata to knowledge graph', required=True)
     parser.add_argument('-o', '--out', help='name/path to directory where to write knowledge graph', required=True)
 
     args = parser.parse_args()
@@ -40,30 +39,30 @@ def main():
     # see the 'Data_Preparation.ipynb' and 'Ontology_Cleaning.ipynb' file for examples and guidelines
 
     # STEP 3: DOWNLOAD ONTOLOGIES
-    print('\n' + '=' * 33 + '\nDOWNLOADING DATA: ONTOLOGY DATA\n' + '=' * 33 + '\n')
+    print('\n' + '=' * 33 + '\nPKT: DOWNLOADING DATA: ONTOLOGY DATA\n' + '=' * 33 + '\n')
     start = time.time()
     ont = OntData(data_path=args.onts, resource_data=args.res)
     # ont = OntData(data_path='resources/ontology_source_list.txt', resource_data='resources/resource_info.txt')
     ont.downloads_data_from_url()
     end = time.time()
     timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
-    print('\nTOTAL SECONDS TO DOWNLOAD ONTOLOGIES: {} @ {}'.format(end - start, timestamp))
+    print('\nPKT: TOTAL SECONDS TO DOWNLOAD ONTOLOGIES: {} @ {}'.format(end - start, timestamp))
 
     # STEP 4: DOWNLOAD EDGE DATA SOURCES
-    print('\n' + '=' * 33 + '\nDOWNLOADING DATA: CLASS DATA\n' + '=' * 33 + '\n')
+    print('\n' + '=' * 33 + '\nPKT: DOWNLOADING DATA: CLASS DATA\n' + '=' * 33 + '\n')
     start = time.time()
     ent = LinkedData(data_path=args.edg, resource_data=args.res)
     # ent = LinkedData(data_path='resources/edge_source_list.txt', resource_data='resources/resource_info.txt')
     ent.downloads_data_from_url()
     end = time.time()
     timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
-    print('\nTOTAL SECONDS TO DOWNLOAD NON-ONTOLOGY DATA: {} @ {}'.format(end - start, timestamp))
+    print('\nPKT: TOTAL SECONDS TO DOWNLOAD NON-ONTOLOGY DATA: {} @ {}'.format(end - start, timestamp))
 
     #####################
     # CREATE EDGE LISTS #
     #####################
 
-    print('\n' + '=' * 33 + '\nPROCESSING EDGE DATA\n' + '=' * 33 + '\n')
+    print('\n' + '=' * 33 + '\nPKT: PROCESSING EDGE DATA\n' + '=' * 33 + '\n')
     start = time.time()
     combined_edges = dict(ent.data_files, **ont.data_files)
     master_edges = CreatesEdgeList(data_files=combined_edges, source_file=args.res)
@@ -71,13 +70,13 @@ def main():
     master_edges.creates_knowledge_graph_edges()
     end = time.time()
     timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
-    print('\nTOTAL SECONDS TO BUILD THE MASTER EDGE LIST: {} @ {}'.format(end - start, timestamp))
+    print('\nPKT: TOTAL SECONDS TO BUILD THE MASTER EDGE LIST: {} @ {}'.format(end - start, timestamp))
 
     #########################
     # BUILD KNOWLEDGE GRAPH #
     #########################
 
-    print('\n' + '=' * 33 + '\nBUILDING KNOWLEDGE GRAPH\n' + '=' * 33 + '\n')
+    print('\n' + '=' * 33 + '\nPKT: BUILDING KNOWLEDGE GRAPH\n' + '=' * 33 + '\n')
     start = time.time()
 
     if args.kg == 'partial':
@@ -102,7 +101,7 @@ def main():
 
     end = time.time()
     timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
-    print('\nTOTAL SECONDS TO CONSTRUCT A KG: {} @ {}'.format(end - start, timestamp))
+    print('\nPKT: TOTAL SECONDS TO CONSTRUCT A KG: {} @ {}'.format(end - start, timestamp))
 
 
 if __name__ == '__main__':
diff --git a/Ontology_Cleaning.ipynb b/Ontology_Cleaning.ipynb
index 9678555d2..c80b5828a 100644
--- a/Ontology_Cleaning.ipynb
+++ b/Ontology_Cleaning.ipynb
@@ -28,7 +28,7 @@
     "**Dependencies:**   \n",
     "- <u>Scripts</u>: This notebook utilizes several helper functions, which are stored in [utility scripts](https://github.com/callahantiff/PheKnowLator/blob/master/pkt_kg/utils) and the main code to perform the error checks in the  [`builds/temp/ontology_cleaning.py`](https://github.com/callahantiff/PheKnowLator/blob/master/builds/ontology_cleaning.py) script.\n",
     "- <u>Software</u>:[`OWLTools`](https://github.com/owlcollab/owltools)  \n",
-    "- <u>Data</u>: [`Merged_gene_rna_protein_identifiers.pkl`](https://storage.googleapis.com/pheknowlator/release_v2.0.0/current_build/data/processed_data/Merged_gene_rna_protein_identifiers.pkl), which is automatically downloaded to the `./resources/ontologies` directory     \n",
+    "- <u>Data</u>: [`Merged_gene_rna_protein_identifiers.pkl`](https://storage.googleapis.com/pheknowlator/current_build/data/processed_data/Merged_gene_rna_protein_identifiers.pkl), which is automatically downloaded to the `./resources/ontologies` directory     \n",
     "\n",
     "<br>\n",
     "\n",
@@ -194,6 +194,7 @@
     "import datetime\n",
     "import glob\n",
     "import pickle\n",
+    "import shutil\n",
     "\n",
     "from rdflib import Graph\n",
     "from tqdm import tqdm\n",
@@ -299,7 +300,10 @@
     "ont_data = OntologyCleaner('', '', '', write_location)\n",
     "\n",
     "# updating ontology info dictionary\n",
-    "ont_data.ontology_info = {k.split('/')[-1]: {} for k, v in ont_data.ontology_info.items()}"
+    "ont_data.ontology_info = {k.split('/')[-1]: {} for k, v in ont_data.ontology_info.items()}\n",
+    "\n",
+    "# set owl tools location\n",
+    "ont_data.owltools_location = './pkt_kg/libs/owltools'"
    ]
   },
   {
@@ -426,15 +430,6 @@
     "ontology_file_formatter(knowledge_graphs_location, '/' + ont_data.ont_file_location, ont_data.owltools_location)"
    ]
   },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "ontology_file_formatter(knowledge_graphs_location, '/' + ont_data.ont_file_location, ont_data.owltools_location)"
-   ]
-  },
   {
    "cell_type": "markdown",
    "metadata": {},
@@ -480,6 +475,11 @@
     "            else: o.write('\\t\\t- {}\\n'.format(x['Normalized - Duplicates']))\n",
     "            o.write('\\t\\t- Other Classes that May Need Normalization: {}\\n'.format(x['Normalized - NonOnt']))\n",
     "            o.write('\\t\\t- Normalized HGNC IDs: {}\\n'.format(x['Normalized - Gene IDs']))\n",
+    "            o.write('\\t- Deprecated Ontology HGNC Identifiers Needing Alignment:\\n')\n",
+    "            if x['Normalized - Dep'] != 'None':\n",
+    "                for i in x['Normalized - Dep']: o.write('\\t\\t- {}\\n'.format(i))\n",
+    "            else: o.write('\\t\\t- {}\\n'.format(x['Normalized - Dep']))\n",
+    "    \n",
     "o.close()"
    ]
   },
@@ -498,7 +498,11 @@
    "source": [
     "# remove temp file in resources/ontologies\n",
     "os.remove(write_location + '/' + ont_data.ont_file_location)\n",
-    "os.remove(write_location + '/Merged_gene_rna_protein_identifiers.pkl')"
+    "os.remove(write_location + '/Merged_gene_rna_protein_identifiers.pkl')\n",
+    "\n",
+    "# # remove logs directory\n",
+    "# logs = glob.glob('*/logs/*.log')\n",
+    "# shutil.rmtree('/'.join(logs[0].split('/')[:-1]))"
    ]
   },
   {
diff --git a/README.rst b/README.rst
index 848d5bbdd..983dd2773 100644
--- a/README.rst
+++ b/README.rst
@@ -1,7 +1,7 @@
-|logo|
+|logo| 
 
 
-|github_action| |mypy|
+|github_action| |mypy|  
 
 |sonar_quality| |code_climate_maintainability| |codacy| |code_climate_coverage| |coveralls|
 
@@ -325,11 +325,12 @@ Citing this Work
      url          = {https://doi.org/10.5281/zenodo.3401437}}
 
 
-
-
 .. |logo| image:: https://user-images.githubusercontent.com/8030363/106306246-01df9100-621b-11eb-81c3-d1f2c2e124a6.png
    :target: https://github.com/callahantiff/PheKnowLator
 
+.. |logo| image:: https://user-images.githubusercontent.com/8030363/106306246-01df9100-621b-11eb-81c3-d1f2c2e124a6.png
+   :target: https://github.com/callahantiff/PheKnowLator  
+   
 .. |ABRA| image:: https://img.shields.io/badge/ReproducibleResearch-AbraCollaboratory-magenta.svg
    :target: https://github.com/callahantiff/Abra-Collaboratory
 
diff --git a/builds/.dockerignore b/builds/.dockerignore
deleted file mode 100644
index affcc4f0f..000000000
--- a/builds/.dockerignore
+++ /dev/null
@@ -1,2 +0,0 @@
-!Dockerfile
-temp/
\ No newline at end of file
diff --git a/builds/Dockerfile b/builds/Dockerfile
deleted file mode 100644
index a1050f5a5..000000000
--- a/builds/Dockerfile
+++ /dev/null
@@ -1,13 +0,0 @@
-# import phase 3 container
-#FROM callahantiff/pheknowlator:latest
-#
-#COPY *.py *.txt /PheKnowLator/builds/
-#
-#WORKDIR /PheKnowLator/builds/
-#RUN pip install -r build_requirements.txt
-#
-#ENTRYPOINT ["python", "build_phase_3.py"]
-
-FROM ubuntu:latest
-
-RUN echo "Hello world"
\ No newline at end of file
diff --git a/builds/Dockerfile.phase3 b/builds/Dockerfile.phase3
new file mode 100755
index 000000000..cf2b3ecb7
--- /dev/null
+++ b/builds/Dockerfile.phase3
@@ -0,0 +1,53 @@
+#!/usr/local/bin/docker
+# -*- version: 20.10.2 -*-
+
+# import phase 3 container
+FROM callahantiff/pheknowlator:latest
+
+# allow statements and log messages to immediately appear in logs
+ENV PYTHONUNBUFFERED True
+
+# set wroking directory in container
+WORKDIR /PheKnowLator/
+
+# download build dependencies and move them to the correct location
+RUN curl -O https://storage.googleapis.com/pheknowlator/current_build/dependencies/subclass_construction_map.pkl && mv subclass_construction_map.pkl resources/construction_approach/
+RUN curl -O https://storage.googleapis.com/pheknowlator/current_build/dependencies/PheKnowLator_MergedOntologies.owl && mv PheKnowLator_MergedOntologies.owl resources/knowledge_graphs/
+RUN curl -O https://storage.googleapis.com/pheknowlator/current_build/dependencies/node_metadata_dict.pkl && mv node_metadata_dict.pkl resources/node_data/
+RUN curl -O https://storage.googleapis.com/pheknowlator/current_build/data/processed_data/DISEASE_MONDO_MAP.txt && mv DISEASE_MONDO_MAP.txt resources/processed_data/
+RUN curl -O https://storage.googleapis.com/pheknowlator/current_build/data/processed_data/ENSEMBL_GENE_ENTREZ_GENE_MAP.txt && mv ENSEMBL_GENE_ENTREZ_GENE_MAP.txt resources/processed_data/
+RUN curl -O https://storage.googleapis.com/pheknowlator/current_build/data/processed_data/ENTREZ_GENE_PRO_ONTOLOGY_MAP.txt && mv ENTREZ_GENE_PRO_ONTOLOGY_MAP.txt resources/processed_data/
+RUN curl -O https://storage.googleapis.com/pheknowlator/current_build/data/processed_data/GENE_SYMBOL_ENSEMBL_TRANSCRIPT_MAP.txt && mv GENE_SYMBOL_ENSEMBL_TRANSCRIPT_MAP.txt resources/processed_data/
+RUN curl -O https://storage.googleapis.com/pheknowlator/current_build/data/processed_data/HPA_GTEx_TISSUE_CELL_MAP.txt && mv HPA_GTEx_TISSUE_CELL_MAP.txt resources/processed_data/
+RUN curl -O https://storage.googleapis.com/pheknowlator/current_build/data/processed_data/MESH_CHEBI_MAP.txt && mv MESH_CHEBI_MAP.txt resources/processed_data/
+RUN curl -O https://storage.googleapis.com/pheknowlator/current_build/data/processed_data/PHENOTYPE_HPO_MAP.txt && mv PHENOTYPE_HPO_MAP.txt resources/processed_data/
+RUN curl -O https://storage.googleapis.com/pheknowlator/current_build/data/processed_data/STRING_PRO_ONTOLOGY_MAP.txt && mv STRING_PRO_ONTOLOGY_MAP.txt resources/processed_data/
+RUN curl -O https://storage.googleapis.com/pheknowlator/current_build/data/processed_data/UNIPROT_ACCESSION_PRO_ONTOLOGY_MAP.txt && mv UNIPROT_ACCESSION_PRO_ONTOLOGY_MAP.txt resources/processed_data/
+RUN curl -O https://storage.googleapis.com/pheknowlator/current_build/dependencies/INVERSE_RELATIONS.txt && mv INVERSE_RELATIONS.txt resources/relations_data/
+RUN curl -O https://storage.googleapis.com/pheknowlator/current_build/dependencies/RELATIONS_LABELS.txt && mv RELATIONS_LABELS.txt resources/relations_data/
+
+# copying local scripts to container
+COPY build_phase_3.py /PheKnowLator/
+COPY phase3_log_daemon.py build_utilities.py build_requirements.txt logging.ini /PheKnowLator/builds/
+COPY logging.ini /PheKnowLator/pkt_kg
+
+# install requirements
+RUN pip install -r builds/build_requirements.txt
+
+# set OWlTools memory (set to a high value, system will only use available memory)
+ENV OWLTOOLS_MEMORY=100g
+RUN echo $OWLTOOLS_MEMORY
+
+# set working directory and python path
+WORKDIR /PheKnowLator/
+ENV PYTHONPATH='.'
+
+# run script
+ARG app_arg
+ENV app=$app_arg
+ARG rel_arg
+ENV rel=$rel_arg
+ARG owl_arg
+ENV owl=$owl_arg
+
+ENTRYPOINT exec python build_phase_3.py --app $app --rel $rel --owl $owl
diff --git a/builds/Dockerfile.phases12 b/builds/Dockerfile.phases12
new file mode 100755
index 000000000..bea547645
--- /dev/null
+++ b/builds/Dockerfile.phases12
@@ -0,0 +1,32 @@
+#!/usr/local/bin/docker
+# -*- version: 20.10.2 -*-
+
+# import phase 3 container
+FROM callahantiff/pheknowlator:latest
+
+# allow statements and log messages to immediately appear in logs
+ENV PYTHONUNBUFFERED True
+
+# copying local scripts to container
+COPY *.py *.txt *.ini /PheKnowLator/builds/
+
+# set working directory in container
+WORKDIR /PheKnowLator/builds/
+
+# download
+RUN wget https://github.com/callahantiff/PheKnowLator/raw/master/pkt_kg/libs/owltools && \
+    chmod 755 owltools
+
+# set working directory and install requirements
+WORKDIR /PheKnowLator/
+RUN pip install -r builds/build_requirements.txt pkt_kg
+
+# set OWlTools memory (set to a high value, system will only use available memory)
+ENV OWLTOOLS_MEMORY=100g
+RUN echo $OWLTOOLS_MEMORY
+
+# set python envrionment encoding
+RUN export PYTHONIOENCODING=utf-8
+ENV PYTHONPATH='.'
+
+ENTRYPOINT ["python", "builds/phases1_2_entrypoint.py"]
\ No newline at end of file
diff --git a/builds/README.md b/builds/README.md
old mode 100644
new mode 100755
index 0bbd5a0f2..a44bed43b
--- a/builds/README.md
+++ b/builds/README.md
@@ -1,57 +1,85 @@
 # PheKnowLator Builds  
 **Current Release:** [`v2.0.0`](https://github.com/callahantiff/PheKnowLator/wiki/v2.0.0)  
 
-This directory stores the scripts and files utilized by GitHub Actions to perform the monthly knowledge graph builds. As described below the CI/CD Workflow consists of three primary phases, each which serves to process a different component of the full build process. Each of these phases and their triggers and associated scripts are briefly outlines below.
+This directory stores the scripts and files needed for the automatic monthly builds. Our continuous integration (CI)
+/Continuous deployment (CD) pipeline is managed entirely using [GitHub Actions](https://github.com/actions) and [Google's AI-Platform](https://cloud.google.com/ai-platform). Builds are triggered on the first of each month and consist of three separate asynchronous phases (each phase is briefly described below):  
+1. Data Download 
+2. Data Processing and Quality Control   
+3. Knowledge Graph Construction  
 
-## Build Phases  
-### Phase 1: Download Build Data  
+*NOTE.* Build Progress can be monitored (during the build) via the `pkt_builder_logs.log` file in the `current_build` directory of the current release in the dedicated project [Google Cloud Storage Bucket](https://console.cloud.google.com/storage/browser/pheknowlator?project=pheknowlator). Once a build is complete, the log is moved to the dataed build directory under the `archived_builds` sub-directory.
+ 
+## Phase 1: Download Build Data 
 **Script(s):** `build_phase_1.py`  
 
 This phase is triggered on the same date each month and consists of the following three steps:    
-1. **Create Google Cloud Storage Bucket:** To store the build files, a new Google Cloud Storage bucket under the current release and named with the current date is created (e.g. `release_v2.0.0/build_27DEC2020`). Under this directory, the rest of the needed build directories are created. See example below:
+1. **Create Google Cloud Storage Bucket:** Current builds are stored in a new Google Cloud Storage bucket under the current release in a directory called `current_build` (e.g. `release_v2.0.0/current_build`). Each subsequent build is also archived and saved with the build date under the `archived_builds` directory for the current release (e.g. `release_v2.0.0/archived_builds/build_27DEC2020`). Under this directory, the rest of the needed build directories are created. See example below:
    ```
-    |---- *build_<<date>>/
-    |     |---- data/
-    |     |     |---- original_data/
-    |     |     |---- processed_data/   
-    |     |     |---- knowledge_graphs/  
-    |     |     |     |---- subclass_builds/
-    |     |     |     |     |---- relations_only/
-    |     |     |     |     |     |---- owl/
-    |     |     |     |     |     |---- owlnets/     
-    |     |     |     |     |---- inverse_relations/
-    |     |     |     |     |     |---- owl/
-    |     |     |     |     |     |---- owlnets/     
-    |     |     |     |---- instance_builds/
-    |     |     |     |     |---- relations_only/
-    |     |     |     |     |     |---- owl/
-    |     |     |     |     |     |---- owlnets/     
-    |     |     |     |     |---- inverse_relations/
-    |     |     |     |     |     |---- owl/
-    |     |     |     |     |     |---- owlnets/    
+   curated_data/
+   current_build/
+   |---- data/
+   |     |---- original_data/
+   |     |---- processed_data/   
+   |---- knowledge_graphs/  
+   |     |---- subclass_builds/
+   |     |     |---- relations_only/
+   |     |     |     |---- owl/
+   |     |     |     |---- owlnets/     
+   |     |     |---- inverse_relations/
+   |     |     |     |---- owl/
+   |     |     |     |---- owlnets/     
+   |     |---- instance_builds/
+   |     |     |---- relations_only/
+   |     |     |     |---- owl/
+   |     |     |     |---- owlnets/     
+   |     |     |---- inverse_relations/
+   |     |     |     |---- owl/
+   |     |     |     |---- owlnets/
+   archived_builds
+   |---- release_v*.0.0/
+   |     |---- *build_<<date>>/
+   |     |     |---- data/
+   |     |     |     |---- original_data/
+   |     |     |     |---- processed_data/   
+   |     |     |---- knowledge_graphs/  
+   |     |     |     |---- subclass_builds/
+   |     |     |     |     |---- relations_only/
+   |     |     |     |     |     |---- owl/
+   |     |     |     |     |     |---- owlnets/     
+   |     |     |     |     |---- inverse_relations/
+   |     |     |     |     |     |---- owl/
+   |     |     |     |     |     |---- owlnets/     
+   |     |     |     |---- instance_builds/
+   |     |     |     |     |---- relations_only/
+   |     |     |     |     |     |---- owl/
+   |     |     |     |     |     |---- owlnets/     
+   |     |     |     |     |---- inverse_relations/
+   |     |     |     |     |     |---- owl/
+   |     |     |     |     |     |---- owlnets/   
+      
    ```
+   <<date>> is the date of download.  
+   curated_data is a directory of hand-curated resources utilized in the builds.  
+   
 2. **Download Knowledge Graph Data:** Using the `data_to_download.txt` text file, all needed build data are downloaded to the `data/original_data` directory associated with the current build. In this file, each source to download is provided as a single URL per row.   
 3. **Upload Local Build Data:** Generate `download_metadata.txt` a document that lives in the `original_data` directory and provides provenance information on each downloaded data source.
 
 <br>
 
-### Phase 2: Preprocess Downloaded Build Data     
+## Phase 2: Preprocess Downloaded Build Data       
 **Script(s):** `build_phase_2.py`; `pkt_kg/data_preprocessing.py`; `pkt_kg/ontology_cleaning.py`   
 **Data:** `genomic_typing_dict.pkl`
 
 This phase is triggered upon the successful completion of [Phase 1](#Phase-1:-Download-Build-Data) and consists of the following three steps:  
 1. **Preprocess Linked Open Data:** Runs the `pkt_kg/data_preprocessing.py` script, which preprocesses and prepares all Linked Open Data sources (i.e. non-ontology data) needed to the build the knowledge graphs. The cleaned data are output to the `data/processed_data` directory associated with the current build along with a metadata document providing provenance information on the preprocessed and cleaned documents.      
 2. **Preprocess Ontology Data:** Runs the `pkt_kg/ontology_cleaning.py` script, which preprocesses and prepares all ontology data needed to the build the knowledge graphs. The cleaned data are output to the `data/processed_data` directory associated with the current build along with a metadata document providing provenance information on the preprocessed and cleaned documents.  
-3. **Update Input Build Dependencies:** The URLS referenced in the knowledge graph [input dependency documents](https://github.com/callahantiff/PheKnowLator/wiki/Dependencies) `resource_info.txt`, `edge_source_list.txt`, and `ontology_source_list.txt` documents are updated with the Google Cloud Storage bucket URLs for each associated 
-   preprocessed document located in the `data/processed_data` directory associated with the current build.
-   
+3. **Update Input Build Dependencies:** The URLS referenced in the knowledge graph [input dependency documents](https://github.com/callahantiff/PheKnowLator/wiki/Dependencies) `resource_info.txt`, `edge_source_list.txt`, and `ontology_source_list.txt` documents are updated with the Google Cloud Storage bucket URLs for each associated preprocessed document located in the `data/processed_data` directory associated with the current build.  
 4. **Upload Local Build Data:** Generate `preprocessed_build_metadata.txt` a document that lives in the 
-   `processed_data` directory and provides provenance information on each downloaded data source. Also uploads the 
-   ontology data cleaning results (`ontology_cleaning_report.txt`), which provides additional insight into the errors that were cleaned for each ontology.
+   `processed_data` directory and provides provenance information on each downloaded data source. Also uploads the ontology data cleaning results (`ontology_cleaning_report.txt`), which provides additional insight into the errors that were cleaned for each ontology.
 
 <br>
 
-### Phase 3: Build Knowledge Graph  
+## Phase 3: Build Knowledge Graph    
 **Script(s):** `build_phase_3.py`; `complete_build.py`  
 
 This phase is triggered by the successful completion of [Phase 2](#Phase-2:-Preprocess-Downloaded-Build-Data) and is the primary step responsible for constructing the PheKnowLator knowledge graphs and consists of the following seven steps:  
@@ -59,11 +87,23 @@ This phase is triggered by the successful completion of [Phase 2](#Phase-2:-Prep
    `ontology_source_list.txt` documents updated in [Phase 2](#Phase-2:-Preprocess-Downloaded-Build-Data) are downloaded locally and committed to the Master branch of the GitHub repository.     
 2. **Downloads Processed Data:** All data processed during [Phase 2](#Phase-2:-Preprocess-Downloaded-Build-Data) are downloaded in preparation of constructing the build Docker container.  
 3. **Build Docker Container:** The primary build Docker container is built and published to Docker Hub.  
-4. **Container Parameterization and Deployment:** GitHub Actions communicates with 
-   Google Cloud Run to duplicate the constructor container and parameterize it for each of the PheKnowLator builds allowing for the knowledge graphs to be constructed in parallel.  
-5. **Completes Build:** Waits for each container to complete and then uploads associated data to the correct Google Cloud Storage bucket associated with the current build.
-6. **Update SPARQL Endpoint:** The successfully built knowledge graphs are pushed to the GraphDB SPARQL Endpoint for 
-   public consumption (*<<TODO: ADD ENDPOINT LINK HERE>>*)   
+4. **Container Parameterization and Deployment:** GitHub Actions communicates with Google Cloud Run to duplicate the constructor container and parameterize it for each of the PheKnowLator builds allowing for the knowledge graphs to be constructed in parallel.  
+5. **Completes Build:** Waits for each container to complete and then uploads associated data to the correct Google Cloud Storage bucket associated with the current build. The table below maps the names of each [GitHub Action Workflow](https://github.com/callahantiff/PheKnowLator/blob/master/.github/workflows/kg-build.yml) job to each build type.
+6. **Update Public Endpoints:** After a successful build, knowledge graphs are pushed to:   
+    - Blazegraph SPARQL Endpoint: [http://sparql.pheknowlator.com](http://sparql.pheknowlator.com/)  
+    - Neo4J Endpoint and User Interface: [http://neo4j.pheknowlator.com]()  -- *COMING SOON*   
+
+**GitHUb Actions - Phase 3 Build Job Names**
+GitHub   Action Job | Job Name | Construction   Approach | Relations | OWL Decoding
+:--: | -- | :--: | :--: | :--:
+1 | Phase   3 - Job 1 (Subclass + RelationsOnly + OWL) | Subclass | Relations   Only | OWL
+2 | Phase   3 - Job 1 (Subclass + RelationsOnly + No OWL) | Subclass | Relations   Only | No   OWL
+3 | Phase   3 - Job 1 (Subclass +InverseRelations + OWL) | Subclass | Inverse   Relations | OWL
+4 | Phase   3 - Job 1 (Subclass + InverseRelations + No OWL) | Subclass | Inverse   Relations | No   OWL
+5 | Phase   3 - Job 1 (Instance + RelationsOnly + OWL) | Instance | Relations   Only | OWL
+6 | Phase   3 - Job 1 (Instance + RelationsOnly + No OWL) | Instance | Relations   Only | No   OWL
+7 | Phase   3 - Job 1 (Instance +InverseRelations + OWL) | Instance | Inverse   Relations | OWL
+8 | Phase   3 - Job 1 (Instance + InverseRelations + No OWL) | Instance | Inverse   Relations | No   OWL
 
 ____
 
@@ -71,4 +111,4 @@ ____
 - The contents in this directory are updated for every release and as needed to prepare bugs and unexpected issues. Any important to changes to the workflow and associated scripts will be documented her as they arise.
 
 - Any issues that may arise during any of the Phases described above will be pushed to a build log and saved under the current build's directory in the Google Cloud Storage bucket.  
-  
\ No newline at end of file
+  
diff --git a/builds/__init__.py b/builds/__init__.py
new file mode 100755
index 000000000..e69de29bb
diff --git a/builds/build_phase_1.py b/builds/build_phase_1.py
old mode 100644
new mode 100755
index 8c63aeccc..638b8acfc
--- a/builds/build_phase_1.py
+++ b/builds/build_phase_1.py
@@ -2,26 +2,40 @@
 # -*- coding: utf-8 -*-
 
 # import needed libraries
+import fnmatch
+import glob
+import logging.config
 import os
 import re
 import shutil
+import sys
+import traceback
 
 from datetime import date, datetime
 from google.cloud import storage  # type: ignore
 
+from builds.build_utilities import *  # type: ignore
 from pkt_kg.__version__ import __version__
 from pkt_kg.utils import data_downloader
 
-# set environment variable -- this should be replaced with GitHub Secret for build
-os.environ['GOOGLE_APPLICATION_CREDENTIALS'] = 'resources/project_keys/pheknowlator-6cc612b4cbee.json'
+# set environment variables
+# logging
+log_dir, log, log_config = 'builds/logs', 'pkt_builder_phases12_log.log', glob.glob('**/logging.ini', recursive=True)
+if not os.path.exists(log_dir): os.mkdir(log_dir)
+logger = logging.getLogger(__name__)
+logging.config.fileConfig(log_config[0], disable_existing_loggers=False, defaults={'log_file': log_dir + '/' + log})
+# owl tools location
+owltools_location = './builds/owltools'
+# owltools_location = './pkt_kg/libs/owltools'
 
 
-def creates_build_directory_structure(bucket, release):
+def creates_build_directory_structure(bucket, release, build):
     """Takes a Google Cloud Storage bucket and generates all of the folders needed for a knowledge graph build.
 
     Args:
         bucket: A storage Bucket object specifying a Google Cloud Storage bucket.
         release: A string containing the current pkt software release.
+        build: A string containing the name of the current build.
 
     Returns:
          original_data: A string containing the Google Cloud Storage bucket directory to download data to for the
@@ -29,20 +43,20 @@ def creates_build_directory_structure(bucket, release):
     """
 
     print('Creating Build Directory Structure')
+    logger.info('Creating Build Directory Structure')
 
     # set-up file names
-    build = 'build_' + datetime.strftime(datetime.strptime(str(date.today()), '%Y-%m-%d'), '%d%b%Y').upper()
     folder_list = [
-        '{}/{}/data/original_data/'.format(release, build),
-        '{}/{}/data/processed_data/'.format(release, build),
-        '{}/{}/knowledge_graphs/subclass_builds/relations_only/owl/'.format(release, build),
-        '{}/{}/knowledge_graphs/subclass_builds/relations_only/owlnets/'.format(release, build),
-        '{}/{}/knowledge_graphs/subclass_builds/inverse_relations/owl/'.format(release, build),
-        '{}/{}/knowledge_graphs/subclass_builds/inverse_relations/owlnets/'.format(release, build),
-        '{}/{}/knowledge_graphs/instance_builds/relations_only/owl/'.format(release, build),
-        '{}/{}/knowledge_graphs/instance_builds/relations_only/owlnets/'.format(release, build),
-        '{}/{}/knowledge_graphs/instance_builds/inverse_relations/owl/'.format(release, build),
-        '{}/{}/knowledge_graphs/instance_builds/inverse_relations/owlnets/'.format(release, build)
+        'archived_builds/{}/{}/data/original_data/'.format(release, build),
+        'archived_builds/{}/{}/data/processed_data/'.format(release, build),
+        'archived_builds/{}/{}/knowledge_graphs/subclass_builds/relations_only/owl/'.format(release, build),
+        'archived_builds/{}/{}/knowledge_graphs/subclass_builds/relations_only/owlnets/'.format(release, build),
+        'archived_builds/{}/{}/knowledge_graphs/subclass_builds/inverse_relations/owl/'.format(release, build),
+        'archived_builds/{}/{}/knowledge_graphs/subclass_builds/inverse_relations/owlnets/'.format(release, build),
+        'archived_builds/{}/{}/knowledge_graphs/instance_builds/relations_only/owl/'.format(release, build),
+        'archived_builds/{}/{}/knowledge_graphs/instance_builds/relations_only/owlnets/'.format(release, build),
+        'archived_builds/{}/{}/knowledge_graphs/instance_builds/inverse_relations/owl/'.format(release, build),
+        'archived_builds/{}/{}/knowledge_graphs/instance_builds/inverse_relations/owlnets/'.format(release, build)
     ]
 
     # add directories to Google Cloud Storage bucket
@@ -50,29 +64,7 @@ def creates_build_directory_structure(bucket, release):
         blob = bucket.blob(folder)
         blob.upload_from_string('')
 
-    return '{}/{}/data/original_data/'.format(release, build)
-
-
-def uploads_data_to_gcs_bucket(bucket, original_data, temp_directory, filename):
-    """Takes a file name and pushes the data referenced by the filename object and stored locally in that object to
-    a Google Cloud Storage bucket.
-
-    Args:
-        bucket: A storage Bucket object specifying a Google Cloud Storage bucket.
-        original_data: A string specifying the location of the original_data directory for a specific build.
-        temp_directory: A local directory where preprocessed data is stored.
-        filename: A string containing a filename.
-
-    Returns:
-        None.
-    """
-
-    print('Uploading {} to GCS bucket: {}'.format(filename, original_data))
-
-    blob = bucket.blob(original_data + filename)
-    blob.upload_from_filename(temp_directory + '/' + filename)
-
-    return None
+    return 'archived_builds/{}/{}/data/original_data/'.format(release, build)
 
 
 def get_file_metadata(url, file_location, gcs_url):
@@ -113,7 +105,6 @@ def writes_metadata(metadata, bucket, original_data, temp_directory):
     """
 
     filename = 'downloaded_build_metadata.txt'
-
     with open(temp_directory + '/' + filename, 'w') as out:
         out.write('=' * 35 + '\n{}\n'.format(str(datetime.utcnow().strftime('%a %b %d %X UTC %Y'))) + '=' * 35 + '\n\n')
         for row in metadata:
@@ -125,7 +116,7 @@ def writes_metadata(metadata, bucket, original_data, temp_directory):
     return None
 
 
-def downloads_build_data(bucket, original_data, gcs_url, temp_directory, file_loc='./builds/data_to_download.txt'):
+def downloads_build_data(bucket, original_data, gcs_url, temp_directory, file_loc='builds/data_to_download.txt'):
     """Reads in the list of data to download for the current build, downloads each object, and pushes the downloaded
     object up to a Google Cloud Storage bucket. Once all of the data are downloaded, a metadata file object is
     generated and pushed with the downloaded data to the original_data Google Cloud Storage bucket for the current
@@ -144,19 +135,37 @@ def downloads_build_data(bucket, original_data, gcs_url, temp_directory, file_lo
     """
 
     print('Downloading Build Data')
+    logger.info('Downloading Build Data')
 
+    # check what data has already been downloaded
+    gcs_original_path = '/'.join(gcs_url.split('/')[4:-1])
+    downloaded_data = [file.name for file in bucket.list_blobs(prefix=gcs_original_path)]
     urls, metadata = [x.strip('\n') for x in open(file_loc, 'r').readlines() if not x.startswith('#') and x != '\n'], []
     for url in urls:
+        print('Downloading {}'.format(url))
+        logger.info('Downloading {}'.format(url))
         if url.startswith('http://purl.obolibrary.org/obo/'):
-            file_path = temp_directory + '/' + url.split('/')[-1][:-4] + '_with_imports.owl'
-            os.system("./pkt_kg/libs/owltools {} --merge-import-closure -o {}".format(url, file_path))
+            ont_name = url.split('/')[-1][:-4] + '_with_imports.owl'
+            file_path = temp_directory + '/' + ont_name
+            if len([x for x in downloaded_data if x.endswith(ont_name)]) > 0:
+                downloads_data_from_gcs_bucket(bucket, gcs_original_path, None, ont_name, temp_directory)
+            else:
+                command_string = '{} {} --merge-import-closure -o {}'
+                return_code = os.system(command_string.format(owltools_location, url, file_path))
+                if return_code != 0:
+                    logger.error('ERROR: Unable to successfully download {}: {}'.format(url, return_code))
+                    raise Exception('ERROR: Unable to successfully download {}'.format(url))
         else:
             filename, url = url.split(', ')
             file_path = temp_directory + '/' + re.sub('.zip|.gz', '', filename)
-            data_downloader(url, temp_directory + '/', filename)
+            if len([x for x in downloaded_data if x.endswith(file_path)]) > 0:
+                downloads_data_from_gcs_bucket(bucket, gcs_original_path, None, file_path, temp_directory)
+            else: data_downloader(url, temp_directory + '/', filename)
+
         metadata += [get_file_metadata(url, file_path, gcs_url)]
         f_name = re.sub('.zip|.gz', '', file_path.replace(temp_directory + '/', ''))
         uploads_data_to_gcs_bucket(bucket, original_data, temp_directory, f_name)
+        uploads_data_to_gcs_bucket(bucket, 'current_build/', log_dir, log)
 
     # writes metadata locally and pushes it to a Google Cloud Storage bucket
     writes_metadata(metadata, bucket, original_data, temp_directory)
@@ -164,9 +173,7 @@ def downloads_build_data(bucket, original_data, gcs_url, temp_directory, file_lo
     return None
 
 
-def main():
-
-    print('#' * 35 + '\nBUILD PHASE 1: DOWNLOADING BUILD DATA\n' + '#' * 35)
+def run_phase_1():
 
     # create temp directory to use locally for writing data GCS data to
     temp_dir = 'builds/temp'
@@ -174,18 +181,14 @@ def main():
 
     ###############################################
     # STEP 1 - INITIALIZE GOOGLE STORAGE BUCKET OBJECTS
-    storage_client = storage.Client()
-    bucket = storage_client.get_bucket('pheknowlator')
-    release = 'release_v' + __version__
-
-    # create gcs bucket directories
-    gcs_original_data = creates_build_directory_structure(bucket, release)
+    bucket = storage.Client().get_bucket('pheknowlator')
+    build = 'build_' + datetime.strftime(datetime.strptime(str(date.today()), '%Y-%m-%d'), '%d%b%Y').upper()
+    gcs_original_data = creates_build_directory_structure(bucket, 'release_v' + __version__, build)
+    uploads_data_to_gcs_bucket(bucket, 'current_build/', log_dir, log)
 
     ###############################################
     # STEP 2 - DOWNLOAD BUILD DATA
-    gcs_url = 'https://storage.googleapis.com/pheknowlator/{}/{}/data/original_data/'.format(release, build)
+    gcs_url = 'https://storage.googleapis.com/pheknowlator/{}'.format(gcs_original_data)
     downloads_build_data(bucket, gcs_original_data, gcs_url, temp_dir)
 
-
-if __name__ == '__main__':
-    main()
+    return None
diff --git a/builds/build_phase_2.py b/builds/build_phase_2.py
old mode 100644
new mode 100755
index 2ecce9ed0..cfa0f5a36
--- a/builds/build_phase_2.py
+++ b/builds/build_phase_2.py
@@ -4,20 +4,27 @@
 # import needed libraries
 import fnmatch
 import glob
+import logging.config
 import os
 import shutil
 import re
 
 from datetime import datetime
 from google.cloud import storage  # type: ignore
+from tqdm import tqdm  # type: ignore
 
-from builds.data_preprocessing import DataPreprocessing  # type: ignore
-from builds.ontology_cleaning import OntologyCleaner  # type: ignore
+from builds.build_utilities import *
+from builds.data_preprocessing import DataPreprocessing
+from builds.ontology_cleaning import OntologyCleaner
 from pkt_kg.__version__ import __version__
 from pkt_kg.utils import data_downloader
 
-# set environment variable -- this should be replaced with GitHub Secret for build
-os.environ['GOOGLE_APPLICATION_CREDENTIALS'] = 'resources/project_keys/pheknowlator-6cc612b4cbee.json'
+# set environment variables
+# logging
+log_dir, log, log_config = 'builds/logs', 'pkt_builder_phases12_log.log', glob.glob('**/logging.ini', recursive=True)
+if not os.path.exists(log_dir): os.mkdir(log_dir)
+logger = logging.getLogger(__name__)
+logging.config.fileConfig(log_config[0], disable_existing_loggers=False, defaults={'log_file': log_dir + '/' + log})
 
 
 def get_file_metadata(url, file_location, gcs_url):
@@ -43,14 +50,16 @@ def get_file_metadata(url, file_location, gcs_url):
     return metadata
 
 
-def writes_metadata(metadata, temp_directory):
+def writes_metadata(bucket, metadata, temp_directory, gcs_processed_location):
     """Writes the downloaded URL metadata information locally and to the original_data directory in Google Cloud
     Storage bucket for the current build.
 
     Args:
+        bucket: A storage bucket object specifying a Google Cloud Storage bucket.
         metadata: A nested list containing metadata information on each downloaded url.
         temp_directory: A local directory where preprocessed data is stored.
-        metadata: A nested list containing metadata information on each downloaded url.
+        gcs_processed_location: A string containing a path to a directory in the Google Cloud Storage Bucket for the
+            current build.
 
     Returns:
         None.
@@ -58,13 +67,13 @@ def writes_metadata(metadata, temp_directory):
 
     filename = 'preprocessed_build_metadata.txt'
     with open(temp_directory + '/' + filename, 'w') as out:
-        out.write('=' * 35 + '\n{}\n'.format(str(datetime.utcnow().strftime('%a %b %d %X UTC %Y'))) +
-                  '=' * 35 + '\n\n')
+        date_info = str(datetime.utcnow().strftime('%a %b %d %X UTC %Y'))
+        out.write('=' * 35 + '\n{}\n'.format(date_info) + '=' * 35 + '\n\n')
         for row in metadata:
             for i in range(4):
                 out.write(str(row[i]) + '\n')
 
-    lod_data.uploads_data_to_gcs_bucket(filename)
+    uploads_data_to_gcs_bucket(bucket, gcs_processed_location, temp_directory, filename)
 
     return None
 
@@ -77,7 +86,7 @@ def updates_dependency_documents(gcs_url, file_url, bucket, temp_directory):
     Args:
         gcs_url: A string containing a URL to the current Google Cloud Storage bucket.
         file_url: A string containing a URL to a build dependency document.
-        bucket: A storage Bucket object specifying a Google Cloud Storage bucket.
+        bucket: A storage bucket object specifying a Google Cloud Storage bucket.
         temp_directory: A local directory where preprocessed data is stored.
 
     Returns:
@@ -88,12 +97,10 @@ def updates_dependency_documents(gcs_url, file_url, bucket, temp_directory):
     bucket_path = '/'.join(gcs_url.split('/')[4:])
     gcs_processed = [_.name.split('/')[-1] for _ in bucket.list_blobs(prefix=bucket_path + 'processed_data/')]
     gcs_original = [_.name.split('/')[-1] for _ in bucket.list_blobs(prefix=bucket_path + 'original_data/')]
-
     # download dependency file and read in data
     data_downloader(file_url, temp_directory + '/')
     filename = file_url.split('/')[-1]
     data = open(temp_directory + '/' + filename).readlines()
-
     # update file with current gcs bucket url
     if filename != 'resource_info.txt':
         with open(temp_directory + '/' + filename, 'w') as out:
@@ -112,50 +119,67 @@ def updates_dependency_documents(gcs_url, file_url, bucket, temp_directory):
                     d_file = fnmatch.filter(gcs_processed, suffix.split('/')[-1].replace('.', '*.'))
                     if len(d_file) > 0: out.write(prefix + ', ' + gcs_url + 'processed_data/' + d_file[0] + '\n')
 
-    # push data to bucket
-    lod_data.uploads_data_to_gcs_bucket(filename)
+    uploads_data_to_gcs_bucket(bucket, bucket_path + 'processed_data/', temp_directory, filename)
 
     return None
 
 
-def main():
-    print('#' * 35 + '\nBUILD PHASE 2: DATA PRE-PROCESSING\n' + '#' * 35)
+def run_phase_2():
 
-    temp_dir = 'temp'
+    # set temp directory to use locally for writing data to
+    temp_dir = 'builds/temp'
     if not os.path.exists(temp_dir): os.mkdir(temp_dir)
 
-    ###############################################
+    #####################################################
     # STEP 1 - INITIALIZE GOOGLE STORAGE BUCKET OBJECTS
-    storage_client = storage.Client()
-    bucket = storage_client.get_bucket('pheknowlator')
     # define write path to Google Cloud Storage bucket
+    bucket = storage.Client().get_bucket('pheknowlator')
     release = 'release_v' + __version__
-    bucket_files = [file.name.split('/')[2] for file in bucket.list_blobs(prefix='{}/archived_builds'.format(release))]
-    build = 'build_' + sorted([x[0] for x in [re.findall(r'(?<=_)\d.*', x) for x in bucket_files] if len(x) > 0])[-1]
-    gcs_original_data = '{}/archived_builds/{}/data/{}'.format(release, build, 'original_data/')
-    gcs_processed_data = '{}/archived_builds/{}/data/{}'.format(release, build, 'processed_data/')
-    gcs_url = 'https://storage.googleapis.com/pheknowlator/{}/{}/data/'.format(release, build)
+    bucket_files = [file.name.split('/')[2] for file in bucket.list_blobs(prefix='archived_builds/' + release + '/')]
+
+    # find current archived build directory
+    builds = [x[0] for x in [re.findall(r'(?<=_)\d.*', x) for x in bucket_files] if len(x) > 0]
+    sorted_dates = sorted([datetime.strftime(datetime.strptime(str(x), '%d%b%Y'), '%Y-%m-%d').upper() for x in builds])
+    build = 'build_' + datetime.strftime(datetime.strptime(sorted_dates[-1], '%Y-%m-%d'), '%d%b%Y').upper()
 
-    ###############################################
+    # set gcs bucket variables
+    base_folder_path = 'archived_builds/{}/{}/data/'.format(release, build)
+    gcs_original_data = base_folder_path + '{}'.format('original_data/')
+    gcs_processed_data = base_folder_path + '{}'.format('processed_data/')
+    gcs_current_build = 'current_build/'
+    gcs_url = 'https://storage.googleapis.com/pheknowlator/{}'.format(base_folder_path)
+
+    #####################################################
     # STEP 2 - PREPROCESS BUILD DATA
     lod_data = DataPreprocessing(bucket, gcs_original_data, gcs_processed_data, temp_dir)
     lod_data.preprocesses_build_data()
+    uploads_data_to_gcs_bucket(bucket, gcs_current_build, log_dir, log)
 
-    ###############################################
+    #####################################################
     # STEP 3 - CLEAN ONTOLOGY DATA
     ont_data = OntologyCleaner(bucket, gcs_original_data, gcs_processed_data, temp_dir)
     ont_data.cleans_ontology_data()
+    uploads_data_to_gcs_bucket(bucket, gcs_current_build, log_dir, log)
 
-    ###############################################
+    #####################################################
     # STEP 4 - GENERATE METADATA DOCUMENTATION
-    metadata, processed_data = [], glob.glob(temp_dir + '/*')
+    logger.info('Generating and Writing Preprocessed Data Metadata')
+    metadata, processed_data_files, processed_data = [], glob.glob(temp_dir + '/*'), []
+    gcs_processed_files = [f.name for f in bucket.list_blobs(prefix=gcs_processed_data)][1:]
+    if len(processed_data_files) != len(gcs_processed_files):
+        for _ in tqdm(gcs_processed_files):
+            bucket.blob(_).download_to_filename(temp_dir + '/' + _.split('/')[-1])
+            processed_data.append(temp_dir + '/' + _.split('/')[-1])
+    else: processed_data = processed_data_files
     for data_file in tqdm(processed_data):
         url = gcs_url + 'original_data/' + data_file.replace(temp_dir + '/', '')
         metadata += [get_file_metadata(url, data_file, gcs_url + 'processed_data/')]
-    writes_metadata(metadata, temp_dir)
+    writes_metadata(bucket, metadata, temp_dir, gcs_processed_data)
+    uploads_data_to_gcs_bucket(bucket, gcs_current_build, log_dir, log)
 
-    ###############################################
+    #####################################################
     # STEP 5 - UPDATE INPUT DEPENDENCY DOCUMENTS
+    logger.info('Updating Input Dependency Documents')
     # edge source list
     edge_src_list = 'https://raw.githubusercontent.com/callahantiff/PheKnowLator/master/resources/edge_source_list.txt'
     updates_dependency_documents(gcs_url, edge_src_list, bucket, temp_dir)
@@ -165,10 +189,30 @@ def main():
     # resource info
     resources = 'https://raw.githubusercontent.com/callahantiff/PheKnowLator/master/resources/resource_info.txt'
     updates_dependency_documents(gcs_url, resources, bucket, temp_dir)
+    uploads_data_to_gcs_bucket(bucket, gcs_current_build, log_dir, log)
+
+    #####################################################
+    # STEP 6 - PREPARE FOR PHASE 3
+    # ensures that all input dependencies needed for build phase 3 are uploaded to the current_build directory in GCS
+    logger.info('Uploading Input Dependency Documents to current_build Directory')
+    dep_dat = ['resource_info.txt', 'edge_source_list.txt', 'ontology_source_list.txt', 'node_metadata_dict.pkl',
+               'subclass_construction_map.pkl', 'INVERSE_RELATIONS.txt', 'RELATIONS_LABELS.txt',
+               'PheKnowLator_MergedOntologies.owl']
+    copies_data_between_gcs_bucket_directories(bucket, gcs_processed_data, gcs_current_build + 'dependencies/', dep_dat)
+    uploads_data_to_gcs_bucket(bucket, gcs_current_build, log_dir, log)
+
+    # update current_build data directory -- delete existing data
+    print('Clearing current_build/data Directory')
+    logger.info('Clearing current_build/data Directory')
+    deletes_bucket_files(bucket, gcs_current_build + 'data/')
+    bucket.blob(gcs_current_build + 'data/').upload_from_string('')  # re-creates the data bucket
+
+    # copy data from the archived_builds directory to the current_data directory
+    source_directory, destination_directory = base_folder_path, gcs_current_build + 'data/'
+    source_data = ['/'.join(_.name.split('/')[-2:]) for _ in bucket.list_blobs(prefix=source_directory)]
+    print('Copying Data FROM: {} TO: {}'.format(source_directory, destination_directory))
+    logger.info('Copying Data FROM: {} TO: {}'.format(source_directory, destination_directory))
+    copies_data_between_gcs_bucket_directories(bucket, source_directory, destination_directory, source_data)
+    uploads_data_to_gcs_bucket(bucket, gcs_current_build, log_dir, log)
 
-    # clean up environment after uploading all processed data
-    shutil.rmtree(temp_dir)
-
-
-if __name__ == '__main__':
-    main()
+    return None
diff --git a/builds/build_phase_3.py b/builds/build_phase_3.py
old mode 100644
new mode 100755
index ce98c5d4e..8976fffe6
--- a/builds/build_phase_3.py
+++ b/builds/build_phase_3.py
@@ -1,10 +1,173 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
 
-def main():
+# import needed libraries
+import click
+import fnmatch
+import glob
+import logging.config
+import os
+import ray
+import re
+import shutil
+import subprocess
+import traceback
 
-    print('Hello World\n\n')
+from datetime import datetime
+from google.cloud import storage  # type: ignore
 
-    with open('/output/test.txt', 'w') as o:
-        o.write('hello world')
+from builds.build_utilities import *  # type: ignore
+from builds.phase3_log_daemon import PKTLogUploader  # type: ignore
+from pkt_kg.__version__ import __version__  # type: ignore
+from pkt_kg.utils import *  # type: ignore
+
+# set environment variables
+# os.environ['GOOGLE_APPLICATION_CREDENTIALS'] = 'resources/project_keys/pheknowlator-6cc612b4cbee.json'
+# logging
+log_dir, log, log_config = 'builds/logs', 'pkt_build_log.log', glob.glob('**/logging.ini', recursive=True)
+if os.path.exists(log_dir): shutil.rmtree(log_dir)
+os.mkdir(log_dir)
+logger = logging.getLogger(__name__)
+logging.config.fileConfig(log_config[0], disable_existing_loggers=False, defaults={'log_file': log_dir + '/' + log})
+
+
+def uploads_build_data(bucket, gcs_location):
+    """Methods moves data generated by the knowledge graph construction process from the docker container to the
+    dedicated Google Cloud Storage Bucket for the current build.
+
+    Args:
+        bucket: A storage bucket object specifying a Google Cloud Storage bucket.
+        gcs_location: A string containing the location for the archive build in the current release directory
+            within the dedicated project Google Cloud Storage Bucket.
+
+    Returns:
+        None.
+    """
+
+    # create variables to store source directory locations
+    resources_loc = 'resources/'
+    kg_loc = resources_loc + 'knowledge_graphs/'
+    metadata_loc = resources_loc + 'node_data/'
+    construct_app = resources_loc + 'construction_approach/'
+
+    # move knowledge graph data
+    for kg_file in [x for x in glob.glob(kg_loc + '*') if 'README.md' not in x]:
+        uploads_data_to_gcs_bucket(bucket, gcs_location, kg_loc, kg_file.split('/')[-1])
+    # move master edge list
+    uploads_data_to_gcs_bucket(bucket, gcs_location, resources_loc, 'Master_Edge_List_Dict.json')
+    # move metadata files
+    uploads_data_to_gcs_bucket(bucket, gcs_location, resources_loc + 'edge_data/', 'edge_source_metadata.txt')
+    uploads_data_to_gcs_bucket(bucket, gcs_location, resources_loc + 'ontologies/', 'ontology_source_metadata.txt')
+    # node metadata dict
+    uploads_data_to_gcs_bucket(bucket, gcs_location, metadata_loc, 'node_metadata_dict.pkl')
+    # construction approach logs
+    uploads_data_to_gcs_bucket(bucket, gcs_location, construct_app, 'subclass_map_missing_node_log.json')
+
+    return None
+
+
+@click.command()
+@click.option('--app', prompt='construction approach to use (i.e. instance or subclass)')
+@click.option('--rel', prompt='yes/no - adding inverse relations to knowledge graph')
+@click.option('--owl', prompt='yes/no - removing OWL Semantics from knowledge graph')
+def main(app, rel, owl):
+
+    print('#' * 35 + '\nBUILD PHASE 3: DATA PRE-PROCESSING\n' + '#' * 35)
+    logger.info('#' * 5 + 'BUILD PHASE 3: DATA PRE-PROCESSING' + '#' * 5)
+    start_time = datetime.now()
+
+    #############################################################################
+    # STEP 1 - INITIALIZE GOOGLE STORAGE BUCKET OBJECTS
+    print('\nSTEP 1: INITIALIZE GOOGLE STORAGE BUCKET AND REFORMAT INPUT ARGUMENTS')
+    logger.info('STEP 1: INITIALIZE GOOGLE STORAGE BUCKET AND REFORMAT INPUT ARGUMENTS')
+
+    # set bucket information and find current archived build directory
+    release = 'release_v' + __version__
+    bucket = storage.Client().get_bucket('pheknowlator')
+    bucket_files = [file.name.split('/')[2] for file in bucket.list_blobs(prefix='archived_builds/' + release + '/')]
+    builds = [x[0] for x in [re.findall(r'(?<=_)\d.*', x) for x in bucket_files] if len(x) > 0]
+    sorted_dates = sorted([datetime.strftime(datetime.strptime(str(x), '%d%b%Y'), '%Y-%m-%d').upper() for x in builds])
+    build = 'build_' + datetime.strftime(datetime.strptime(sorted_dates[-1], '%Y-%m-%d'), '%d%b%Y').upper()
+
+    # reformat input arguments and create gcs directory variables
+    build_app = 'instance_builds' if app == 'instance' else 'subclass_builds'
+    rel_type = 'relations_only' if rel == 'no' else 'inverse_relations'
+    owl_decoding = 'owl' if owl == 'no' else 'owlnets'
+    arch_string = 'archived_builds/{}/{}/knowledge_graphs/{}/{}/{}/'
+    gcs_archive_loc = arch_string.format(release, build, build_app, rel_type, owl_decoding)
+    gcs_current_loc = 'current_build/knowledge_graphs/{}/{}/{}/'.format(build_app, rel_type, owl_decoding)
+
+    # clean out directory before writing new data
+    print('Removing Existing Data from Build Directory on Google Cloud Storage')
+    logging.info('Removing Existing Data from Build Directory on Google Cloud Storage')
+    deletes_bucket_files(bucket, gcs_current_loc)
+    bucket.blob(gcs_current_loc).upload_from_string('')
+
+    uploads_data_to_gcs_bucket(bucket, gcs_current_loc, log_dir, log)  # uploads log to gcs bucket
+
+    #############################################################################
+    # STEP 2 - CONSTRUCT KNOWLEDGE GRAPH
+    print('\nSTEP 2: CONSTRUCT KNOWLEDGE GRAPH')
+    print('Knowledge Graph Build: {} + {} + {}.txt'.format(app, rel_type.lower(), owl_decoding.lower()))
+    logger.info('STEP 2: CONSTRUCT KNOWLEDGE GRAPH')
+    logger.info('Knowledge Graph Build: {} + {} + {}.txt'.format(app, rel_type.lower(), owl_decoding.lower()))
+    # start background process to upload logs while the pkt main knowledge graph function runs
+    ray.init()
+    background_task = PKTLogUploader.remote('pheknowlator', gcs_current_loc, log_dir, 90)
+    logger.info('RAN THE CONSTRUCT KNOWLEDGE GRAPH CODE')
+    # run the pkt_kg main method
+    command = 'python Main.py --onts resources/ontology_source_list.txt --edg resources/edge_source_list.txt ' \
+              '--res resources/resource_info.txt --out ./resources/knowledge_graphs --nde yes --kg full ' \
+              '--app {} --rel {} --owl {}'
+    try:
+        return_code = os.system(command.format(app, rel, owl))
+        if return_code != 0:
+            logger.error('ERROR: Program Finished with Errors: {}'.format(return_code))
+            raise Exception('ERROR: Program Finished with Errors: {}'.format(return_code))
+    except: logger.error('ERROR: Uncaught Exception: {}'.format(traceback.format_exc()))
+    background_task.__ray_terminate__.remote()  # kills the process with an `exit(0)`
+    ray.shutdown()
+
+    uploads_data_to_gcs_bucket(bucket, gcs_current_loc, log_dir, log)  # uploads log to gcs bucket
+
+    #############################################################################
+    # STEP 3 - UPLOAD BUILD DATA TO GOOGLE CLOUD STORAGE
+    print('\nSTEP 3: UPLOAD KNOWLEDGE GRAPH DATA TO GOOGLE CLOUD STORAGE')
+    logger.info('STEP 3: UPLOAD KNOWLEDGE GRAPH DATA TO GOOGLE CLOUD STORAGE')
+
+    # upload data from Docker to archived_builds Google Cloud Storage Bucket
+    print('Uploading Knowledge Graph Data to the current_build Directory')
+    logger.info('Uploading Knowledge Graph Data to the current_build Directory')
+    uploads_build_data(bucket, gcs_current_loc)
+    uploads_data_to_gcs_bucket(bucket, gcs_current_loc, log_dir, log)  # uploads log to gcs bucket
+
+    # upload data from Docker to archived_builds Google Cloud Storage Bucket
+    print('Copying Knowledge Graph Data from the current_build Directory to the archived_builds Directory')
+    logger.info('Copying Knowledge Graph Data from the current_build Directory to the archived_builds Directory')
+    source_data = [_.name.split('/')[-1] for _ in bucket.list_blobs(prefix=gcs_current_loc)]
+    copies_data_between_gcs_bucket_directories(bucket, gcs_current_loc, gcs_archive_loc, source_data)
+    uploads_data_to_gcs_bucket(bucket, gcs_current_loc, log_dir, log)  # uploads log to gcs bucket
+
+    #############################################################################
+    # STEP 4 - CLEAN UP BUILD ENVIRONMENT + LOG EXIT STATUS TO FINISH RUN
+    print('\nSTEP 4: BUILD CLEAN-UP')
+    logger.info('STEP 4: BUILD CLEAN-UP')
+    runtime = round((datetime.now() - start_time).total_seconds() / 60, 3)
+    print('\n\n' + '*' * 5 + ' COMPLETED BUILD PHASE 3: {} MINUTES '.format(runtime) + '*' * 5)
+    logger.info('COMPLETED BUILD PHASE 3: {} MINUTES'.format(runtime))  # don't delete needed for build monitoring
+    logger.info('EXIT BUILD PHASE 3')  # don't delete needed for build monitoring
+
+    # copy build logs
+    print('Copying Logs from the current_build Directory to the archived_builds Directory')
+    logger.info('Copying Logs from the current_build Directory to the archived_builds Directory')
+    log_1 = [x for x in [_.name for _ in bucket.list_blobs(prefix='current_build/')] if x.endswith('phases12_log.log')]
+    copies_data_between_gcs_bucket_directories(bucket, 'current_build/', gcs_archive_loc, [log_1[0].split('/')[-1]])
+    copies_data_between_gcs_bucket_directories(bucket, gcs_current_loc, gcs_archive_loc, [log])
+
+    # exit build
+    uploads_data_to_gcs_bucket(bucket, gcs_current_loc, log_dir, log)  # uploads log to gcs bucket
+
+    return None
 
 
 if __name__ == '__main__':
diff --git a/builds/build_requirements.txt b/builds/build_requirements.txt
old mode 100644
new mode 100755
index 88f30c04f..57681ea1c
--- a/builds/build_requirements.txt
+++ b/builds/build_requirements.txt
@@ -1,10 +1,20 @@
+click==7.1.2
+DateTime~=4.2
 google==1.9.3
+google-api-core==1.24.1
+google-api-python-client~=1.7.9
 google-cloud-storage==1.28.0
+lxml==4.5.2
 networkx==2.4
 numpy==1.18.1
 openpyxl==3.0.3
+oauth2client~=4.1.3
 Owlready2==0.25
 pandas==1.0.5
+python-json-logger==2.0.1
+ray~=1.1.0
 rdflib==4.2.2
 reactome2py==0.0.8
+requests~=2.22.0
+responses~=0.10.12
 tqdm==4.47.0
\ No newline at end of file
diff --git a/builds/build_utilities.py b/builds/build_utilities.py
new file mode 100755
index 000000000..a7bbd543f
--- /dev/null
+++ b/builds/build_utilities.py
@@ -0,0 +1,131 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# import needed libraries
+import fnmatch
+import os
+
+from google.cloud import storage  # type: ignore
+
+
+def uploads_data_to_gcs_bucket(bucket, original_data, temp_directory, filename):
+    """Takes a file name and pushes the data referenced by the filename object and stored locally in that object to
+    a Google Cloud Storage bucket.
+
+    Args:
+        bucket: A storage Bucket object specifying a Google Cloud Storage bucket.
+        original_data: A string specifying the location of the original_data directory for a specific build.
+        temp_directory: A local directory where preprocessed data is stored.
+        filename: A string containing a filename.
+
+    Returns:
+        None.
+    """
+
+    print('Uploading {} to GCS bucket: {}'.format(filename, original_data))
+
+    blob = bucket.blob(original_data + filename)
+    blob.upload_from_filename(temp_directory + '/' + filename)
+
+    return None
+
+
+def downloads_data_from_gcs_bucket(bucket, original_data, processed_data, filename, temp_directory):
+    """Takes a filename and and downloads the corresponding data to a local temporary directory, if it has not
+    already been downloaded.
+
+    Args:
+        bucket: A storage Bucket object specifying a Google Cloud Storage bucket.
+        original_data: A string containing a path to the original data directory in a Google Cloud Storage bucket.
+        processed_data: A string containing a path to the processed data directory in a Google Cloud Storage bucket.
+        filename: A string containing the name of file to write to a Google Cloud Storage bucket.
+        temp_directory: A local directory where preprocessed data is stored.
+
+    Returns:
+        data_file: A string containing the local filepath for a file downloaded from a GSC bucket.
+
+    Raises:
+        ValueError: when trying to download a non-existent file from the GCS original_data dir of the current build.
+    """
+
+    if isinstance(bucket, storage.bucket.Bucket):
+        try:  # search processed bucket first
+            if processed_data is not None:
+                _files = [_.name for _ in bucket.list_blobs(prefix=processed_data)]
+                proc_file = fnmatch.filter(_files, '*/' + filename)[0]
+                data_file = temp_directory + '/' + proc_file.split('/')[-1]
+                if not os.path.exists(data_file):  # only download if file has not yet been downloaded
+                    bucket.blob(proc_file).download_to_filename(temp_directory + '/' + proc_file.split('/')[-1])
+            else: raise IndexError('processed_data is None, Exception Trigger in Order to Search original_data')
+        except IndexError:
+            try:  # then search the original bucket
+                _files = [_.name for _ in bucket.list_blobs(prefix=original_data)]
+                org_file = fnmatch.filter(_files, '*/' + filename)[0]
+                data_file = temp_directory + '/' + org_file.split('/')[-1]
+                if not os.path.exists(data_file):  # only download if file has not yet been downloaded
+                    bucket.blob(org_file).download_to_filename(temp_directory + '/' + org_file.split('/')[-1])
+            except IndexError:
+                logger.error('Cannot find {} in the GCS directories of the current build'.format(filename))
+                raise ValueError('Cannot find {} in the GCS directories of the current build'.format(filename))
+        return data_file
+    else: return None
+
+
+def deletes_single_file(bucket, file_path):
+    """Method deletes a single file form a Google Cloud Storage Bucket.
+
+    Args:
+        bucket: A storage Bucket object specifying a Google Cloud Storage bucket.
+        file_path: A string containing a Google Cloud Storage Bucket path to a specific file.
+
+    Returns:
+        None.
+    """
+
+    blob = bucket.blob(file_path)
+    blob.delete()
+
+    return None
+
+
+def deletes_bucket_files(bucket, gcs_directory):
+    """Deletes all files within a specific directory in a Google Cloud Storage Bucket.
+
+    Args:
+        bucket: A storage bucket object specifying a Google Cloud Storage bucket.
+        gcs_directory: A string containing the location for directory within a Google Cloud Storage bucket.
+
+    Returns:
+        None.
+    """
+
+    # get all files currently in the directory
+    gcs_directory_files = [file.name for file in bucket.list_blobs(prefix=gcs_directory)]
+
+    # delete directory files
+    for _ in gcs_directory_files:
+        blob = bucket.blob(_)
+        blob.delete()
+
+    return None
+
+
+def copies_data_between_gcs_bucket_directories(bucket, source_directory, destination_directory, source_data_files):
+    """Method copies files between Google Cloud Storage bucket directories.
+
+    Args:
+        bucket: A storage bucket object specifying a Google Cloud Storage bucket.
+        source_directory: A Google CLoud Storage Bucket directory to copy from.
+        destination_directory: A Google CLoud Storage Bucket directory to copy to.
+        source_data_files: A list of Google Cloud Storage Bucket paths, including filenames that need to be copied.
+
+    Returns:
+        None.
+    """
+
+    for data_file in source_data_files:
+        source_data_file = bucket.blob(source_directory + data_file)
+        destination_data_file = destination_directory + data_file
+        _ = bucket.copy_blob(source_data_file, bucket, destination_data_file)
+
+    return None
diff --git a/builds/data_preprocessing.py b/builds/data_preprocessing.py
old mode 100644
new mode 100755
index 710f3467a..faf56835c
--- a/builds/data_preprocessing.py
+++ b/builds/data_preprocessing.py
@@ -3,7 +3,9 @@
 
 # import needed libraries
 import fnmatch
+import glob
 import itertools
+import logging.config
 import networkx  # type: ignore
 import numpy  # type: ignore
 import os
@@ -19,8 +21,15 @@
 from typing import Dict, List, Optional, Tuple, Union
 
 # import script containing helper functions
+from builds.build_utilities import *
 from pkt_kg.utils import *
 
+# set environment variables
+log_dir, log, log_config = 'builds/logs', 'pkt_builder_phases12_log.log', glob.glob('**/logging.ini', recursive=True)
+if not os.path.exists(log_dir): os.mkdir(log_dir)
+logger = logging.getLogger(__name__)
+logging.config.fileConfig(log_config[0], disable_existing_loggers=False, defaults={'log_file': log_dir + '/' + log})
+
 
 class DataPreprocessing(object):
     """Class provides a container for the data preprocessing methods, original housed in the Data_Preprocessing.ipynb
@@ -41,60 +50,21 @@ def __init__(self, gcs_bucket: storage.bucket.Bucket, org_data: str, processed_d
         self.bucket: storage.bucket.Bucket = gcs_bucket
         self.original_data: str = org_data
         self.processed_data: str = processed_data
+        self.current_build = 'current_build/'
         # SETTING LOCAL VARIABLES
         self.temp_dir = temp_dir
-        self.owltools_location = './pkt_kg/libs/owltools'
+        self.owltools_location = './builds/owltools'
+        # self.owltools_location = './pkt_kg/libs/owltools'
         # OTHER CLASS VARIABLES
         self.genomic_type_mapper: Dict = {}
 
-    def uploads_data_to_gcs_bucket(self, filename: str) -> None:
-        """Takes a file name and pushes the corresponding data referenced by the filename object from a local
-        temporary directory to a Google Cloud Storage bucket.
-
-        Args:
-            filename: A string containing the name of file to write to a Google Cloud Storage bucket.
-
-        Returns:
-            None.
-        """
-
-        blob = self.bucket.blob(self.processed_data + filename)
-        blob.upload_from_filename(self.temp_dir + '/' + filename)
-
-        return None
-
-    def downloads_data_from_gcs_bucket(self, filename: str) -> str:
-        """Takes a filename and and downloads the corresponding data to a local temporary directory, if it has not
-        already been downloaded.
-
-        Args:
-            filename: A string containing the name of file to write to a Google Cloud Storage bucket.
-
-        Returns:
-            data_file: A string containing the local filepath for a file downloaded from a GSC bucket.
-
-        Raises:
-            ValueError: when trying to download a non-existent file from the GCS original_data dir of the current build.
-        """
-
-        try:
-            _files = [_.name for _ in self.bucket.list_blobs(prefix=self.original_data)]
-            matched_file = fnmatch.filter(_files, '*/' + filename)[0]  # poor man's glob
-            data_file = self.temp_dir + '/' + matched_file.split('/')[-1]
-            if not os.path.exists(data_file):  # only download if file has not yet been downloaded
-                self.bucket.blob(matched_file).download_to_filename(self.temp_dir + '/' + matched_file.split('/')[-1])
-        except IndexError:
-            raise ValueError('Cannot find {} in the GCS original_data directory of the current build'.format(filename))
-
-        return data_file
-
-    def reads_gcs_bucket_data_to_df(self, filename: str, delm: str, skip: int = 0,
+    def reads_gcs_bucket_data_to_df(self, f_name: str, delm: str, skip: int = 0,
                                     head: Optional[Union[int, List]] = None,
                                     sht: Optional[Union[int, str]] = None) -> pandas.DataFrame:
         """Reads data corresponding to the input file_location variable into a Pandas DataFrame.
 
         Args:
-            filename: A string containing the name of file that exists in a Google Cloud Storage bucket.
+            f_name: A string containing the name of file that exists in a Google Cloud Storage bucket.
             delm: A string specifying a file delimiter.
             skip: An integer specifying the number of rows to skip when reading in the data.
             head: An integer specifying the header row, None for no header or a list of header names.
@@ -104,18 +74,18 @@ def reads_gcs_bucket_data_to_df(self, filename: str, delm: str, skip: int = 0,
              df: A Pandas DataFrame object containing data read from a Google Cloud Storage bucket.
         """
 
-        data = self.downloads_data_from_gcs_bucket(filename)
+        x = downloads_data_from_gcs_bucket(self.bucket, self.original_data, self.processed_data, f_name, self.temp_dir)
 
         if not isinstance(head, List):
             if sht is not None:
-                df = pandas.read_excel(data, sheet_name=sht, header=head, skiprows=skip, engine='openpyxl')
+                df = pandas.read_excel(x, sheet_name=sht, header=head, skiprows=skip, engine='openpyxl')
             else:
-                df = pandas.read_csv(data, header=head, delimiter=delm, skiprows=skip, low_memory=0)
+                df = pandas.read_csv(x, header=head, delimiter=delm, skiprows=skip, low_memory=0)
         else:
             if sht is not None:
-                df = pandas.read_excel(data, sheet_name=sht, header=None, names=head, skiprows=skip, engine='openpyxl')
+                df = pandas.read_excel(x, sheet_name=sht, header=None, names=head, skiprows=skip, engine='openpyxl')
             else:
-                df = pandas.read_csv(data, header=None, names=head, delimiter=delm, skiprows=skip, low_memory=0)
+                df = pandas.read_csv(x, header=None, names=head, delimiter=delm, skiprows=skip, low_memory=0)
 
         return df
 
@@ -129,8 +99,10 @@ def _loads_genomic_typing_dictionary(self) -> None:
             None.
         """
 
-        data_file = self.downloads_data_from_gcs_bucket('genomic_typing_dict.pkl')
-        self.genomic_type_mapper = pickle.load(open(data_file, 'rb'))
+        logger.info('Loading Genomic Typing Dictionary')
+        f_name = 'genomic_typing_dict.pkl'
+        x = downloads_data_from_gcs_bucket(self.bucket, self.original_data, self.processed_data, f_name, self.temp_dir)
+        self.genomic_type_mapper = pickle.load(open(x, 'rb'))
 
         return None
 
@@ -145,7 +117,8 @@ def _preprocess_hgnc_data(self) -> pandas.DataFrame:
             explode_df_hgnc: A Pandas DataFrame containing processed and filtered data.
         """
 
-        hgnc = self.reads_gcs_bucket_data_to_df(filename='hgnc_complete_set.txt', delm='\t', skip=0, head=0, sht=None)
+        logger.info('Preprocessing HGNC Data')
+        hgnc = self.reads_gcs_bucket_data_to_df(f_name='hgnc_complete_set.txt', delm='\t', skip=0, head=0, sht=None)
         hgnc = hgnc.loc[hgnc['status'].apply(lambda x: x == 'Approved')]
         hgnc = hgnc[['hgnc_id', 'entrez_id', 'ensembl_gene_id', 'uniprot_ids', 'symbol', 'locus_type', 'alias_symbol',
                      'name', 'location', 'alias_name']]
@@ -187,8 +160,9 @@ def _preprocess_ensembl_data(self) -> pandas.DataFrame:
             ensembl_geneset: A Pandas DataFrame containing processed and filtered data.
         """
 
+        logger.info('Preprocessing Ensembl Data')
         f_name, cleaned_col = 'Homo_sapiens.GRCh38.*.gtf', []
-        ensembl_geneset = self.reads_gcs_bucket_data_to_df(filename=f_name, delm='\t', skip=5, head=None, sht=None)
+        ensembl_geneset = self.reads_gcs_bucket_data_to_df(f_name=f_name, delm='\t', skip=5, head=None, sht=None)
         data_cols = ['gene_id', 'transcript_id', 'gene_name', 'gene_biotype', 'transcript_name', 'transcript_biotype']
         for data_list in tqdm(list(ensembl_geneset[8])):
             data_list = data_list if not data_list.endswith(';') else data_list[:-1]
@@ -237,16 +211,17 @@ def merges_ensembl_mapping_data(self) -> pandas.DataFrame:
                 additional annotation  mapping data from uniprot and entrez.
         """
 
+        logger.info('Merging Ensembl Annotation Data')
         drop_cols = ['db_name', 'info_type', 'source_identity', 'xref_identity', 'linkage_type']
         un_name, ent_name = 'Homo_sapiens.GRCh38.*.uniprot.tsv', 'Homo_sapiens.GRCh38.*.entrez.tsv'
         # uniprot data
-        ensembl_uniprot = self.reads_gcs_bucket_data_to_df(filename=un_name, delm='\t', skip=0, head=0, sht=None)
+        ensembl_uniprot = self.reads_gcs_bucket_data_to_df(f_name=un_name, delm='\t', skip=0, head=0, sht=None)
         ensembl_uniprot.rename(columns={'xref': 'uniprot_id', 'gene_stable_id': 'ensembl_gene_id'}, inplace=True)
         ensembl_uniprot.replace('-', 'None', inplace=True)
         ensembl_uniprot.fillna('None', inplace=True)
         ensembl_uniprot.drop(drop_cols, axis=1, inplace=True)
         # entrez data
-        ensembl_entrez = self.reads_gcs_bucket_data_to_df(filename=ent_name, delm='\t', skip=0, head=0, sht=None)
+        ensembl_entrez = self.reads_gcs_bucket_data_to_df(f_name=ent_name, delm='\t', skip=0, head=0, sht=None)
         ensembl_entrez.rename(columns={'xref': 'entrez_id', 'gene_stable_id': 'ensembl_gene_id'}, inplace=True)
         ensembl_entrez = ensembl_entrez.loc[ensembl_entrez['db_name'].apply(lambda x: x == 'EntrezGene')]
         ensembl_entrez.replace('-', 'None', inplace=True)
@@ -267,7 +242,7 @@ def merges_ensembl_mapping_data(self) -> pandas.DataFrame:
         # save data locally and push to gcs bucket
         filename = 'ensembl_identifier_data_cleaned.txt'
         ensembl.to_csv(self.temp_dir + '/' + filename, header=True, sep='\t', index=False)
-        self.uploads_data_to_gcs_bucket(filename)
+        uploads_data_to_gcs_bucket(self.bucket, self.processed_data, self.temp_dir, filename)
 
         return ensembl
 
@@ -280,8 +255,9 @@ def _preprocess_uniprot_data(self) -> pandas.DataFrame:
             explode_df_uniprot: A Pandas DataFrame containing processed and filtered data.
         """
 
+        logger.info('Preprocessing UniProt Data')
         f_name = 'uniprot_identifier_mapping.tab'
-        uniprot = self.reads_gcs_bucket_data_to_df(filename=f_name, delm='\t', skip=0, head=0, sht=None)
+        uniprot = self.reads_gcs_bucket_data_to_df(f_name=f_name, delm='\t', skip=0, head=0, sht=None)
         uniprot.fillna('None', inplace=True)
         uniprot.rename(columns={'Entry': 'uniprot_id', 'Cross-reference (GeneID)': 'entrez_id',
                                 'Ensembl transcript': 'transcript_stable_id', 'Cross-reference (HGNC)': 'hgnc_id',
@@ -307,8 +283,9 @@ def _preprocess_ncbi_data(self) -> pandas.DataFrame:
             explode_df_ncbi_gene: A Pandas DataFrame containing processed and filtered data.
         """
 
+        logger.info('Preprocessing Entrez Data')
         f_name = 'Homo_sapiens.gene_info'
-        ncbi_gene = self.reads_gcs_bucket_data_to_df(filename=f_name, delm='\t', skip=0, head=0, sht=None)
+        ncbi_gene = self.reads_gcs_bucket_data_to_df(f_name=f_name, delm='\t', skip=0, head=0, sht=None)
         ncbi_gene = ncbi_gene.loc[ncbi_gene['#tax_id'].apply(lambda x: x == 9606)]  # remove non-human rows
         ncbi_gene.replace('-', 'None', inplace=True)
         ncbi_gene.rename(columns={'GeneID': 'entrez_id', 'Symbol': 'symbol', 'Synonyms': 'synonyms'}, inplace=True)
@@ -345,8 +322,9 @@ def _preprocess_protein_ontology_mapping_data(self) -> pandas.DataFrame:
             pro: A Pandas DataFrame containing processed and filtered data.
         """
 
+        logger.info('Preprocessing Protein Ontology Data')
         col_names = ['pro_id', 'Entry', 'pro_mapping']
-        pro = self.reads_gcs_bucket_data_to_df(filename='promapping.txt', delm='\t', skip=0, head=col_names, sht=None)
+        pro = self.reads_gcs_bucket_data_to_df(f_name='promapping.txt', delm='\t', skip=0, head=col_names, sht=None)
         pro = pro.loc[pro['Entry'].apply(lambda x: x.startswith('UniProtKB:') and '_VAR' not in x and ', ' not in x)]
         pro = pro.loc[pro['pro_mapping'].apply(lambda x: x.startswith('exact'))]
         pro['pro_id'].replace('PR:', 'PR_', inplace=True, regex=True)  # replace PR: with PR_
@@ -365,6 +343,7 @@ def _merges_genomic_identifier_data(self) -> pandas.DataFrame:
         """
 
         print('\t- Loading, Processing, and Merging Genomic Identifier Data')
+        logger.info('Merging Genomic Identifier Data')
 
         # hgnc + ensembl
         hgnc, ensembl = self._preprocess_hgnc_data(), self.merges_ensembl_mapping_data()
@@ -399,6 +378,7 @@ def _fixes_genomic_symbols(self) -> pandas.DataFrame:
             merged_data_clean: A Pandas DataFrame with fix genomic symbols.
         """
 
+        logger.info('Fixing genomic Symbols')
         clean_dates, merged_data = [], self._merges_genomic_identifier_data()
         for x in tqdm(list(merged_data['symbol'])):
             if '-' in x and len(x.split('-')[0]) < 3 and len(x.split('-')[1]) == 3:
@@ -429,6 +409,7 @@ def _cross_maps_genomic_identifier_data(self) -> Dict:
                 metadata mapped to that identifier.
         """
 
+        logger.info('Cross-Mapping Genomic Identifier Data')
         # reformat data to convert all nones, empty values, and unknowns to NaN
         merged_data: pandas.DataFrame = self._fixes_genomic_symbols()
         master_dict: Dict = {}
@@ -462,6 +443,7 @@ def creates_master_genomic_identifier_map(self) -> Dict:
         """
 
         print('\t- Creating Genomic Identifier Cross-Mapping Dictionary')
+        logger.info('Creating Genomic Identifier Cross-Mapping Dictionary')
 
         master_dict = self._cross_maps_genomic_identifier_data()
         reformatted_mapped_identifiers, gene_type, transcript_type = {}, 'gene_type', 'transcript_type'
@@ -481,8 +463,7 @@ def creates_master_genomic_identifier_map(self) -> Dict:
                 for i in [t_types if len(t_types) > 0 else ['None']]:
                     t_type_list += ['protein-coding' if 'protein-coding' in i else 'not protein-coding']
                 identifier_info += ['transcript_type_update_' + max(set(t_type_list), key=t_type_list.count)]
-            else:
-                identifier_info = values
+            else: identifier_info = values
             reformatted_mapped_identifiers[key] = identifier_info
 
         # save results for output > 4GB requires special approach: https://stackoverflow.com/questions/42653386
@@ -490,7 +471,7 @@ def creates_master_genomic_identifier_map(self) -> Dict:
         with open(self.temp_dir + '/' + filename, 'wb') as f_out:
             for idx in range(0, sys.getsizeof(pickle.dumps(reformatted_mapped_identifiers)), 2 ** 31 - 1):
                 f_out.write(pickle.dumps(reformatted_mapped_identifiers)[idx:idx + (2 ** 31 - 1)])
-        self.uploads_data_to_gcs_bucket(filename)
+        uploads_data_to_gcs_bucket(self.bucket, self.processed_data, self.temp_dir, filename)
 
         return reformatted_mapped_identifiers
 
@@ -502,26 +483,27 @@ def generates_specific_genomic_identifier_maps(self) -> None:
         """
 
         print('\t- Generating Genomic Identifier Cross-Mapping Sets')
+        logger.info('Generating Pairwise Genomic Cross-Map Sets')
 
         self._loads_genomic_typing_dictionary()  # creates genomic typing dictionary
         reformatted_mapped_identifiers = self.creates_master_genomic_identifier_map()
         gene_sets = [
-            ['/ENSEMBL_GENE_ENTREZ_GENE_MAP.txt', 'ensembl_gene_id', 'entrez_id', 'gene_type_update', False, False],
-            ['/ENSEMBL_TRANSCRIPT_PROTEIN_ONTOLOGY_MAP.txt', 'transcript_stable_id', 'pro_id',
+            ['ENSEMBL_GENE_ENTREZ_GENE_MAP.txt', 'ensembl_gene_id', 'entrez_id', 'gene_type_update', False, False],
+            ['ENSEMBL_TRANSCRIPT_PROTEIN_ONTOLOGY_MAP.txt', 'transcript_stable_id', 'pro_id',
              'transcript_type_update', False, True],
-            ['/ENTREZ_GENE_ENSEMBL_TRANSCRIPT_MAP.txt', 'entrez_id', 'transcript_stable_id', 'transcript_type_update',
+            ['ENTREZ_GENE_ENSEMBL_TRANSCRIPT_MAP.txt', 'entrez_id', 'transcript_stable_id', 'transcript_type_update',
              False, False],
-            ['/ENTREZ_GENE_PRO_ONTOLOGY_MAP.txt', 'entrez_id', 'pro_id', 'gene_type_update', False, True],
-            ['/GENE_SYMBOL_ENSEMBL_TRANSCRIPT_MAP.txt', 'symbol', 'transcript_stable_id', 'transcript_type_update',
+            ['ENTREZ_GENE_PRO_ONTOLOGY_MAP.txt', 'entrez_id', 'pro_id', 'gene_type_update', False, True],
+            ['GENE_SYMBOL_ENSEMBL_TRANSCRIPT_MAP.txt', 'symbol', 'transcript_stable_id', 'transcript_type_update',
              False, False],
-            ['/STRING_PRO_ONTOLOGY_MAP.txt', 'protein_stable_id', 'pro_id', None, False, True],
-            ['/UNIPROT_ACCESSION_PRO_ONTOLOGY_MAP.txt', 'uniprot_id', 'pro_id', None, False, True]
+            ['STRING_PRO_ONTOLOGY_MAP.txt', 'protein_stable_id', 'pro_id', None, False, True],
+            ['UNIPROT_ACCESSION_PRO_ONTOLOGY_MAP.txt', 'uniprot_id', 'pro_id', None, False, True]
         ]
 
         for x in gene_sets:
             genomic_id_mapper(reformatted_mapped_identifiers,
-                              self.temp_dir + x[0], x[1], x[2], x[3], x[4], x[5])  # type: ignore
-            self.uploads_data_to_gcs_bucket(x[0])  # type: ignore
+                              self.temp_dir + '/' + x[0], x[1], x[2], x[3], x[4], x[5])  # type: ignore
+            uploads_data_to_gcs_bucket(self.bucket, self.processed_data, self.temp_dir, x[0])  # type: ignore
 
         return None
 
@@ -537,8 +519,11 @@ def _processes_mesh_data(self) -> pandas.DataFrame:
         """
 
         print('\t- Processing MeSH Data')
+        logger.info('Preprocessing MeSH Data')
 
-        mesh = [x.split('> ') for x in tqdm(open(self.downloads_data_from_gcs_bucket('mesh*.nt'), 'r').readlines())]
+        f_name = 'mesh*.nt'
+        x = downloads_data_from_gcs_bucket(self.bucket, self.original_data, self.processed_data, f_name, self.temp_dir)
+        mesh = [i.split('> ') for i in tqdm(open(x, 'r').readlines())]
         msh_dict: Dict = {}
         res: List = []
         for row in tqdm(mesh):
@@ -580,8 +565,9 @@ def _processes_chebi_data(self) -> pandas.DataFrame:
         """
 
         print('\t- Processing ChEBI Data')
+        logger.info('Preprocessing ChEBI Data')
 
-        chebi = self.reads_gcs_bucket_data_to_df(filename='names.tsv', delm='\t', skip=0, head=0, sht=None)
+        chebi = self.reads_gcs_bucket_data_to_df(f_name='names.tsv', delm='\t', skip=0, head=0, sht=None)
         chebi_df = chebi[['COMPOUND_ID', 'TYPE', 'NAME']]  # remove unneeded columns
         chebi_df.drop_duplicates(subset=None, keep='first', inplace=True)
         chebi_df.columns = ['ID', 'TYP', 'STR']  # rename columns
@@ -602,6 +588,7 @@ def creates_chebi_to_mesh_identifier_mappings(self) -> None:
         """
 
         print('Creating MeSH-ChEBI Identifier Cross-Mapping Data')
+        logger.info('Creating MeSH-ChEBI Identifier Cross-Mapping Data')
 
         mesh_df, mesh_dict = self._processes_mesh_data()
         chebi_df = self._processes_chebi_data()
@@ -623,7 +610,7 @@ def creates_chebi_to_mesh_identifier_mappings(self) -> None:
         with open(self.temp_dir + '/' + filename, 'w') as out:
             for pair in mesh_edges:
                 out.write(pair[0] + '\t' + pair[1] + '\n')
-        self.uploads_data_to_gcs_bucket(filename)
+        uploads_data_to_gcs_bucket(self.bucket, self.processed_data, self.temp_dir, filename)
 
         return None
 
@@ -638,8 +625,11 @@ def _preprocess_mondo_mapping_data(self) -> Dict:
         """
 
         print('\t- Loading MonDO Disease Ontology Data')
+        logger.info('Loading MonDO Disease Ontology Data')
 
-        mondo_graph = Graph().parse(self.downloads_data_from_gcs_bucket('mondo_with_imports.owl'))
+        f_name = 'mondo_with_imports.owl'
+        x = downloads_data_from_gcs_bucket(self.bucket, self.original_data, self.processed_data, f_name, self.temp_dir)
+        mondo_graph = Graph().parse(x)
         dbxref_res = gets_ontology_class_dbxrefs(mondo_graph)[0]
         mondo_dict = {str(k).lower().split('/')[-1]: {str(v).split('/')[-1].replace('_', ':')}
                       for k, v in dbxref_res.items()
@@ -658,8 +648,11 @@ def _preprocess_hpo_mapping_data(self) -> Dict:
         """
 
         print('\t- Loading Human Phenotype Ontology Data')
+        logger.info('Loading Human Phenotype Ontology Data')
 
-        hp_graph = Graph().parse(self.downloads_data_from_gcs_bucket('hp_with_imports.owl'))
+        f_name = 'hp_with_imports.owl'
+        x = downloads_data_from_gcs_bucket(self.bucket, self.original_data, self.processed_data, f_name, self.temp_dir)
+        hp_graph = Graph().parse(x)
         dbxref_res = gets_ontology_class_dbxrefs(hp_graph)[0]
         hp_dict = {str(k).lower().split('/')[-1]: {str(v).split('/')[-1].replace('_', ':')}
                    for k, v in dbxref_res.items()
@@ -677,9 +670,10 @@ def creates_disease_identifier_mappings(self) -> None:
         """
 
         print('Creating Phenotype and Disease Identifier Cross-Mapping Data')
+        logger.info('Creating Phenotype and Disease Identifier Cross-Mapping Data')
 
         mondo_dict, hp_dict = self._preprocess_mondo_mapping_data(), self._preprocess_hpo_mapping_data()
-        data = self.reads_gcs_bucket_data_to_df(filename='disease_mappings.tsv', delm='\t', skip=0, head=0, sht=None)
+        data = self.reads_gcs_bucket_data_to_df(f_name='disease_mappings.tsv', delm='\t', skip=0, head=0, sht=None)
         data['vocabulary'], data['diseaseId'] = data['vocabulary'].str.lower(), data['diseaseId'].str.lower()
         data['vocabulary'] = ['doid' if x == 'do' else 'ordoid' if x == 'ordo' else x for x in data['vocabulary']]
 
@@ -720,8 +714,8 @@ def creates_disease_identifier_mappings(self) -> None:
                 if any(x for x in v if x.startswith('HP')):
                     for idx in [x.replace(':', '_') for x in v if 'HP' in x]:
                         out2.write(k.upper().split(':')[-1] + '\t' + idx + '\n')
-        self.uploads_data_to_gcs_bucket(file1)
-        self.uploads_data_to_gcs_bucket(file2)
+        uploads_data_to_gcs_bucket(self.bucket, self.processed_data, self.temp_dir, file1)
+        uploads_data_to_gcs_bucket(self.bucket, self.processed_data, self.temp_dir, file2)
 
         return None
 
@@ -734,8 +728,9 @@ def _hpa_gtex_ontology_alignment(self) -> None:
             None.
         """
 
+        logger.info('Preprocessing HPA Data')
         data_file, sheet = 'zooma_tissue_cell_mapping_04JAN2020.xlsx', 'Concept_Mapping - 04JAN2020'
-        mapping_data = self.reads_gcs_bucket_data_to_df(filename=data_file, delm='\t', skip=0, head=0, sht=sheet)
+        mapping_data = self.reads_gcs_bucket_data_to_df(f_name=data_file, delm='\t', skip=0, head=0, sht=sheet)
         mapping_data.fillna('NA', inplace=True)
 
         # write data to useful format for pheknowlator and push to gcs
@@ -745,7 +740,7 @@ def _hpa_gtex_ontology_alignment(self) -> None:
                 if row['UBERON'] != 'NA': out.write(str(row['TERM']).strip() + '\t' + str(row['UBERON']).strip() + '\n')
                 if row['CL'] != 'NA': out.write(str(row['TERM']).strip() + '\t' + str(row['CL']).strip() + '\n')
                 if row['CLO'] != 'NA': out.write(str(row['TERM']).strip() + '\t' + str(row['CLO']).strip() + '\n')
-        self.uploads_data_to_gcs_bucket(filename)
+        uploads_data_to_gcs_bucket(self.bucket, self.processed_data, self.temp_dir, filename)
 
         return None
 
@@ -758,7 +753,8 @@ def _extracts_hpa_tissue_information(self) -> pandas.DataFrame:
              hpa: A Pandas DataFrame object containing tissue data.
         """
 
-        hpa = self.reads_gcs_bucket_data_to_df(filename='proteinatlas_search.tsv', delm='\t', skip=0, head=0, sht=None)
+        logger.info('Extracting HPA Tissue and Cell Information')
+        hpa = self.reads_gcs_bucket_data_to_df(f_name='proteinatlas_search.tsv', delm='\t', skip=0, head=0, sht=None)
         hpa.fillna('None', inplace=True)
 
         # write results
@@ -766,7 +762,7 @@ def _extracts_hpa_tissue_information(self) -> pandas.DataFrame:
         with open(self.temp_dir + '/' + filename, 'w') as outfile:
             for x in tqdm(list(hpa.columns)):
                 if x.endswith('[NX]'): outfile.write(x.split('RNA - ')[-1].split(' [NX]')[:-1][0] + '\n')
-        self.uploads_data_to_gcs_bucket(filename)
+        uploads_data_to_gcs_bucket(self.bucket, self.processed_data, self.temp_dir, filename)
 
         return hpa
 
@@ -785,10 +781,11 @@ def processes_hpa_gtex_data(self) -> None:
         """
 
         print('Creating Human Protein Atlas and Genotype-Tissue Expression Project Cross-Mapping Data')
+        logger.info('Creating Human Protein Atlas and Genotype-Tissue Expression Project Cross-Mapping Data')
 
         hpa = self._extracts_hpa_tissue_information()
         f_name = 'GTEx_Analysis_*_RNASeQC*_gene_median_tpm.gct'
-        gtex = self.reads_gcs_bucket_data_to_df(filename=f_name, delm='\t', skip=2, head=0, sht=None)
+        gtex = self.reads_gcs_bucket_data_to_df(f_name=f_name, delm='\t', skip=2, head=0, sht=None)
         gtex.fillna('None', inplace=True)
         gtex['Name'].replace('(\..*)', '', inplace=True, regex=True)
 
@@ -826,7 +823,7 @@ def processes_hpa_gtex_data(self) -> None:
         with open(self.temp_dir + '/' + filename, 'w') as out:
             for x in hpa_results + gtex_results:
                 out.write(x[0] + '\t' + x[1] + '\t' + x[2] + '\t' + x[3] + '\t' + x[4] + '\t' + x[5] + '\n')
-        self.uploads_data_to_gcs_bucket(filename)
+        uploads_data_to_gcs_bucket(self.bucket, self.processed_data, self.temp_dir, filename)
 
         return None
 
@@ -839,8 +836,11 @@ def _preprocess_pathway_mapping_data(self) -> Dict:
         """
 
         print('\t- Loading Protein Ontology Data')
+        logger.info('Loading Protein Ontology Data')
 
-        pw_graph = Graph().parse(self.downloads_data_from_gcs_bucket('pw_with_imports.owl'))
+        f_name = 'pw_with_imports.owl'
+        x = downloads_data_from_gcs_bucket(self.bucket, self.original_data, self.processed_data, f_name, self.temp_dir)
+        pw_graph = Graph().parse(x)
         dbxref_res = gets_ontology_class_dbxrefs(pw_graph)[0]
         dbxref_dict = {str(k).lower().split('/')[-1]: {str(v).split('/')[-1].replace('_', ':')}
                        for k, v in dbxref_res.items() if 'PW_' in str(v)}
@@ -859,19 +859,19 @@ def _processes_reactome_data(self) -> Dict:
         """
 
         print('\t- Loading Reactome Annotation Data')
+        logger.info('Loading Reactome Annotation Data')
 
         r_name, g_name, c_name = 'ReactomePathways.txt', 'gene_association.reactome', 'ChEBI2Reactome_All_Levels.txt'
-
         # reactome pathways
-        reactome_pathways = self.reads_gcs_bucket_data_to_df(filename=r_name, delm='\t', skip=0, head=None, sht=None)
+        reactome_pathways = self.reads_gcs_bucket_data_to_df(f_name=r_name, delm='\t', skip=0, head=None, sht=None)
         reactome_pathways = reactome_pathways.loc[reactome_pathways[2].apply(lambda x: x == 'Homo sapiens')]
         reactome = {x: {'PW_0000001'} for x in set(list(reactome_pathways[0]))}
         # reactome - GO associations
-        reactome_pathways2 = self.reads_gcs_bucket_data_to_df(filename=g_name, delm='\t', skip=1, head=None, sht=None)
+        reactome_pathways2 = self.reads_gcs_bucket_data_to_df(f_name=g_name, delm='\t', skip=1, head=None, sht=None)
         reactome_pathways2 = reactome_pathways2.loc[reactome_pathways2[12].apply(lambda x: x == 'taxon:9606')]
         reactome.update({x.split(':')[-1]: {'PW_0000001'} for x in set(list(reactome_pathways2[5]))})
         # reactome CHEBI
-        reactome_pathways3 = self.reads_gcs_bucket_data_to_df(filename=c_name, delm='\t', skip=0, head=None, sht=None)
+        reactome_pathways3 = self.reads_gcs_bucket_data_to_df(f_name=c_name, delm='\t', skip=0, head=None, sht=None)
         reactome_pathways3 = reactome_pathways3.loc[reactome_pathways3[5].apply(lambda x: x == 'Homo sapiens')]
         reactome.update({x: {'PW_0000001'} for x in set(list(reactome_pathways3[1]))})
 
@@ -890,23 +890,20 @@ def _processes_compath_pathway_data(self, reactome: Dict, pw_dict: Dict) -> Dict
         """
 
         print('\t- Loading ComPath Canonical Pathway Data')
+        logger.info('Loading ComPath Canonical Pathway Data')
 
         f_name = 'compath_canonical_pathway_mappings.txt'
-        compath = self.reads_gcs_bucket_data_to_df(filename=f_name, delm='\t', skip=0, head=None, sht=None)
+        compath = self.reads_gcs_bucket_data_to_df(f_name=f_name, delm='\t', skip=0, head=None, sht=None)
         compath.fillna('None', inplace=True)
         for idx, row in tqdm(compath.iterrows(), total=compath.shape[0]):
             if row[6] == 'kegg' and 'kegg:' + row[5].strip('path:hsa') in pw_dict.keys() and row[2] == 'reactome':
                 for x in pw_dict['kegg:' + row[5].strip('path:hsa')]:
-                    if row[1] in reactome.keys():
-                        reactome[row[1]] |= {x.split('/')[-1]}
-                    else:
-                        reactome[row[1]] = {x.split('/')[-1]}
+                    if row[1] in reactome.keys(): reactome[row[1]] |= {x.split('/')[-1]}
+                    else: reactome[row[1]] = {x.split('/')[-1]}
             if (row[2] == 'kegg' and 'kegg:' + row[1].strip('path:hsa') in pw_dict.keys()) and row[6] == 'reactome':
                 for x in pw_dict['kegg:' + row[1].strip('path:hsa')]:
-                    if row[5] in reactome.keys():
-                        reactome[row[5]] |= {x.split('/')[-1]}
-                    else:
-                        reactome[row[5]] = {x.split('/')[-1]}
+                    if row[5] in reactome.keys(): reactome[row[5]] |= {x.split('/')[-1]}
+                    else: reactome[row[5]] = {x.split('/')[-1]}
 
         return reactome
 
@@ -923,11 +920,11 @@ def _processes_kegg_pathway_data(self, reactome: Dict, pw_dict: Dict) -> Dict:
         """
 
         print('\t- Loading KEGG Data')
+        logger.info('Loading KEGG Data')
 
         f_name = 'kegg_reactome.csv'
-        kegg_reactome_map = self.reads_gcs_bucket_data_to_df(filename=f_name, delm=',', skip=0, head=0, sht=None)
+        kegg_reactome_map = self.reads_gcs_bucket_data_to_df(f_name=f_name, delm=',', skip=0, head=0, sht=None)
         src, tar, tar_ids, src_ids = 'Source Resource', 'Target Resource', 'Target ID', 'Source ID'
-
         for idx, row in tqdm(kegg_reactome_map.iterrows(), total=kegg_reactome_map.shape[0]):
             if row[src] == 'reactome' and 'kegg:' + row[tar_ids].strip('path:hsa') in pw_dict.keys():
                 for x in pw_dict['kegg:' + row[tar_ids].strip('path:hsa')]:
@@ -953,6 +950,7 @@ def _queries_reactome_api(reactome: Dict) -> Dict:
         """
 
         print('\t- Querying Reactome API to Obtain Reactome-GO Biological Process Mappings')
+        logger.info('Querying Reactome API to Obtain Reactome-GO Biological Process Mappings')
 
         for request_ids in tqdm(list(chunks(list(reactome.keys()), 20))):
             result, key = content.query_ids(ids=','.join(request_ids)), 'goBiologicalProcess'
@@ -974,20 +972,20 @@ def _creates_pathway_identifier_mappings(self) -> Dict:
         """
 
         print('\nCreating Pathway Ontology Identifier Cross-Mapping Data')
+        logger.info('Creating Pathway Ontology Identifier Cross-Mapping Data')
 
         pw_dict = self._preprocess_pathway_mapping_data()
         reactome = self._processes_reactome_data()
         compath_reactome = self._processes_compath_pathway_data(reactome, pw_dict)
         kegg_reactome = self._processes_kegg_pathway_data(compath_reactome, pw_dict)
         reactome = self._queries_reactome_api(kegg_reactome)
-
         # write data
         filename = 'REACTOME_PW_GO_MAPPINGS.txt'
         with open(self.temp_dir + '/' + filename, 'w') as out:
             for key in tqdm(reactome.keys()):
                 for x in reactome[key]:
                     if x.startswith('PW') or x.startswith('GO'): out.write(key + '\t' + x + '\n')
-        self.uploads_data_to_gcs_bucket(filename)
+        uploads_data_to_gcs_bucket(self.bucket, self.processed_data, self.temp_dir, filename)
 
         return reactome
 
@@ -1002,6 +1000,7 @@ def _preprocesses_gene_types(self, genomic_map: Dict) -> Dict:
         """
 
         print('\t- Mapping Sequence Ontology Classes to Gene Identifiers')
+        logger.info('Mapping Sequence Ontology Classes to Gene Identifiers')
 
         gene_ids = pickle.load(open(self.temp_dir + '/Merged_gene_rna_protein_identifiers.pkl', 'rb'), encoding='bytes')
         sequence_map: Dict = {}
@@ -1037,6 +1036,7 @@ def _preprocesses_transcript_types(self, genomic_map: Dict, sequence_map: Dict)
         """
 
         print('\t- Mapping Sequence Ontology Classes to Transcript Identifiers')
+        logger.info('Mapping Sequence Ontology Classes to Transcript Identifiers')
 
         trans: Dict = {}
         trans_id: str = 'transcript_stable_id'
@@ -1048,7 +1048,6 @@ def _preprocesses_transcript_types(self, genomic_map: Dict, sequence_map: Dict)
                     trans[row[trans_id].replace('transcript_stable_id_', '')] += [row['ensembl_transcript_type']]
                 else:
                     trans[row[trans_id].replace('transcript_stable_id_', '')] = [row['ensembl_transcript_type']]
-
         # update SO map dictionary
         for ids in tqdm(trans.keys()):
             if trans[ids][0] == 'protein_coding': trans_type = genomic_map['protein-coding_Transcript']
@@ -1070,10 +1069,11 @@ def _preprocesses_variant_types(self, genomic_map: Dict, sequence_map: Dict) ->
         """
 
         print('\t- Mapping Sequence Ontology Classes to Variant Identifiers')
+        logger.info('Mapping Sequence Ontology Classes to Variant Identifiers')
 
         f_name: str = 'variant_summary.txt'
         v_df: Dict = {}
-        variant_data = self.reads_gcs_bucket_data_to_df(filename=f_name, delm='\t', skip=0, head=0, sht=None)
+        variant_data = self.reads_gcs_bucket_data_to_df(f_name=f_name, delm='\t', skip=0, head=0, sht=None)
         for idx, row in tqdm(variant_data.iterrows(), total=variant_data.shape[0]):
             if row['Assembly'] == 'GRCh38' and row['RS# (dbSNP)'] != -1:
                 if 'rs' + str(row['RS# (dbSNP)']) in v_df.keys(): v_df['rs' + str(row['RS# (dbSNP)'])] |= {row['Type']}
@@ -1096,25 +1096,24 @@ def _creates_sequence_identifier_mappings(self) -> Dict:
         """
 
         print('\nCreating Sequence Ontology Identifier Cross-Mapping Data')
+        logger.info('Creating Sequence Ontology Identifier Cross-Mapping Data')
 
         f_name, sht = 'genomic_sequence_ontology_mappings.xlsx', 'GenomicType_SO_Map_09Mar2020'
-        mapping_data = self.reads_gcs_bucket_data_to_df(filename=f_name, delm='\t', skip=0, head=0, sht=sht)
+        mapping_data = self.reads_gcs_bucket_data_to_df(f_name=f_name, delm='\t', skip=0, head=0, sht=sht)
         genomic_type_so_map = {}
         for idx, row in tqdm(mapping_data.iterrows(), total=mapping_data.shape[0]):
             genomic_type_so_map[row['source_*_type'] + '_' + row['Genomic']] = row['SO ID']
-
         # add genes, transcripts, and variants
         genomic_sequence_map = self._preprocesses_gene_types(genomic_type_so_map)
         trans_sequence_map = self._preprocesses_transcript_types(genomic_type_so_map, genomic_sequence_map)
         sequence_map = self._preprocesses_variant_types(genomic_type_so_map, trans_sequence_map)
-
         # output data
         filename = 'SO_GENE_TRANSCRIPT_VARIANT_TYPE_MAPPING.txt'
         with open(self.temp_dir + '/' + filename, 'w') as outfile:
             for key in tqdm(sequence_map.keys()):
                 for map_type in sequence_map[key]:
                     outfile.write(key + '\t' + map_type + '\n')
-        self.uploads_data_to_gcs_bucket(filename)
+        uploads_data_to_gcs_bucket(self.bucket, self.processed_data, self.temp_dir, filename)
 
         return sequence_map
 
@@ -1127,20 +1126,19 @@ def combines_pathway_and_sequence_ontology_dictionaries(self) -> None:
         """
 
         print('Creating Pathway and Sequence Ontology Mapping Dictionary')
+        logger.info('Creating Pathway and Sequence Ontology Mapping Dictionary')
 
         sequence_map = self._creates_sequence_identifier_mappings()
         reactome_map = self._creates_pathway_identifier_mappings()
-
         # combine genomic and pathway maps + iterate over pathway lists and combine them into a single dictionary
         sequence_map.update(reactome_map)
         subclass_mapping = {}
         for key in tqdm(sequence_map.keys()):
             subclass_mapping[key] = sequence_map[key]
-
         # save file and push to gcs bucket
         filename = 'subclass_construction_map.pkl'
         pickle.dump(subclass_mapping, open(self.temp_dir + '/' + filename, 'wb'), protocol=4)
-        self.uploads_data_to_gcs_bucket(filename)
+        uploads_data_to_gcs_bucket(self.bucket, self.processed_data, self.temp_dir, filename)
 
         return None
 
@@ -1153,9 +1151,11 @@ def _processes_protein_ontology_data(self) -> networkx.MultiDiGraph:
         """
 
         print('\t- Loading Protein Ontology Data')
+        logger.info('Loading Protein Ontology Data')
 
-        pr_graph = Graph().parse(self.downloads_data_from_gcs_bucket('pr_with_imports.owl'))
-
+        f_name = 'pr_with_imports.owl'
+        x = downloads_data_from_gcs_bucket(self.bucket, self.original_data, self.processed_data, f_name, self.temp_dir)
+        pr_graph = Graph().parse(x)
         # convert rdf graph to networkx multidigraph
         networkx_mdg: networkx.MultiDiGraph = networkx.MultiDiGraph()
         for s, p, o in tqdm(pr_graph):
@@ -1163,12 +1163,12 @@ def _processes_protein_ontology_data(self) -> networkx.MultiDiGraph:
 
         return networkx_mdg
 
-    def _logically_verifies_human_protein_ontology(self, input_filename, output_filename, reasoner) -> None:
+    def _logically_verifies_human_protein_ontology(self, in_filename, out_filename, reasoner) -> None:
         """Logically verifies constructed Human Protein Ontology by running a deductive logic reasoner.
 
         Args:
-            input_filename: A string containing the name of the file to run the reasoner on.
-            output_filename: A string containing the filename to write the reasoner results to.
+            in_filename: A string containing the name of the file to run the reasoner on.
+            out_filename: A string containing the filename to write the reasoner results to.
             reasoner: A string containing the name of the deductive reasoner to use.
 
         Returns:
@@ -1176,18 +1176,19 @@ def _logically_verifies_human_protein_ontology(self, input_filename, output_file
         """
 
         print('\t- Logically Verifying Constructed Human Protein Ontology')
+        logger.info('Logically Verifying Constructed Human Protein Ontology')
 
         # run reasoner
-        command = "./pkt_kg/libs/owltools ./{} --reasoner {} --run-reasoner --assert-implied -o ./{}"
-        return_code = os.system(command.format(input_filename, reasoner.lower(), output_filename))
-
+        command = "{} ./{} --reasoner {} --run-reasoner --assert-implied -o ./{}"
+        return_code = os.system(command.format(self.owltools_location, in_filename, reasoner.lower(), out_filename))
         if return_code == 0:
-            ontology_file_formatter(self.temp_dir, '/' + input_filename.split('/')[-1], self.owltools_location)
-            ontology_file_formatter(self.temp_dir, '/' + output_filename.split('/')[-1], self.owltools_location)
-            self.uploads_data_to_gcs_bucket(input_filename.split('/')[-1])
-            self.uploads_data_to_gcs_bucket(output_filename.split('/')[-1])
+            ontology_file_formatter(self.temp_dir, '/' + in_filename.split('/')[-1], self.owltools_location)
+            ontology_file_formatter(self.temp_dir, '/' + out_filename.split('/')[-1], self.owltools_location)
+            uploads_data_to_gcs_bucket(self.bucket, self.processed_data, self.temp_dir, in_filename.split('/')[-1])
+            uploads_data_to_gcs_bucket(self.bucket, self.processed_data, self.temp_dir, out_filename.split('/')[-1])
         else:
-            raise ValueError('Reasoner Finished with Errors.')
+            logger.error('ERROR: Reasoner Finished with Errors - {}: {}'.format(in_filename, return_code))
+            raise Exception('ERROR: Reasoner Finished with Errors - {}: {}'.format(in_filename, return_code))
 
         return None
 
@@ -1204,12 +1205,13 @@ def constructs_human_protein_ontology(self) -> None:
         """
 
         print('Constructing a Human Version of the PRotein Ontology')
+        logger.info('Constructing a Human Version of the PRotein Ontology')
 
-        # download needed data
         networkx_mdg = self._processes_protein_ontology_data()
-        df_list = pandas.read_html(self.downloads_data_from_gcs_bucket('human_pro_classes.html'))
+        f_name = 'human_pro_classes.html'
+        x = downloads_data_from_gcs_bucket(self.bucket, self.original_data, self.processed_data, f_name, self.temp_dir)
+        df_list = pandas.read_html(x)
         human_pro_classes = list(df_list[-1]['PRO_term'])
-
         # create a new graph using breadth first search paths
         human_pro_graph, human_networkx_mdg = Graph(), networkx.MultiDiGraph()
         for node in tqdm(human_pro_classes):
@@ -1219,13 +1221,13 @@ def constructs_human_protein_ontology(self) -> None:
             for path in forward + reverse:
                 human_pro_graph.add((path[0], path[2], path[1]))
                 human_networkx_mdg.add_edge(path[0], path[1], path[2])
-
         # check data and write it locally
         if networkx.number_connected_components(human_networkx_mdg.to_undirected()) == 1:
             human_pro_graph.serialize(destination=self.temp_dir + '/human_pro.owl', format='xml')
             f_name1, f_name2 = self.temp_dir + '/human_pro.owl', self.temp_dir + '/pr_with_imports.owl'
             self._logically_verifies_human_protein_ontology(f_name1, f_name2, 'elk')
         else:
+            logger.error('ValueError: Human PRO Contains More than a Single Connected Component')
             raise ValueError('Human PRO Contains More than a Single Connected Component')
 
         return None
@@ -1239,27 +1241,28 @@ def processes_relation_ontology_data(self) -> None:
         """
 
         print('Creating Required Relations Ontology Data')
+        logger.info('Creating Required Relations Ontology Data')
 
-        ro_graph = Graph().parse(self.downloads_data_from_gcs_bucket('ro_with_imports.owl'))
+        f_name = 'ro_with_imports.owl'
+        x = downloads_data_from_gcs_bucket(self.bucket, self.original_data, self.processed_data, f_name, self.temp_dir)
+        ro_graph = Graph().parse(x)
         labs = {str(x[2]).lower(): str(x[0]) for x in ro_graph if '/RO_' in str(x[0]) and 'label' in str(x[1]).lower()}
-
         # identify relations and their inverses
-        write_dir, filename1 = './resources/construction_approach/', 'INVERSE_RELATIONS.txt'
+        filename1 = 'INVERSE_RELATIONS.txt'
         with open(self.temp_dir + '/' + filename1, 'w') as out1:
             out1.write('Relation' + '\t' + 'Inverse_Relation' + '\n')
             for s, p, o in ro_graph:
                 if 'owl#inverseOf' in str(p) and ('RO' in str(s) and 'RO' in str(o)):
                     out1.write(str(s.split('/')[-1]) + '\t' + str(o.split('/')[-1]) + '\n')
                     out1.write(str(o.split('/')[-1]) + '\t' + str(s.split('/')[-1]) + '\n')
-        self.uploads_data_to_gcs_bucket(filename1)
-
+        uploads_data_to_gcs_bucket(self.bucket, self.processed_data, self.temp_dir, filename1)
         # identify relation labels
         filename2 = 'RELATIONS_LABELS.txt'
         with open(self.temp_dir + '/' + filename2, 'w') as out1:
             out1.write('Relation' + '\t' + 'Label' + '\n')
             for k, v in labs.items():
                 out1.write(str(k).split('/')[-1] + '\t' + str(v) + '\n')
-        self.uploads_data_to_gcs_bucket(filename2)
+        uploads_data_to_gcs_bucket(self.bucket, self.processed_data, self.temp_dir, filename2)
 
         return None
 
@@ -1272,21 +1275,20 @@ def processes_clinvar_data(self) -> None:
         """
 
         print('Generating ClinVar Cross-Mapping Data')
+        logger.info('Generating ClinVar Cross-Mapping Data')
 
         f_name = 'variant_summary.txt'
-        clinvar_data = self.reads_gcs_bucket_data_to_df(filename=f_name, delm='\t', skip=0, head=0, sht=None)
+        clinvar_data = self.reads_gcs_bucket_data_to_df(f_name=f_name, delm='\t', skip=0, head=0, sht=None)
         clinvar_data.fillna('None', inplace=True)
-
         # explode nested data
         explode_df_clinvar = explodes_data(clinvar_data.copy(), ['PhenotypeIDS'], ';')
         explode_df_clinvar = explodes_data(explode_df_clinvar.copy(), ['PhenotypeIDS'], ',')
         explode_df_clinvar['PhenotypeIDS'].replace('Orphanet:ORPHA', 'ORPHA:', inplace=True, regex=True)
         explode_df_clinvar['PhenotypeIDS'].replace('Human Phenotype Ontology:HP:', 'HP_', inplace=True, regex=True)
-
         # write data
         filename = 'CLINVAR_VARIANT_GENE_DISEASE_PHENOTYPE_EDGES.txt'
         explode_df_clinvar.to_csv(self.temp_dir + '/' + filename, header=True, sep='\t', encoding='utf-8', index=False)
-        self.uploads_data_to_gcs_bucket(filename)
+        uploads_data_to_gcs_bucket(self.bucket, self.processed_data, self.temp_dir, filename)
 
         return None
 
@@ -1299,10 +1301,12 @@ def processes_cofactor_catalyst_data(self) -> None:
         """
 
         print('Creating Protein-Cofactor and Protein-Catalyst Cross-Mapping Data')
-
-        data = open(self.downloads_data_from_gcs_bucket('uniprot-cofactor-catalyst.tab')).readlines()
+        logger.info('Creating Protein-Cofactor and Protein-Catalyst Cross-Mapping Data')
 
         # reformat data and write data
+        f_name = 'uniprot-cofactor-catalyst.tab'
+        x = downloads_data_from_gcs_bucket(self.bucket, self.original_data, self.processed_data, f_name, self.temp_dir)
+        data = open(x).readlines()
         filename1, filename2 = 'UNIPROT_PROTEIN_COFACTOR.txt', 'UNIPROT_PROTEIN_CATALYST.txt'
         with open(self.temp_dir + '/' + filename1, 'w') as out1, open(self.temp_dir + '/' + filename2, 'w') as out2:
             for line in tqdm(data):
@@ -1315,14 +1319,14 @@ def processes_cofactor_catalyst_data(self) -> None:
                         chebi = j.split('[')[-1].replace(']', '').replace(':', '_')
                         out2.write('PR_' + line.split('\t')[3].strip(';') + '\t' + chebi + '\n')
         # push data to gsc bucket
-        self.uploads_data_to_gcs_bucket(filename1)
-        self.uploads_data_to_gcs_bucket(filename2)
+        uploads_data_to_gcs_bucket(self.bucket, self.processed_data, self.temp_dir, filename1)
+        uploads_data_to_gcs_bucket(self.bucket, self.processed_data, self.temp_dir, filename2)
 
         return None
 
     def _creates_gene_metadata_dict(self) -> Dict:
-        """Creates a dictionary to store labels, synonyms, and a description for each Entrez gene identifier present
-        in the input data file.
+        """Creates a dictionary to store labels, synonyms, and a description for each Entrez gene identifier present in
+        the input data file.
 
         Returns:
             gene_metadata_dict: A dict containing metadata that's keyed by Entrez gene identifier and whose values are
@@ -1334,13 +1338,14 @@ def _creates_gene_metadata_dict(self) -> Dict:
         """
 
         print('\t- Generating Metadata for Gene Identifiers')
+        logger.info('Generating Metadata for Gene Identifiers')
 
-        f_name = self.temp_dir + '/' + 'Homo_sapiens.gene_info'
-        data = pandas.read_csv(f_name, header=0, delimiter='\t', low_memory=False)
-        data = data.loc[data['#tax_id'].apply(lambda x: x == 9606)]
+        f_name = 'Homo_sapiens.gene_info'
+        x = downloads_data_from_gcs_bucket(self.bucket, self.original_data, self.processed_data, f_name, self.temp_dir)
+        data = pandas.read_csv(x, header=0, delimiter='\t', low_memory=False)
+        data = data.loc[data['#tax_id'].apply(lambda i: i == 9606)]
         data.fillna('None', inplace=True)
         data.replace('-', 'None', inplace=True, regex=False)
-
         # create metadata
         genes, lab, desc, syn = [], [], [], []
         for idx, row in tqdm(data.iterrows(), total=data.shape[0]):
@@ -1348,24 +1353,17 @@ def _creates_gene_metadata_dict(self) -> Dict:
             chrom, map_loc, s1, s2 = row['chromosome'], row['map_location'], row['Synonyms'], row['Other_designations']
             if gene_id != 'None':
                 genes.append('http://www.ncbi.nlm.nih.gov/gene/' + str(gene_id))
-                if sym != 'None' or sym != '':
-                    lab.append(sym)
-                else:
-                    lab.append('Entrez_ID:' + gene_id)
+                if sym != 'None' or sym != '': lab.append(sym)
+                else: lab.append('Entrez_ID:' + gene_id)
                 if 'None' not in [defn, gene_type, chrom, map_loc]:
                     desc_str = "{} has locus group '{}' and is located on chromosome {} ({})."
                     desc.append(desc_str.format(sym, gene_type, chrom, map_loc))
-                else:
-                    desc.append("{} locus group '{}'.".format(sym, gene_type))
+                else: desc.append("{} locus group '{}'.".format(sym, gene_type))
                 if s1 != 'None' and s2 != 'None':
                     syn.append('|'.join(set([x for x in (s1 + s2).split('|') if x != 'None' or x != ''])))
-                elif s1 != 'None':
-                    syn.append('|'.join(set([x for x in s1.split('|') if x != 'None' or x != ''])))
-                elif s2 != 'None':
-                    syn.append('|'.join(set([x for x in s2.split('|') if x != 'None' or x != ''])))
-                else:
-                    syn.append('None')
-
+                elif s1 != 'None': syn.append('|'.join(set([x for x in s1.split('|') if x != 'None' or x != ''])))
+                elif s2 != 'None': syn.append('|'.join(set([x for x in s2.split('|') if x != 'None' or x != ''])))
+                else: syn.append('None')
         # combine into new data frame then convert it to dictionary
         metadata = pandas.DataFrame(list(zip(genes, lab, desc, syn)), columns=['ID', 'Label', 'Description', 'Synonym'])
         metadata = metadata.astype(str)
@@ -1389,16 +1387,17 @@ def _creates_transcript_metadata_dict(self) -> Dict:
         """
 
         print('\t- Generating Metadata for Transcript Identifiers')
+        logger.info('Generating Metadata for Transcript Identifiers')
 
         f_name = 'ensembl_identifier_data_cleaned.txt'
+        x = downloads_data_from_gcs_bucket(self.bucket, self.original_data, self.processed_data, f_name, self.temp_dir)
         dup_cols = ['transcript_stable_id', 'transcript_name', 'ensembl_transcript_type']
-        data = pandas.read_csv(self.temp_dir + '/' + f_name, header=0, delimiter='\t', low_memory=False)
-        data = data.loc[data['transcript_stable_id'].apply(lambda x: x != 'None')]
+        data = pandas.read_csv(x, header=0, delimiter='\t', low_memory=False)
+        data = data.loc[data['transcript_stable_id'].apply(lambda i: i != 'None')]
         data.drop(['ensembl_gene_id', 'symbol', 'protein_stable_id', 'uniprot_id', 'master_transcript_type',
                    'entrez_id', 'ensembl_gene_type', 'master_gene_type', 'symbol'], axis=1, inplace=True)
         data.drop_duplicates(subset=dup_cols, keep='first', inplace=True)
         data.fillna('None', inplace=True)
-
         # create metadata
         rna, lab, desc, syn = [], [], [], []
         for idx, row in tqdm(data.iterrows(), total=data.shape[0]):
@@ -1439,11 +1438,13 @@ def _creates_variant_metadata_dict(self) -> Dict:
         """
 
         print('\t- Generating Metadata for Variant Identifiers')
+        logger.info('Generating Metadata for Variant Identifiers')
 
-        f_name = self.temp_dir + '/' + 'variant_summary.txt'
-        data = pandas.read_csv(f_name, header=0, delimiter='\t', low_memory=False)
-        data = data.loc[data['Assembly'].apply(lambda x: x == 'GRCh38')]
-        data = data.loc[data['RS# (dbSNP)'].apply(lambda x: x != -1)]
+        f_name = 'variant_summary.txt'
+        x = downloads_data_from_gcs_bucket(self.bucket, self.original_data, self.processed_data, f_name, self.temp_dir)
+        data = pandas.read_csv(x, header=0, delimiter='\t', low_memory=False)
+        data = data.loc[data['Assembly'].apply(lambda i: i == 'GRCh38')]
+        data = data.loc[data['RS# (dbSNP)'].apply(lambda i: i != -1)]
         data = data[['#AlleleID', 'Type', 'Name', 'ClinicalSignificance', 'RS# (dbSNP)', 'Origin', 'Start', 'Stop',
                      'ChromosomeAccession', 'Chromosome', 'ReferenceAllele', 'Assembly', 'AlternateAllele',
                      'Cytogenetic', 'ReviewStatus', 'LastEvaluated']]
@@ -1451,7 +1452,6 @@ def _creates_variant_metadata_dict(self) -> Dict:
         data.fillna('None', inplace=True)
         data.sort_values('LastEvaluated', ascending=False, inplace=True)
         data.drop_duplicates(subset='RS# (dbSNP)', keep='first', inplace=True)
-
         # create metadata
         var, label, desc, syn = [], [], [], []
         for idx, row in tqdm(data.iterrows(), total=data.shape[0]):
@@ -1469,7 +1469,6 @@ def _creates_variant_metadata_dict(self) -> Dict:
                                 row['ClinicalSignificance'], row['Assembly'], row['LastEvaluated'],
                                 row['ReviewStatus']).replace('None', 'UNKNOWN'))
                 syn.append('None')
-
         # combine into new data frame then convert it to dictionary
         metadata = pandas.DataFrame(list(zip(var, label, desc, syn)), columns=['ID', 'Label', 'Description', 'Synonym'])
         metadata.drop_duplicates(subset=None, keep='first', inplace=True)
@@ -1495,19 +1494,23 @@ def _creates_pathway_metadata_dict(self) -> Dict:
         """
 
         print('\t- Generating Metadata for Pathway Identifiers')
+        logger.info('Generating Metadata for Pathway Identifiers')
 
         # reactome pathways
-        f_name = self.temp_dir + '/' + 'ReactomePathways.txt'
-        data = pandas.read_csv(f_name, header=None, delimiter='\t', low_memory=False)
+        f_name = 'ReactomePathways.txt'
+        f = downloads_data_from_gcs_bucket(self.bucket, self.original_data, self.processed_data, f_name, self.temp_dir)
+        data = pandas.read_csv(f, header=None, delimiter='\t', low_memory=False)
         data = data.loc[data[2].apply(lambda x: x == 'Homo sapiens')]
         # reactome gene association data
-        f_name1 = self.temp_dir + '/' + 'gene_association.reactome'
-        data1 = pandas.read_csv(f_name1, header=None, delimiter='\t', skiprows=1, low_memory=False)
+        f_name1 = 'gene_association.reactome'
+        g = downloads_data_from_gcs_bucket(self.bucket, self.original_data, self.processed_data, f_name1, self.temp_dir)
+        data1 = pandas.read_csv(g, header=None, delimiter='\t', skiprows=1, low_memory=False)
         data1 = data1.loc[data1[12].apply(lambda x: x == 'taxon:9606')]
         data1[5].replace('REACTOME:', '', inplace=True, regex=True)
         # reactome CHEBI data
-        f_name2 = self.temp_dir + '/' + 'ChEBI2Reactome_All_Levels.txt'
-        data2 = pandas.read_csv(f_name2, header=None, delimiter='\t', low_memory=False)
+        f_name2 = 'ChEBI2Reactome_All_Levels.txt'
+        h = downloads_data_from_gcs_bucket(self.bucket, self.original_data, self.processed_data, f_name2, self.temp_dir)
+        data2 = pandas.read_csv(h, header=None, delimiter='\t', low_memory=False)
         data2 = data2.loc[data2[5].apply(lambda x: x == 'Homo sapiens')]
 
         # set unique node list
@@ -1535,13 +1538,17 @@ def _creates_relations_metadata_dict(self) -> Dict:
                     'Synonym': 'None'}}, ...}
         """
 
+        print('\t- Generating Metadata for Relations Identifiers')
+        logger.info('Generating Metadata for Relations Identifiers')
+
         # get ontology information
-        ro_graph = Graph().parse(self.downloads_data_from_gcs_bucket('ro_with_imports.owl'))
+        f_name = 'ro_with_imports.owl'
+        x = downloads_data_from_gcs_bucket(self.bucket, self.original_data, self.processed_data, f_name, self.temp_dir)
+        ro_graph = Graph().parse(x)
         relation_metadata_dict, obo = {}, Namespace('http://purl.obolibrary.org/obo/')
         cls = [x for x in gets_ontology_classes(ro_graph) if '/RO_' in str(x)] + \
               [x for x in gets_object_properties(ro_graph) if '/RO_' in str(x)]
         master_synonyms = [x for x in ro_graph if 'synonym' in str(x[1]).lower() and isinstance(x[0], URIRef)]
-
         for x in tqdm(cls):
             cls_label = list(ro_graph.objects(x, RDFS.label))
             labels = str(cls_label[0]) if len(cls_label) > 0 else 'None'
@@ -1574,6 +1581,7 @@ def creates_non_ontology_class_metadata_dict(self) -> None:
         """
 
         print('Creating Master Metadata Dictionary for Non-Ontology Entities')
+        logger.info('Creating Master Metadata Dictionary for Non-Ontology Entities')
 
         # create single dictionary of
         master_metadata_dictionary = {'nodes': {**self._creates_gene_metadata_dict(),
@@ -1581,11 +1589,10 @@ def creates_non_ontology_class_metadata_dict(self) -> None:
                                                 **self._creates_variant_metadata_dict(),
                                                 **self._creates_pathway_metadata_dict()},
                                       'relations': self._creates_relations_metadata_dict()}
-
         # save data and push to gcs bucket
         filename = 'node_metadata_dict.pkl'
         pickle.dump(master_metadata_dictionary, open(self.temp_dir + '/' + filename, 'wb'), protocol=4)
-        self.uploads_data_to_gcs_bucket(filename)
+        uploads_data_to_gcs_bucket(self.bucket, self.processed_data, self.temp_dir, filename)
 
         return None
 
@@ -1601,43 +1608,63 @@ def preprocesses_build_data(self) -> None:
 
         # STEP 1: Human Transcript, Gene, and Protein Identifier Mapping
         print('\nSTEP 1: HUMAN TRANSCRIPT, GENE, PROTEIN IDENTIFIER MAPPING')
+        logger.info('STEP 1: HUMAN TRANSCRIPT, GENE, PROTEIN IDENTIFIER MAPPING')
         self.generates_specific_genomic_identifier_maps()
+        uploads_data_to_gcs_bucket(self.bucket, self.current_build, log_dir, log)
 
         # STEP 2: MeSH-ChEBI Identifier Mapping
-        print('\STEP 2: MESH-CHEBI IDENTIFIER MAPPING')
+        print('\nSTEP 2: MESH-CHEBI IDENTIFIER MAPPING')
+        logger.info('STEP 2: MESH-CHEBI IDENTIFIER MAPPING')
         self.creates_chebi_to_mesh_identifier_mappings()
+        uploads_data_to_gcs_bucket(self.bucket, self.current_build, log_dir, log)
 
         # STEP 3: Disease and Phenotype Identifier Mapping
         print('\nSTEP 3: DISEASE-PHENOTYPE IDENTIFIER MAPPING')
+        logger.info('STEP 3: DISEASE-PHENOTYPE IDENTIFIER MAPPING')
         self.creates_disease_identifier_mappings()
+        uploads_data_to_gcs_bucket(self.bucket, self.current_build, log_dir, log)
 
         # STEP 4: Human Protein Atlas/GTEx Tissue/Cells Edge Data
         print('\nSTEP 4: CREATING HPA + GTEX IDENTIFIER EDGE DATA')
+        logger.info('STEP 4: CREATING HPA + GTEX IDENTIFIER EDGE DATA')
         self._hpa_gtex_ontology_alignment()
         self.processes_hpa_gtex_data()
+        uploads_data_to_gcs_bucket(self.bucket, self.current_build, log_dir, log)
 
         # STEP 5: Creating Pathway and Sequence Ontology Mappings
         print('\nSTEP 5: PATHWAY + SEQUENCE ONTOLOGY IDENTIFIER MAPPING')
+        logger.info('STEP 5: PATHWAY + SEQUENCE ONTOLOGY IDENTIFIER MAPPING')
         self.combines_pathway_and_sequence_ontology_dictionaries()
+        uploads_data_to_gcs_bucket(self.bucket, self.current_build, log_dir, log)
 
         # STEP 6: Creating a Human Protein Ontology
         print('\nSTEP 6: CREATING A HUMAN PROTEIN ONTOLOGY')
+        logger.info('STEP 6: CREATING A HUMAN PROTEIN ONTOLOGY')
         self.constructs_human_protein_ontology()
+        uploads_data_to_gcs_bucket(self.bucket, self.current_build, log_dir, log)
 
         # STEP 7: Extracting Relations Ontology Information
         print('\nSTEP 7: EXTRACTING RELATION ONTOLOGY INFORMATION')
+        logger.info('STEP 7: EXTRACTING RELATION ONTOLOGY INFORMATION')
         self.processes_relation_ontology_data()
+        uploads_data_to_gcs_bucket(self.bucket, self.current_build, log_dir, log)
 
         # STEP 8: Clinvar Variant-Diseases and Phenotypes Edge Data
         print('\nSTEP 8: CREATING CLINVAR VARIANT, DISEASE, PHENOTYPE DATA')
+        logger.info('STEP 8: CREATING CLINVAR VARIANT, DISEASE, PHENOTYPE DATA')
         self.processes_clinvar_data()
+        uploads_data_to_gcs_bucket(self.bucket, self.current_build, log_dir, log)
 
         # STEP 9: Uniprot Protein-Cofactor and Protein-Catalyst Edge Data
         print('\nSTEP 9: CREATING COFACTOR + CATALYST EDGE DATA')
+        logger.info('STEP 9: CREATING COFACTOR + CATALYST EDGE DATA')
         self.processes_cofactor_catalyst_data()
+        uploads_data_to_gcs_bucket(self.bucket, self.current_build, log_dir, log)
 
         # STEP 10: Non-Ontology Metadata Dictionary
-        print('\nSTEP 10: CREATING ONO-ONTOLOGY METADATA DICTIONARY')
+        print('\nSTEP 10: CREATING OBO-ONTOLOGY METADATA DICTIONARY')
+        logger.info('STEP 10: CREATING OBO-ONTOLOGY METADATA DICTIONARY')
         self.creates_non_ontology_class_metadata_dict()
+        uploads_data_to_gcs_bucket(self.bucket, self.current_build, log_dir, log)
 
         return None
diff --git a/builds/data_to_download.txt b/builds/data_to_download.txt
old mode 100644
new mode 100755
index 78d03e45d..62aa1175a
--- a/builds/data_to_download.txt
+++ b/builds/data_to_download.txt
@@ -27,7 +27,7 @@ disease_mappings.tsv, https://www.disgenet.org/static/disgenet_ap1/files/downloa
 # human protein atlas/gtex tissue/cells - uberon + cell ontology + cell line ontology
 proteinatlas_search.tsv.gz, https://www.proteinatlas.org/api/search_download.php?search=&columns=g,eg,up,pe,rnatsm,rnaclsm,rnacasm,rnabrsm,rnabcsm,rnablsm,scl,t_RNA_adipose_tissue,t_RNA_adrenal_gland,t_RNA_amygdala,t_RNA_appendix,t_RNA_basal_ganglia,t_RNA_bone_marrow,t_RNA_breast,t_RNA_cerebellum,t_RNA_cerebral_cortex,t_RNA_cervix,_uterine,t_RNA_colon,t_RNA_corpus_callosum,t_RNA_ductus_deferens,t_RNA_duodenum,t_RNA_endometrium_1,t_RNA_epididymis,t_RNA_esophagus,t_RNA_fallopian_tube,t_RNA_gallbladder,t_RNA_heart_muscle,t_RNA_hippocampal_formation,t_RNA_hypothalamus,t_RNA_kidney,t_RNA_liver,t_RNA_lung,t_RNA_lymph_node,t_RNA_midbrain,t_RNA_olfactory_region,t_RNA_ovary,t_RNA_pancreas,t_RNA_parathyroid_gland,t_RNA_pituitary_gland,t_RNA_placenta,t_RNA_pons_and_medulla,t_RNA_prostate,t_RNA_rectum,t_RNA_retina,t_RNA_salivary_gland,t_RNA_seminal_vesicle,t_RNA_skeletal_muscle,t_RNA_skin_1,t_RNA_small_intestine,t_RNA_smooth_muscle,t_RNA_spinal_cord,t_RNA_spleen,t_RNA_stomach_1,t_RNA_testis,t_RNA_thalamus,t_RNA_thymus,t_RNA_thyroid_gland,t_RNA_tongue,t_RNA_tonsil,t_RNA_urinary_bladder,t_RNA_vagina,t_RNA_B-cells,t_RNA_dendritic_cells,t_RNA_granulocytes,t_RNA_monocytes,t_RNA_NK-cells,t_RNA_T-cells,t_RNA_total_PBMC,cell_RNA_A-431,cell_RNA_A549,cell_RNA_AF22,cell_RNA_AN3-CA,cell_RNA_ASC_diff,cell_RNA_ASC_TERT1,cell_RNA_BEWO,cell_RNA_BJ,cell_RNA_BJ_hTERT+,cell_RNA_BJ_hTERT+_SV40_Large_T+,cell_RNA_BJ_hTERT+_SV40_Large_T+_RasG12V,cell_RNA_CACO-2,cell_RNA_CAPAN-2,cell_RNA_Daudi,cell_RNA_EFO-21,cell_RNA_fHDF/TERT166,cell_RNA_HaCaT,cell_RNA_HAP1,cell_RNA_HBEC3-KT,cell_RNA_HBF_TERT88,cell_RNA_HDLM-2,cell_RNA_HEK_293,cell_RNA_HEL,cell_RNA_HeLa,cell_RNA_Hep_G2,cell_RNA_HHSteC,cell_RNA_HL-60,cell_RNA_HMC-1,cell_RNA_HSkMC,cell_RNA_hTCEpi,cell_RNA_hTEC/SVTERT24-B,cell_RNA_hTERT-HME1,cell_RNA_HUVEC_TERT2,cell_RNA_K-562,cell_RNA_Karpas-707,cell_RNA_LHCN-M2,cell_RNA_MCF7,cell_RNA_MOLT-4,cell_RNA_NB-4,cell_RNA_NTERA-2,cell_RNA_PC-3,cell_RNA_REH,cell_RNA_RH-30,cell_RNA_RPMI-8226,cell_RNA_RPTEC_TERT1,cell_RNA_RT4,cell_RNA_SCLC-21H,cell_RNA_SH-SY5Y,cell_RNA_SiHa,cell_RNA_SK-BR-3,cell_RNA_SK-MEL-30,cell_RNA_T-47d,cell_RNA_THP-1,cell_RNA_TIME,cell_RNA_U-138_MG,cell_RNA_U-2_OS,cell_RNA_U-2197,cell_RNA_U-251_MG,cell_RNA_U-266/70,cell_RNA_U-266/84,cell_RNA_U-698,cell_RNA_U-87_MG,cell_RNA_U-937,cell_RNA_WM-115,blood_RNA_basophil,blood_RNA_classical_monocyte,blood_RNA_eosinophil,blood_RNA_gdT-cell,blood_RNA_intermediate_monocyte,blood_RNA_MAIT_T-cell,blood_RNA_memory_B-cell,blood_RNA_memory_CD4_T-cell,blood_RNA_memory_CD8_T-cell,blood_RNA_myeloid_DC,blood_RNA_naive_B-cell,blood_RNA_naive_CD4_T-cell,blood_RNA_naive_CD8_T-cell,blood_RNA_neutrophil,blood_RNA_NK-cell,blood_RNA_non-classical_monocyte,blood_RNA_plasmacytoid_DC,blood_RNA_T-reg,blood_RNA_total_PBMC,brain_RNA_amygdala,brain_RNA_basal_ganglia,brain_RNA_cerebellum,brain_RNA_cerebral_cortex,brain_RNA_hippocampal_formation,brain_RNA_hypothalamus,brain_RNA_midbrain,brain_RNA_olfactory_region,brain_RNA_pons_and_medulla,brain_RNA_thalamus&format=tsv
 GTEx_Analysis_2017-06-05_v8_RNASeQCv1.1.9_gene_median_tpm.gct, https://storage.googleapis.com/gtex_analysis_v8/rna_seq_data/GTEx_Analysis_2017-06-05_v8_RNASeQCv1.1.9_gene_median_tpm.gct.gz
-zooma_tissue_cell_mapping_04JAN2020.xlsx, https://storage.googleapis.com/pheknowlator/release_v2.0.0/curated_data/zooma_tissue_cell_mapping_04JAN2020.xlsx
+zooma_tissue_cell_mapping_04JAN2020.xlsx, https://storage.googleapis.com/pheknowlator/curated_data/zooma_tissue_cell_mapping_04JAN2020.xlsx
 # mapping reactome pathways to the pathway ontology
 ReactomePathways.txt, https://reactome.org/download/current/ReactomePathways.txt
 gene_association.reactome, https://reactome.org/download/current/gene_association.reactome.gz
@@ -35,7 +35,7 @@ ChEBI2Reactome_All_Levels.txt, https://reactome.org/download/current/ChEBI2React
 compath_canonical_pathway_mappings.txt, http://compath.scai.fraunhofer.de/export_mappings
 kegg_reactome.csv, https://raw.githubusercontent.com/ComPath/resources/master/mappings/kegg_reactome.csv
 # mapping genomic identifiers to the sequence ontology
-genomic_sequence_ontology_mappings.xlsx, https://storage.googleapis.com/pheknowlator/release_v2.0.0/curated_data/genomic_sequence_ontology_mappings.xlsx
+genomic_sequence_ontology_mappings.xlsx, https://storage.googleapis.com/pheknowlator/curated_data/genomic_sequence_ontology_mappings.xlsx
 # protein ontology consortium sparql query results
 human_pro_classes.html, https://sparql.proconsortium.org/virtuoso/sparql?query=PREFIX+obo%3A+%3Chttp%3A%2F%2Fpurl.obolibrary.org%2Fobo%2F%3E%0D%0A%0D%0ASELECT+%3FPRO_term%0D%0AFROM+%3Chttp%3A%2F%2Fpurl.obolibrary.org%2Fobo%2Fpr%3E%0D%0AWHERE+%7B%0D%0A+++++++%3FPRO_term+rdf%3Atype+owl%3AClass+.%0D%0A+++++++%3FPRO_term+rdfs%3AsubClassOf+%3Frestriction+.%0D%0A+++++++%3Frestriction+owl%3AonProperty+obo%3ARO_0002160+.%0D%0A+++++++%3Frestriction+owl%3AsomeValuesFrom+obo%3ANCBITaxon_9606+.%0D%0A%0D%0A+++++++%23+use+this+to+filter-out+things+like+hgnc+ids%0D%0A+++++++FILTER+%28regex%28%3FPRO_term%2C%22http%3A%2F%2Fpurl.obolibrary.org%2Fobo%2F*%22%29%29+.%0D%0A%7D&format=text%2Fhtml&debug=
 # clinvar variant-diseases and phenotypes
@@ -44,7 +44,6 @@ variant_summary.txt, ftp://ftp.ncbi.nlm.nih.gov/pub/clinvar/tab_delimited/varian
 uniprot-cofactor-catalyst.tab, https://www.uniprot.org/uniprot/?query=&fil=organism%3A%22Homo%20sapiens%20(Human)%20%5B9606%5D%22&columns=id%2Creviewed%2Centry%20name%2Cdatabase(PRO)%2Cchebi(Cofactor)%2Cchebi(Catalytic%20activity)&format=tab
 
 # edge source list
-CTD_chemicals_diseases.tsv, http://ctdbase.org/reports/CTD_chemicals_diseases.tsv.gz
 CTD_chem_gene_ixns.tsv, http://ctdbase.org/reports/CTD_chem_gene_ixns.tsv.gz
 CTD_chem_go_enriched.tsv, http://ctdbase.org/reports/CTD_chem_go_enriched.tsv.gz
 ChEBI2Reactome_All_Levels.txt, https://reactome.org/download/current/ChEBI2Reactome_All_Levels.txt
@@ -59,5 +58,4 @@ UniProt2Reactome_All_Levels.txt, https://reactome.org/download/current/UniProt2R
 9606.protein.links.v11.0.txt, https://stringdb-static.org/download/protein.links.v11.0/9606.protein.links.v11.0.txt.gz
 
 # local build data
-OWL_NETS_Property_Types.txt, https://storage.googleapis.com/pheknowlator/release_v2.0.0/curated_data/OWL_NETS_Property_Types.txt
-genomic_typing_dict.pkl, https://storage.googleapis.com/pheknowlator/release_v2.0.0/curated_data/genomic_typing_dict.pkl
\ No newline at end of file
+genomic_typing_dict.pkl, https://storage.googleapis.com/pheknowlator/curated_data/genomic_typing_dict.pkl
\ No newline at end of file
diff --git a/builds/job_monitoring.py b/builds/job_monitoring.py
new file mode 100755
index 000000000..9b450c270
--- /dev/null
+++ b/builds/job_monitoring.py
@@ -0,0 +1,124 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# import needed libraries
+import click
+import json
+import os
+import re
+import subprocess
+import time
+
+from datetime import datetime
+from json import JSONDecodeError
+from pkt_kg.utils import data_downloader
+
+
+def monitor_ai_platform_jobs(project, job, sleep):
+    """Monitors PheKnowLator build jobs for Phases 1-2 of the build process. Method monitors jobs by querying GCloud
+    for the current status of a job running on the AI Platform.
+
+    Args:
+        project: A string containing the name of the Google project.
+        job: A string containing the name of the job submitted to the Google AI Platform.
+        sleep: An integer containing the number of seconds to wait between job status checks.
+
+    Returns:
+        state: A string containing the job's exit status.
+    """
+
+    # create project identifier
+    job_id = 'projects/{}/jobs/{}'.format(project, job)
+
+    # set pattern to extract job state
+    state_pattern, query = r'(?<=state:\s).*(?=\\ntrainingInput)', "gcloud ai-platform jobs describe {}".format(job_id)
+
+    # query job status
+    start_time, state = datetime.now(), 'QUEUED'
+    while state in ['RUNNING', 'QUEUED', 'PREPARING']:
+        time.sleep(sleep)
+        current_time = str(datetime.now().strftime('%b %d %Y %I:%M%p'))
+        state = re.findall(state_pattern, str(subprocess.check_output(query, shell=True)))[0]
+        elapsed_minutes = round((datetime.now() - start_time).total_seconds() / 60, 3)
+        print('Job Status: {} @ {} -- Elapsed Time (min): {}'.format(state, current_time, elapsed_minutes))
+
+    # print process completion information
+    final_state = re.findall(state_pattern, str(subprocess.check_output(query, shell=True)))[0]
+    if state == 'FAILED': raise Exception('Job Failed: {}'.format(final_state))
+    else: state = 'COMPLETED'
+
+    return state
+
+
+def monitor_gce_jobs(phase, sleep, gcs_log_location):
+    """Monitors PheKnowLator build jobs for Phase 2 of the build process. Method monitors jobs by querying the
+    current_build directory of the current release in the dedicated project Google Cloud Storage Bucket.
+
+    Args:
+        phase: An integer representing the build phase (i.e. 1 for "Phases1-2" or 3 for "Phase 3").
+        sleep: An integer containing the number of seconds to wait between job status checks.
+        gcs_log_location: A string containing a Google Cloud Storage file path and the name of the log file to monitor.
+
+    Returns:
+        status: A string containing the job's exit status.
+    """
+
+    quit_status = 'EXIT BUILD PHASES 1-2' if phase == 1 else 'EXIT BUILD PHASE 3'
+    master_log_content = []
+
+    # query job status
+    log_content, start_time, status = None, datetime.now(), 'RUNNING'
+    while status == 'RUNNING':
+        time.sleep(sleep)
+        current_time = str(datetime.now().strftime('%b %d %Y %I:%M%p'))
+        data_downloader(gcs_log_location, '.' + '/')  # download log and retrieve program status
+        try:
+            log_content = [json.loads(x) for x in open(gcs_log_location.split('/')[-1])]
+            messages = ' '.join([x['message'] for x in log_content])
+            if len([x for x in log_content if x['levelname'] == 'ERROR']) > 0: status = 'FAILED'
+            elif quit_status in messages: status = 'COMPLETED'
+            else: status = 'RUNNING'
+        except JSONDecodeError: status, log_content = 'RUNNING', ['QUEUED: Instance is Queued or being Prepared']
+        elapsed_minutes = round((datetime.now() - start_time).total_seconds() / 60, 3)
+        print('\n\nJob Status: {} @ {} -- Elapsed Time (min): {}\n'.format(status, current_time, elapsed_minutes))
+
+        # print log to console -- printing this to update logs via GitHub Actions console
+        for event in log_content:
+            if event not in master_log_content:
+                print(event)
+        master_log_content += log_content  # ensures we are only printing new content to the GitHub Actions console
+
+    return status
+
+
+@click.command()
+@click.option('--gce_type', prompt='Indicate GCE Instance Type (e.g. "reg", "ai").', default='reg')
+@click.option('--phase', prompt='Integer Representing the Build Phase (i.e. "1" or "3").', type=int)
+@click.option('--sleep', prompt='Time in Seconds to Sleep when Monitoring', default=60, type=int)
+@click.option('--gcs_dir', prompt="Write directory within GCS current_build directory.", required=False, default='')
+@click.option('--project', prompt='Google Project Identifier', required=False, default='')
+@click.option('--job', prompt='Google Job Name', required=False, default='')
+def main(gce_type, phase, sleep, gcs_dir, project, job):
+
+    start_time = datetime.now()
+
+    # set log name -- used for monitoring "reg" or regular GCE instances and gcs log location
+    if phase == 1:
+        gcs_url_string = 'https://storage.googleapis.com/pheknowlator/current_build/{}'
+        gcs_log_location = gcs_url_string.format('pkt_builder_phases12_log.log')
+    else:
+        gcs_url_string = 'https://storage.googleapis.com/pheknowlator/current_build/knowledge_graphs/{}/{}'
+        gcs_log_location = gcs_url_string.format(gcs_dir, 'pkt_build_log.log')
+
+    # identify build phase and activate job monitoring
+    if gce_type == "ai": state = monitor_ai_platform_jobs(project=project, job=job, sleep=sleep)
+    else: state = monitor_gce_jobs(phase=phase, sleep=sleep, gcs_log_location=gcs_log_location)
+
+    # print job run information
+    current_time = str(datetime.now().strftime('%b %d %Y %I:%M%p'))
+    elapsed_minutes = round((datetime.now() - start_time).total_seconds() / 60, 3)
+    print('\nJOB STATUS: {}! {} -- Total Elapsed Time (min): {}'.format(state, current_time, elapsed_minutes))
+
+
+if __name__ == '__main__':
+    main()
diff --git a/builds/logging.ini b/builds/logging.ini
new file mode 100755
index 000000000..bb8e684de
--- /dev/null
+++ b/builds/logging.ini
@@ -0,0 +1,32 @@
+;source: https://www.datadoghq.com/blog/python-logging-best-practices/
+[loggers]
+keys=root
+
+[handlers]
+keys=fileHandler, consoleHandler
+
+[formatters]
+keys=simpleFormatter,json
+
+[logger_root]
+level=INFO
+handlers=fileHandler
+
+[handler_consoleHandler]
+class=StreamHandler
+level=DEBUG
+formatter=simpleFormatter
+args=(sys.stdout,)
+
+[handler_fileHandler]
+class=FileHandler
+level=INFO
+formatter=json
+args=('%(log_file)s',)
+
+[formatter_json]
+class=pythonjsonlogger.jsonlogger.JsonFormatter
+format=%(asctime)s : %(levelname)s : %(name)s : %(module)s : %(funcName)s : %(lineno)d : %(message)s
+
+[formatter_simpleFormatter]
+format=%(asctime)s : %(levelname)s : %(name)s : %(module)s : %(funcName)s : %(message)s
\ No newline at end of file
diff --git a/builds/ontology_cleaning.py b/builds/ontology_cleaning.py
old mode 100644
new mode 100755
index 9c8a54d59..09cf8eab8
--- a/builds/ontology_cleaning.py
+++ b/builds/ontology_cleaning.py
@@ -5,9 +5,11 @@
 import datetime
 import fnmatch
 import glob
+import logging.config
 import pickle
 import os
 import re
+import subprocess
 
 from google.cloud import storage  # type: ignore
 from owlready2 import get_ontology, OwlReadyOntologyParsingError  # type: ignore
@@ -17,12 +19,19 @@
 from typing import Dict, List, Optional, Union
 
 # import script containing helper functions
+from builds.build_utilities import downloads_data_from_gcs_bucket, uploads_data_to_gcs_bucket
 from pkt_kg.utils import *
 
-# set global namespace
+# set environment variables
+# global namespace
 schema = Namespace('http://www.w3.org/2001/XMLSchema#')
 obo = Namespace('http://purl.obolibrary.org/obo/')
 oboinowl = Namespace('http://www.geneontology.org/formats/oboInOwl#')
+# logging
+log_dir, log, log_config = 'builds/logs', 'pkt_builder_phases12_log.log', glob.glob('**/logging.ini', recursive=True)
+if not os.path.exists(log_dir): os.mkdir(log_dir)
+logger = logging.getLogger(__name__)
+logging.config.fileConfig(log_config[0], disable_existing_loggers=False, defaults={'log_file': log_dir + '/' + log})
 
 
 class OntologyCleaner(object):
@@ -43,8 +52,10 @@ def __init__(self, gcs_bucket: Union[storage.bucket.Bucket, str], org_data: str,
         self.bucket: Union[storage.bucket.Bucket, str] = gcs_bucket
         self.original_data: str = org_data
         self.processed_data: str = proc_data
-        # SETTING LOCAL VARIABLES
-        self.owltools_location = './pkt_kg/libs/owltools'
+        self.current_build: str = 'current_build/'
+        # LOCAL VARIABLES
+        self.owltools_location = './builds/owltools'
+        # self.owltools_location = './pkt_kg/libs/owltools'
         self.temp_dir = temp
         self.merged_ontology_filename: str = 'PheKnowLator_MergedOntologies.owl'
         # ONTOLOGY INFORMATION DICTIONARY
@@ -58,8 +69,7 @@ def __init__(self, gcs_bucket: Union[storage.bucket.Bucket, str], org_data: str,
         self.ont_graph: Graph = Graph()
         self.ontology_classes: List = []
         # GENE IDENTIFIER DATA
-        # keep list of withdrawn hgnc gene ids in HPO that need updating
-        self.withdrawn_genes = {  # keep list of withdrawn hgnc gene ids in HPO that need updating
+        self.withdrawn_map = {  # keep list of withdrawn hgnc gene ids in HPO that need updating
             'http://www.genenames.org/cgi-bin/gene_symbol_report?hgnc_id=21508': ['653067', '653220'],
             'http://www.genenames.org/cgi-bin/gene_symbol_report?hgnc_id=23418': ['653450'],
             'http://www.genenames.org/cgi-bin/gene_symbol_report?hgnc_id=18372': ['653067', '653220'],
@@ -70,73 +80,81 @@ def __init__(self, gcs_bucket: Union[storage.bucket.Bucket, str], org_data: str,
             'http://www.genenames.org/cgi-bin/gene_symbol_report?hgnc_id=33870': ['114112'],
             'http://www.genenames.org/cgi-bin/gene_symbol_report?hgnc_id=8103': ['10896'],
             'http://www.genenames.org/cgi-bin/gene_symbol_report?hgnc_id=26619': ['5058'],
-            'http://www.genenames.org/cgi-bin/gene_symbol_report?hgnc_id=31424': ['101362076']
-        }
+            'http://www.genenames.org/cgi-bin/gene_symbol_report?hgnc_id=31424': ['101362076'],
+            'http://identifiers.org/hgnc/12764': ['7467'],
+            'http://identifiers.org/hgnc/1881': ['10167']}
         f_data = self.temp_dir + '/Merged_gene_rna_protein_identifiers.pkl'
         if not os.path.exists(f_data):
-            url = 'https://storage.googleapis.com/pheknowlator/release_v2.0.0/current_build/data/processed_data'
+            url = 'https://storage.googleapis.com/pheknowlator/{}data/processed_data/'.format(self.current_build)
             data_downloader(url + f_data.split('/')[-1], self.temp_dir + '/')
         with open(f_data, 'rb') as out:
             self.gene_ids = pickle.load(out, encoding='bytes')
 
-    def uploads_data_to_gcs_bucket(self, file_loc: str) -> None:
-        """Takes a file name and pushes the corresponding data referenced by the filename object from a local
-        temporary directory to a Google Cloud Storage bucket.
+    def reads_gcs_bucket_data_to_graph(self, f_name: str) -> Graph:
+        """Reads data corresponding to the input file_location variable into a Pandas DataFrame.
 
         Args:
-            file_loc: A string containing the name of file to write to a Google Cloud Storage bucket.
+            f_name: A string containing the name of file that exists in a Google Cloud Storage bucket.
 
         Returns:
-            None.
+             graph: An RDFLib graph object containing data read from a Google Cloud Storage bucket.
         """
 
-        if isinstance(self.bucket, storage.bucket.Bucket):
-            blob = self.bucket.blob(self.processed_data + file_loc)
-            blob.upload_from_filename(self.temp_dir + '/' + file_loc)
-
-        return None
+        x = downloads_data_from_gcs_bucket(self.bucket, self.original_data, self.processed_data, f_name, self.temp_dir)
+        graph = Graph().parse(x, format='xml')
 
-    def downloads_data_from_gcs_bucket(self, filename: str) -> Optional[str]:
-        """Takes a filename and and downloads the corresponding data to a local temporary directory, if it has not
-        already been downloaded.
+        return graph
 
-        Args:
-            filename: A string containing the name of file to write to a Google Cloud Storage bucket.
+    def checks_for_downloaded_ontology_data(self) -> List:
+        """Checks to see if the processed ontology files are in the current temp directory. If not, they are downloaded
+        from the processed_data bucket for the current build. The local path to each downloaded ontology file is added
+        to a list which is returned. This method is meant to be a helper method to assist with the automatic build
+        process should something interrupt the build between the individual and merged ontology processing steps.
 
         Returns:
-            data_file: A string containing the local filepath for a file downloaded from a GSC bucket.
-
-        Raises:
-            ValueError: when trying to download a non-existent file from the GCS original_data dir of the current build.
+            ont_list: A list of strings where each string is
         """
 
-        if isinstance(self.bucket, storage.bucket.Bucket):
-            try:
-                _files = [_.name for _ in self.bucket.list_blobs(prefix=self.original_data)]
-                match_file = fnmatch.filter(_files, '*/' + filename)[0]  # poor man's glob of bucket file directory
-                data_file = self.temp_dir + '/' + match_file.split('/')[-1]
-                if not os.path.exists(data_file):  # only download if file has not yet been downloaded
-                    self.bucket.blob(match_file).download_to_filename(self.temp_dir + '/' + match_file.split('/')[-1])
-            except IndexError:
-                raise ValueError('{} Not in the GCS original_data Directory of the Current Build'.format(filename))
-            return data_file
-        else:
-            return None
+        onts = [self.temp_dir + '/' + x for x in list(self.ontology_info.keys()) if x != self.merged_ontology_filename]
+        if len(onts) == 0 and isinstance(self.bucket, storage.bucket.Bucket):
+            # look in bucket for ontology files
+            _files = [_.name for _ in self.bucket.list_blobs(prefix=self.processed_data)]
+            ont_list = [x.split('/')[-1] for x in fnmatch.filter(_files, '*/*_with_imports.owl')]
+            # download the files to local temp directory
+            for ont in ont_list:
+                downloads_data_from_gcs_bucket(self.bucket, self.original_data, self.processed_data, ont, self.temp_dir)
+        else: ont_list = onts
 
-    def reads_gcs_bucket_data_to_graph(self, file_location: str) -> Graph:
-        """Reads data corresponding to the input file_location variable into a Pandas DataFrame.
+        return ont_list
+
+    def merge_ontologies(self, ontology_files: List[str], write_location: str, merged_ont_kg: str) -> Graph:
+        """Using the OWLTools API, each ontology listed in in the ontologies attribute is recursively merged with into a
+        master merged ontology file and saved locally to the provided file path via the merged_ontology attribute. The
+        function assumes that the file is written to the directory specified by the write_location attribute.
 
         Args:
-            file_location: A string containing the name of file that exists in a Google Cloud Storage bucket.
+            ontology_files: A list of ontology file paths.
+            write_location: A string pointing to a local directory for writing data.
+            merged_ont_kg: A string pointing to the location of the merged ontology file.
 
         Returns:
-             graph: An RDFLib graph object containing data read from a Google Cloud Storage bucket.
+            None.
         """
 
-        dat = self.downloads_data_from_gcs_bucket(file_location)
-        graph = Graph().parse(dat, format='xml')
-
-        return graph
+        if not ontology_files: return None
+        else:
+            if write_location + merged_ont_kg in glob.glob(write_location + '/*.owl'):
+                ont1, ont2 = ontology_files.pop(), write_location + merged_ont_kg
+            else: ont1, ont2 = ontology_files.pop(), ontology_files.pop()
+            print('Merging Ontologies: {ont1}, {ont2}'.format(ont1=ont1.split('/')[-1], ont2=ont2.split('/')[-1]))
+            logger.info('Merging Ontologies: {ont1}, {ont2}'.format(ont1=ont1.split('/')[-1], ont2=ont2.split('/')[-1]))
+            command = '{} {} {} --merge-support-ontologies -o {}'
+            return_code = os.system(command.format(self.owltools_location, str(ont1), str(ont2),
+                                                   write_location + merged_ont_kg))
+            if return_code == 0: return self.merge_ontologies(ontology_files, write_location, merged_ont_kg)
+            else:
+                logger.error('ERROR: OWL API Merging Failed: {}'.format(return_code))
+                raise Exception('ERROR: OWL API Merging Failed: {}'.format(return_code))
 
     def _logically_verifies_cleaned_ontologies(self) -> None:
         """Logically verifies an ontology by running the ELK deductive logic reasoner. Before running the reasoner
@@ -147,17 +165,19 @@ def _logically_verifies_cleaned_ontologies(self) -> None:
         """
 
         print('Logically Verifying Ontology')
+        logger.info('PKT: Logically Verifying Ontology')
 
         # save graph in order to run reasoner
         filename = self.temp_dir + '/' + self.ont_file_location
         self.ont_graph.serialize(destination=filename, format='xml')
-
-        command = "./pkt_kg/libs/owltools {} --reasoner {} --run-reasoner --assert-implied -o {}"
-        return_code = os.system(command.format(filename, 'elk', filename))
+        command = "{} {} --reasoner {} --run-reasoner --assert-implied -o {}"
+        return_code = os.system(command.format(self.owltools_location, filename, 'elk', filename))
         if return_code == 0:
             ontology_file_formatter(self.temp_dir, '/' + self.ont_file_location, self.owltools_location)
-            self.uploads_data_to_gcs_bucket(self.ont_file_location)
-        else: raise ValueError('Reasoner Finished with Errors.')
+            uploads_data_to_gcs_bucket(self.bucket, self.processed_data, self.temp_dir, self.ont_file_location)
+        else:
+            logger.error('ERROR: Reasoner Finished with Errors - {}: {}'.format(filename, return_code))
+            raise Exception('ERROR: Reasoner Finished with Errors - {}: {}'.format(filename, return_code))
 
         return None
 
@@ -169,6 +189,7 @@ def updates_ontology_reporter(self) -> None:
         """
 
         print('Obtaining Ontology Statistics')
+        logger.info('Obtaining Ontology Statistics')
 
         if isinstance(self.bucket, str):
             gcs_org_url = '{}/{}'.format(self.temp_dir, self.original_data)
@@ -200,18 +221,14 @@ def _finds_ontology_errors(self) -> Dict:
         """
 
         errors = {}
-        try:
-            _ = get_ontology(self.temp_dir + '/' + self.ont_file_location).load()  # type: ignore
-        except OwlReadyOntologyParsingError as o:
-            errors['OwlReadyOntologyParsingError'] = str(o)  # type: ignore
-        except KeyError as k:
-            errors['KeyError'] = str(k)
-        except TypeError as p:
-            errors['PunningError'] = str(p)
+        try: _ = get_ontology(self.temp_dir + '/' + self.ont_file_location).load()  # type: ignore
+        except OwlReadyOntologyParsingError as o: errors['OwlReadyOntologyParsingError'] = str(o)  # type: ignore
+        except KeyError as k: errors['KeyError'] = str(k)
+        except TypeError as p: errors['PunningError'] = str(p)
 
         return errors
 
-    def fixes_ontology_parsing_errors(self):
+    def fixes_ontology_parsing_errors(self) -> None:
         """Fixes parsing errors detected when attempting to load the ontology. Currently, this method only fixes
         value errors (i.e. string-typed entities mis-typed as ints) because these are the only types of errors
         relevant to the set of ontologies utilized in the v2.0.0 PheKnowLator release. It can be easily extended in the
@@ -222,6 +239,7 @@ def fixes_ontology_parsing_errors(self):
         """
 
         print('Finding Parsing Errors')
+        logger.info('Finding Parsing Errors')
 
         errors, error_key, key = self._finds_ontology_errors(), 'OwlReadyOntologyParsingError', self.ont_file_location
         if error_key in errors.keys():
@@ -248,6 +266,7 @@ def fixes_identifier_errors(self) -> None:
         """
 
         print('Fixing Identifier Errors')
+        logger.info('Fixing Identifier Errors')
 
         known_errors = ['PRO', 'PR']  # known errors
         kg_classes, bad_cls, key = gets_ontology_classes(self.ont_graph), set(), self.ont_file_location
@@ -279,22 +298,22 @@ def removes_deprecated_obsolete_entities(self) -> None:
         """
 
         print('Removing Deprecated and Obsolete Classes')
+        logger.info('Removing Deprecated and Obsolete Classes')
 
         ont, key, schma = self.ont_file_location.split('/')[-1].split('_')[0], self.ont_file_location, schema.boolean
-
-        dep_cls = [x[0] for x in tqdm(list(self.ont_graph.triples((None, OWL.deprecated,
-                                                                   Literal('true', datatype=schma)))))]
-        dep_triples = [x for x in tqdm(self.ont_graph)
+        # get deprecated entity information
+        dep_cls = [x[0] for x in list(self.ont_graph.triples((None, OWL.deprecated, Literal('true', datatype=schma))))]
+        dep_triples = [x for x in self.ont_graph
                        if 'deprecated' in ', '.join([str(x[0]).lower(), str(x[1]).lower(), str(x[2]).lower()])
                        and len(list(self.ont_graph.triples((x[0], RDF.type, OWL.Class)))) == 1]
         deprecated_classes = set(dep_cls + [x[0] for x in dep_triples])
-
-        obs_cls = [x[0] for x in tqdm(list(self.ont_graph.triples((None, RDFS.subClassOf, oboinowl.ObsoleteClass))))]
-        obs_triples = [x for x in tqdm(self.ont_graph)
+        # get obsolete entity information
+        obs_cls = [x[0] for x in list(self.ont_graph.triples((None, RDFS.subClassOf, oboinowl.ObsoleteClass)))]
+        obs_triples = [x for x in self.ont_graph
                        if 'obsolete' in ', '.join([str(x[0]).lower(), str(x[1]).lower(), str(x[2]).lower()])
                        and len(list(self.ont_graph.triples((x[0], RDF.type, OWL.Class)))) == 1 and '#' not in str(x[0])]
         obsolete_classes = set(obs_cls + [x[0] for x in obs_triples])
-
+        # remove deprecated and obsolete information
         for node in list(deprecated_classes) + list(obsolete_classes):
             self.ont_graph.remove((node, None, None))
 
@@ -315,6 +334,7 @@ def fixes_punning_errors(self) -> None:
         """
 
         print('Resolving Punning Errors')
+        logger.info('Resolving Punning Errors')
 
         key, bad_cls, bad_obj = self.ont_file_location, set(), set()
         onts_entities = set([x[0] for x in tqdm(self.ont_graph)])
@@ -369,6 +389,7 @@ def normalizes_duplicate_classes(self) -> None:
         """
 
         print('Normalizing Duplicate Concepts')
+        logger.info('Normalizing Duplicate Concepts')
 
         self.ont_graph.add((obo.OGG_0000000002, RDFS.subClassOf, obo.SO_0000704))  # fix gene class inconsistencies
         self.ont_graph.add((obo.PR_000000001, RDFS.subClassOf, obo.SO_0000104))  # fix protein class inconsistencies
@@ -399,28 +420,38 @@ def normalizes_existing_classes(self) -> None:
         """
 
         print('Normalizing Existing Classes')
+        logger.info('Normalizing Existing Classes')
 
+        ents, missing = None, []
         non_ont = set([x for x in gets_ontology_classes(self.ont_graph) if not str(x).startswith(str(obo))])
         hgnc, url, g = set([x for x in non_ont if 'hgnc' in str(x)]), 'http://www.ncbi.nlm.nih.gov/gene/', self.gene_ids
         for node in tqdm(hgnc):
             trips = list(self.ont_graph.triples((node, None, None))) + list(self.ont_graph.triples((None, None, node)))
             nd = 'hgnc_id_' + str(node).split('/')[-1].split('=')[-1]
+            gene_dat = [str(x[2]) for x in trips if x[1] == RDFS.label]
             if nd in g.keys(): ents = [URIRef(url + x) for x in g[nd] if x.startswith('entrez_id_')]
-            else: ents = [URIRef(url + x) for x in self.withdrawn_genes[str(node)]]
-            for edge in trips:
-                if node in edge[0]:
-                    if isinstance(edge[2], URIRef) or isinstance(edge[2], BNode):
+            elif str(node) not in self.withdrawn_map.keys() and len(gene_dat) > 0:
+                if 'symbol_' + gene_dat[0] in g.keys() and 'entrez_id' in g['symbol_' + gene_dat[0]]:
+                    ents = [URIRef(url + x) for x in g['symbol_' + gene_dat[0]] if 'entrez_id' in x][0]
+                else: missing += [str(node)]
+            elif str(node) in self.withdrawn_map.keys(): ents = [URIRef(url + x) for x in self.withdrawn_map[str(node)]]
+            else: missing += [str(node)]
+            if ents:
+                for edge in trips:
+                    if node in edge[0]:
+                        if isinstance(edge[2], URIRef) or isinstance(edge[2], BNode):
+                            for i in ents:
+                                self.ont_graph.add((i, edge[1], edge[2]))
+                                self.ont_graph.remove(edge)
+                        else: self.ont_graph.remove(edge)
+                    if node in edge[2]:
                         for i in ents:
-                            self.ont_graph.add((i, edge[1], edge[2]))
+                            self.ont_graph.add((edge[0], edge[1], i))
                             self.ont_graph.remove(edge)
-                    else: self.ont_graph.remove(edge)
-                if node in edge[2]:
-                    for i in ents:
-                        self.ont_graph.add((edge[0], edge[1], i))
-                        self.ont_graph.remove(edge)
 
         no_ont = len(non_ont) - len(hgnc)
         self.ontology_info[self.ont_file_location]['Normalized - NonOnt'] = no_ont if no_ont != 0 else 'None'
+        self.ontology_info[self.ont_file_location]['Normalized - Dep'] = missing if len(missing) > 0 != 0 else 'None'
         self.ontology_info[self.ont_file_location]['Normalized - Gene IDs'] = len(hgnc) if len(hgnc) > 0 else 'None'
 
         return None
@@ -472,8 +503,12 @@ def generates_ontology_report(self) -> None:
                     else: o.write('\t\t- {}\n'.format(x['Normalized - Duplicates']))
                     o.write('\t\t- Other Classes that May Need Normalization: {}\n'.format(x['Normalized - NonOnt']))
                     o.write('\t\t- Normalized HGNC IDs: {}\n'.format(x['Normalized - Gene IDs']))
+                    o.write('\t- Deprecated Ontology HGNC Identifiers Needing Alignment:\n')
+                    if x['Normalized - Dep'] != 'None':
+                        for i in x['Normalized - Dep']: o.write('\t\t- {}\n'.format(i))
+                    else: o.write('\t\t- {}\n'.format(x['Normalized - Dep']))
 
-        self.uploads_data_to_gcs_bucket(ontology_report_filename)
+        uploads_data_to_gcs_bucket(self.bucket, self.processed_data, self.temp_dir, ontology_report_filename)
 
         return None
 
@@ -490,7 +525,7 @@ def cleans_ontology_data(self) -> None:
         """
 
         print('*** CLEANING INDIVIDUAL ONTOLOGY DATA SOURCES ***')
-
+        logger.info('*** CLEANING INDIVIDUAL ONTOLOGY DATA SOURCES ***')
         for ont in self.ontology_info.keys():
             if ont != self.merged_ontology_filename:
                 print('\nProcessing Ontology: {}'.format(ont.upper()))
@@ -502,12 +537,14 @@ def cleans_ontology_data(self) -> None:
                 self.fixes_punning_errors()
                 self.updates_ontology_reporter()  # get finishing statistics
                 self._logically_verifies_cleaned_ontologies()
+                if self.bucket != '': uploads_data_to_gcs_bucket(self.bucket, self.current_build, log_dir, log)
 
         print('\n\n*** CLEANING MERGED ONTOLOGY DATA ***')
+        logger.info('\n\n*** CLEANING MERGED ONTOLOGY DATA ***')
         self.ont_file_location = self.merged_ontology_filename
-        onts = [self.temp_dir + '/' + x for x in list(self.ontology_info.keys())
-                if x != self.merged_ontology_filename]
-        merges_ontologies(onts, self.temp_dir + '/', self.ont_file_location)
+        individual_ontologies = self.checks_for_downloaded_ontology_data()
+        self.merge_ontologies(individual_ontologies, self.temp_dir + '/', self.ont_file_location)
+        if self.bucket != '': uploads_data_to_gcs_bucket(self.bucket, self.current_build, log_dir, log)
         print('\nLoading Merged Ontology')
         self.ont_graph = Graph().parse(self.temp_dir + '/' + self.ont_file_location)
         self.updates_ontology_reporter()  # get starting statistics
@@ -516,12 +553,16 @@ def cleans_ontology_data(self) -> None:
         self.normalizes_existing_classes()
         self.fixes_punning_errors()
         self.updates_ontology_reporter()  # get finishing statistics
+        if self.bucket != '': uploads_data_to_gcs_bucket(self.bucket, self.current_build, log_dir, log)
         # serializes final ontology graph and uploads graph data and ontology report to gcs
         self.ont_graph.serialize(destination=self.temp_dir + '/' + self.ont_file_location, format='xml')
         ontology_file_formatter(self.temp_dir, '/' + self.ont_file_location, self.owltools_location)
-        self.uploads_data_to_gcs_bucket(self.ont_file_location)
+        uploads_data_to_gcs_bucket(self.bucket, self.processed_data, self.temp_dir, self.ont_file_location)
+        if self.bucket != '': uploads_data_to_gcs_bucket(self.bucket, self.current_build, log_dir, log)
 
-        print('\nGENERATING ONTOLOGY REPORT')
+        print('\n\n*** GENERATING ONTOLOGY CLEANING REPORT ***')
+        logger.info('\n\n*** GENERATING ONTOLOGY CLEANING REPORT ***')
         self.generates_ontology_report()
+        if self.bucket != '': uploads_data_to_gcs_bucket(self.bucket, self.current_build, log_dir, log)
 
         return None
diff --git a/builds/phase3_log_daemon.py b/builds/phase3_log_daemon.py
new file mode 100755
index 000000000..038ed6400
--- /dev/null
+++ b/builds/phase3_log_daemon.py
@@ -0,0 +1,64 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# import needed libraries
+import glob
+import ray
+import threading
+import time
+
+from google.cloud import storage  # type: ignore
+
+from builds.build_utilities import uploads_data_to_gcs_bucket  # type: ignore
+
+
+@ray.remote
+class PKTLogUploader(object):
+    """Implements a background task that uploads logs from a directory to a Google Cloud Storage Bucket while another
+    method, external to the class in run.
+
+    SOURCE: https://github.com/ray-project/ray/issues/854#issuecomment-499056709
+
+        Attributes:
+            bucket_name: A string specifying the name of a Google Cloud Storage bucket.
+            gcs_bucket_location: A string specifying the location of the original_data directory for a specific build.
+            log_directory: A local directory where preprocessed data is stored.
+            sleep_interval: An integer specifying how often logs should be pushed to the Google Cloud Storage Bucket.
+    """
+
+    def __init__(self, bucket_name, gcs_bucket_location, log_directory, sleep_interval):
+        self.bucket: str = storage.Client().get_bucket(bucket_name)
+        self.gcs_bucket_location: str = gcs_bucket_location
+        self.log_directory: str = log_directory
+        self.sleep: int = sleep_interval
+        self.kill_time: int = 43200  # 12 hours
+
+        # START BACKGROUND PROCESS
+        self._thread: threading.Thread = threading.Thread(target=self._run, daemon=True)
+        self._thread.start()
+
+    def _run(self):
+        """Method uploads any log files found in temp_directory from a local directory to a specific directory in a
+        Google Cloud Storage Bucket every "n" minutes as specified by the input interval variable. This method runs
+        the program it is called with finishes. There is also a back-up timer that will kill the program
+
+        Args:
+            bucket: A storage Bucket object specifying a Google Cloud Storage bucket.
+            original_data: A string specifying the location of the original_data directory for a specific build.
+            temp_directory: A local directory where preprocessed data is stored.
+            interval: An integer specifying how often the data should be pushed up to the Google Cloud Storage Bucket.
+
+        Returns:
+            None.
+        """
+
+        # grep for log files in the log_directory
+        log_file = glob.glob(self.log_directory + '/*.log')[0].split('/')[-1]
+        runtime = 0
+
+        while runtime < self.kill_time:
+            uploads_data_to_gcs_bucket(self.bucket, self.gcs_bucket_location, self.log_directory, log_file)
+            time.sleep(self.sleep)
+            runtime += self.sleep
+
+        return None
diff --git a/builds/phases1_2_entrypoint.py b/builds/phases1_2_entrypoint.py
new file mode 100755
index 000000000..f05cc85f2
--- /dev/null
+++ b/builds/phases1_2_entrypoint.py
@@ -0,0 +1,67 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# import needed libraries
+import glob
+import logging.config
+import os
+import shutil
+import traceback
+
+from datetime import datetime
+from google.api_core.exceptions import NotFound  # type: ignore
+from google.cloud import storage  # type: ignore
+
+from builds.build_phase_1 import *  # type: ignore
+from builds.build_phase_2 import *  # type: ignore
+from builds.build_utilities import deletes_single_file, uploads_data_to_gcs_bucket  # type: ignore
+
+# os.environ['GOOGLE_APPLICATION_CREDENTIALS'] = 'resources/project_keys/pheknowlator-6cc612b4cbee.json'
+
+# set environment variables
+log_dir, log, log_config = 'builds/logs', 'pkt_builder_phases12_log.log', glob.glob('**/logging.ini', recursive=True)
+if os.path.exists(log_dir): shutil.rmtree(log_dir)
+os.mkdir(log_dir)
+logger = logging.getLogger(__name__)
+logging.config.fileConfig(log_config[0], disable_existing_loggers=False, defaults={'log_file': log_dir + '/' + log})
+
+
+def main():
+
+    start_time = datetime.now()
+
+    # initialize Google Cloud Storage Bucket object and delete prior logs (if present) from current_build directory
+    bucket = storage.Client().get_bucket('pheknowlator')
+    gcs_current_build = 'current_build/'
+    try: deletes_single_file(bucket, gcs_current_build + '{}'.format(log))
+    except NotFound: pass
+
+    # start logger and configure Google Cloud Storage settings
+    print('\n\n' + '*' * 10 + ' STARTING PHEKNOWLATOR KNOWLEDGE GRAPH BUILD ' + '*' * 10)
+    logger.info('*' * 10 + ' STARTING PHEKNOWLATOR KNOWLEDGE GRAPH BUILD ' + '*' * 10)
+    uploads_data_to_gcs_bucket(bucket, gcs_current_build, log_dir, log)
+
+    # run phase 1 of build
+    print('#' * 35 + '\nBUILD PHASE 1: DOWNLOADING BUILD DATA\n' + '#' * 35)
+    logger.info('#' * 5 + 'BUILD PHASE 1: DOWNLOADING BUILD DATA' + '#' * 5)
+    try: run_phase_1()
+    except: logger.error('ERROR: Uncaught Exception: {}'.format(traceback.format_exc()))
+    uploads_data_to_gcs_bucket(bucket, gcs_current_build, log_dir, log)
+
+    # run phase 2 build
+    print('#' * 35 + '\nBUILD PHASE 2: DATA PRE-PROCESSING\n' + '#' * 35)
+    logger.info('#' * 5 + 'BUILD PHASE 2: DATA PRE-PROCESSING' + '#' * 5)
+    try: run_phase_2()
+    except: logger.error('ERROR: Uncaught Exception: {}'.format(traceback.format_exc()))
+    uploads_data_to_gcs_bucket(bucket, gcs_current_build, log_dir, log)
+
+    # print build statistics and upload logging for data preprocessing and ontology cleaning
+    runtime = round((datetime.now() - start_time).total_seconds() / 60, 3)
+    print('\n\n' + '*' * 10 + ' COMPLETED BUILD PHASES 1-2: {} MINUTES '.format(runtime) + '*' * 10)
+    logger.info('COMPLETED BUILD PHASES 1-2: {} MINUTES'.format(runtime))  # don't delete needed for build monitoring
+    logger.info('EXIT BUILD PHASES 1-2')  # don't delete needed for build monitoring
+    uploads_data_to_gcs_bucket(bucket, gcs_current_build, log_dir, log)
+
+
+if __name__ == '__main__':
+    main()
diff --git a/main.ipynb b/main.ipynb
index 1b0443d31..435fea545 100644
--- a/main.ipynb
+++ b/main.ipynb
@@ -43,7 +43,7 @@
     "<br>\n",
     "\n",
     "**Assumptions:**\n",
-    "1. Please see [this document]() for details on input documents and preparing to construct a knowledge graph.    \n",
+    "1. Hyperlinks to all downloaded and generated data sources are provided through [this](https://console.cloud.google.com/storage/browser/pheknowlator/release_v2.0.0?project=pheknowlator) dedicated Google Cloud Storage Bucket. <u>This includes examples, from prior builds, of the required input documents mentioned below</u>.     \n",
     "2. Make sure that the following input documents have been constructed (see the [Dependencies Wiki](https://github.com/callahantiff/PheKnowLator/wiki/Dependencies) for more information):  \n",
     "  - [`resource_info.txt`](https://github.com/callahantiff/PheKnowLator/blob/master/resources/resource_info.txt)\n",
     "  - [`ontology_source_list.txt`](https://github.com/callahantiff/PheKnowLator/blob/master/resources/ontology_source_list.txt)\n",
@@ -123,7 +123,8 @@
     "<br>\n",
     "\n",
     "**Assumptions:**  \n",
-    "All sources used to construct our knowledge graph need to be preprocessed and ready to download prior to running this code. All mapping, filtering, and label data have been generated. For assistance with creating these datasets, see the [`Data_Preparation.ipynb`](https://github.com/callahantiff/PheKnowLator/blob/master/Data_Preparation.ipynb) Jupyter Notebook.\n"
+    "- All sources used to construct our knowledge graph need to be preprocessed and ready to download prior to running this code. All mapping, filtering, and label data have been generated prior to this step. For assistance with creating these datasets, see the [`Data_Preparation.ipynb`](https://github.com/callahantiff/PheKnowLator/blob/master/Data_Preparation.ipynb) Jupyter Notebook.  \n",
+    "- All downloaded and generated data sources for all PheKnowLator builds can be accessed through [this](https://console.cloud.google.com/storage/browser/pheknowlator/release_v2.0.0?project=pheknowlator) dedicated Google Cloud Storage Bucket.\n"
    ]
   },
   {
diff --git a/pkt_kg/construction_approaches.py b/pkt_kg/construction_approaches.py
index 23471fa45..a35b517b1 100644
--- a/pkt_kg/construction_approaches.py
+++ b/pkt_kg/construction_approaches.py
@@ -4,6 +4,7 @@
 # import needed libraries
 import glob
 import hashlib
+import logging.config
 import os
 import os.path
 import pickle
@@ -18,6 +19,11 @@
 # set global attributes
 obo = Namespace('http://purl.obolibrary.org/obo/')
 pkt = Namespace('https://github.com/callahantiff/PheKnowLator/pkt/')
+# logging
+log_dir, log, log_config = 'builds/logs', 'pkt_build_log.log', glob.glob('**/logging.ini', recursive=True)
+if not os.path.exists(log_dir): os.mkdir(log_dir)
+logger = logging.getLogger(__name__)
+logging.config.fileConfig(log_config[0], disable_existing_loggers=False, defaults={'log_file': log_dir + '/' + log})
 
 
 class KGConstructionApproach(object):
@@ -46,23 +52,31 @@ class KGConstructionApproach(object):
     """
 
     def __init__(self, edge_dict: Dict, write_location: str) -> None:
-
         self.subclass_dict: Dict = dict()
         self.subclass_error: Dict = dict()
 
         # EDGE_DICT
-        if not isinstance(edge_dict, Dict): raise TypeError('edge_dict must be a dictionary.')
-        elif len(edge_dict) == 0: raise TypeError('edge_dict is empty.')
+        if not isinstance(edge_dict, Dict):
+            logger.error('TypeError: edge_dict must be a dictionary.')
+            raise TypeError('edge_dict must be a dictionary.')
+        elif len(edge_dict) == 0:
+            logger.error('TypeError: edge_dict is empty.')
+            raise TypeError('edge_dict is empty.')
         else: self.edge_dict = edge_dict
 
         # WRITE LOCATION
-        if write_location is None: raise ValueError('write_location must contain a valid filepath, not None')
+        if write_location is None:
+            logger.error('ValueError: write_location must contain a valid filepath, not None.')
+            raise ValueError('write_location must contain a valid filepath, not None')
         else: self.write_location = write_location
 
         # LOADING SUBCLASS DICTIONARY
         file_name = self.write_location + '/construction_*/*.pkl'
-        if len(glob.glob(file_name)) == 0: raise OSError('{} does not exist!'.format('subclass_construction_map.pkl'))
+        if len(glob.glob(file_name)) == 0:
+            logger.error('OSError: {} does not exist!'.format('subclass_construction_map.pkl'))
+            raise OSError('{} does not exist!'.format('subclass_construction_map.pkl'))
         elif os.stat(glob.glob(file_name)[0]).st_size == 0:
+            logger.error('TypeError: The input file: {} is empty'.format(glob.glob(file_name)[0]))
             raise TypeError('The input file: {} is empty'.format(glob.glob(file_name)[0]))
         else:
             with open(glob.glob(file_name)[0], 'rb') as filepath:  # type: IO[Any]
@@ -105,7 +119,8 @@ def subclass_edge_constructor(node1: Union[BNode, URIRef], node2: Union[BNode, U
                                   inverse_relation: Optional[URIRef]) -> Tuple:
         """Core subclass-based edge construction method. Constructs a single edge between to ontology classes as well as
         verifies if the user wants an inverse edge created and if so, then this edge is also added to the knowledge
-        graph.
+        graph. Note that a Bnode is used for subclass construction versus the UUID hash + pkt namespace that is used
+        for instance-based construction.
 
         Note. We explicitly type each node and each relation/inverse relation. This may seem redundant, but it is
         needed in order to ensure consistency between the data after applying the OWL API to reformat the data.
@@ -197,9 +212,9 @@ def subclass_constructor(self, edge_info: Dict, edge_type: str) -> Tuple[Dict, L
     @staticmethod
     def instance_edge_constructor(node1: Union[BNode, URIRef], node2: Union[BNode, URIRef], relation: URIRef,
                                   inverse_relation: Optional[URIRef]) -> Tuple:
-        """Core instance-based edge construction method. Constructs a single edge between to ontology classes as well as
-        verifies if the user wants an inverse edge created and if so, then this edge is also added to the knowledge
-        graph.
+        """Core instance-based edge construction method. Constructs a single edge between two ontology classes as
+        well as verifies if the user wants an inverse edge created and if so, then this edge is also added to the
+        knowledge graph.
 
         Note. We explicitly type each node and each relation/inverse relation. This may seem redundant,
         but it is needed in order to ensure consistency between the data after applying the OWL API to reformat the
@@ -264,8 +279,7 @@ def instance_constructor(self, edge_info: Dict, edge_type: str) -> Tuple[Dict, L
             sha_uid = URIRef(pkt + 'N' + hashlib.md5(res['cls1'].encode()).hexdigest())
             x = res['ent1'].replace(uri2, '') if edge_info['n1'] == 'class' else res['ent1'].replace(uri1, '')
             mapped_node = self.maps_node_to_class(edge_type, x, edge_info['edges'])
-            # get non-class node mappings to ontology classes
-            if mapped_node:
+            if mapped_node:  # get non-class node mappings to ontology classes
                 edges = [(sha_uid, RDF.type, URIRef(res['cls1'])),
                          (sha_uid, RDF.type, OWL.NamedIndividual),
                          (URIRef(res['cls1']), RDF.type, OWL.Class),
diff --git a/pkt_kg/downloads.py b/pkt_kg/downloads.py
index 7a87f31e4..424c94463 100644
--- a/pkt_kg/downloads.py
+++ b/pkt_kg/downloads.py
@@ -5,6 +5,7 @@
 # import needed libraries
 import datetime
 import glob
+import logging.config
 import os.path
 import re
 import shutil
@@ -19,6 +20,11 @@
 
 # HANDLE ENVIRONMENT WARNINGS
 urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
+# logging
+log_dir, log, log_config = 'builds/logs', 'pkt_build_log.log', glob.glob('**/logging.ini', recursive=True)
+if not os.path.exists(log_dir): os.mkdir(log_dir)
+logger = logging.getLogger(__name__)
+logging.config.fileConfig(log_config[0], disable_existing_loggers=False, defaults={'log_file': log_dir + '/' + log})
 
 # TODO: need to validate user input data to make sure that it matches the template that the program expects.
 
@@ -46,11 +52,17 @@ class DataSource(object):
     __metaclass__ = ABCMeta
 
     def __init__(self, data_path: str, resource_data: Optional[str] = None) -> None:
-
+        logger.info('*' * 10 + 'PKT STEP: DOWNLOADING KNOWLEDGE GRAPH DATA' + '*' * 10)
         # DATA SOURCE FILE
-        if not isinstance(data_path, str): raise TypeError('data_path must be type str.')
-        elif not os.path.exists(data_path): raise OSError('{} does not exist!'.format(data_path))
-        elif os.stat(data_path).st_size == 0: raise TypeError('Input: {} is empty'.format(data_path))
+        if not isinstance(data_path, str):
+            logger.error('TypeError: data_path must be type str.')
+            raise TypeError('data_path must be type str.')
+        elif not os.path.exists(data_path):
+            logger.error('OSError: {} does not exist!'.format(data_path))
+            raise OSError('{} does not exist!'.format(data_path))
+        elif os.stat(data_path).st_size == 0:
+            logger.error('TypeError: {} is empty'.format(data_path))
+            raise TypeError('Input: {} is empty'.format(data_path))
         else:
             self.data_path: str = data_path
             self.data_type: str = data_path.split('/')[-1].split('.')[0]
@@ -58,9 +70,15 @@ def __init__(self, data_path: str, resource_data: Optional[str] = None) -> None:
         # RESOURCE INFO FILE
         resource_data_search = glob.glob('**/*resource**info*.txt', recursive=True)[0]
         self.resource_data: Optional[str] = resource_data_search if None else resource_data
-        if not isinstance(self.resource_data, str): raise TypeError('resource_data must be type str.')
-        elif not os.path.exists(self.resource_data): raise OSError('{} does not exist!'.format(self.resource_data))
-        elif os.stat(self.resource_data).st_size == 0: raise TypeError('Input: {} is empty'.format(self.resource_data))
+        if not isinstance(self.resource_data, str):
+            logger.error('TypeError: resource_data must be type str.')
+            raise TypeError('resource_data must be type str.')
+        elif not os.path.exists(self.resource_data):
+            logger.error('OSError: {} does not exist!'.format(self.resource_data))
+            raise OSError('{} does not exist!'.format(self.resource_data))
+        elif os.stat(self.resource_data).st_size == 0:
+            logger.error('TypeError: {} is empty'.format(self.resource_data))
+            raise TypeError('Input: {} is empty'.format(self.resource_data))
         else:
             resource_data_file: TextIO = open(self.resource_data)
             self.resource_info: List = resource_data_file.read().splitlines()
@@ -180,6 +198,7 @@ def generates_source_metadata(self) -> None:
         """
 
         print('\n*** Generating Metadata ***\n')
+        logger.info('*** Generating Metadata ***')
         self.metadata.append(['#' + str(datetime.datetime.utcnow().strftime('%a %b %d %X UTC %Y')) + ' \n'])
 
         for i in tqdm(self.data_files.keys()):
@@ -230,6 +249,7 @@ def parses_resource_file(self) -> None:
         """
 
         if os.stat(self.data_path).st_size == 0:
+            logger.error('TypeError: input file: {} is empty'.format(self.data_path))
             raise TypeError('ERROR: input file: {} is empty'.format(self.data_path))
         else:
             with open(self.data_path, 'r') as file_name:
@@ -259,14 +279,16 @@ def downloads_data_from_url(self, owltools_location: str = os.path.abspath('./pk
         self.parses_resource_file()  # check data before download
         file_loc = '/'.join(self.data_path.split('/')[:-1]) + '/ontologies/'
         print('\n ***Downloading Data: {0} to "{1}" ***\n'.format(self.data_type, file_loc))
+        logger.info('***Downloading Data: {0} to "{1}" ***'.format(self.data_type, file_loc))
 
         for i in tqdm(self.source_list.keys()):
             source = self.source_list[i]
             file_prefix = source.split('/')[-1].split('.')[0]
             write_loc = file_loc + file_prefix
             print('\nDownloading: {}'.format(str(file_prefix)))
+            logger.info('Downloading: {}'.format(str(file_prefix)))
             # don't re-download ontologies
-            if any(x for x in os.listdir(file_loc) if re.sub('_with.*.owl', '', x) == file_prefix):
+            if any(x for x in os.listdir(file_loc) if file_prefix == x.split('.')[0]):
                 self.data_files[i] = glob.glob(file_loc + '*' + file_prefix + '*')[0]
             else:
                 if 'purl' in source:
@@ -275,7 +297,8 @@ def downloads_data_from_url(self, owltools_location: str = os.path.abspath('./pk
                                                '-o', str(write_loc) + '_with_imports.owl'])
                         self.data_files[i] = str(write_loc) + '_with_imports.owl'
                     except subprocess.CalledProcessError as error:
-                        print(error.output)
+                        logger.error('Error: {}'.format(error.output))
+                        raise Exception('{}'.format(error.output))
                 else:
                     data_downloader(source, file_loc, str(file_prefix) + '_with_imports.owl')
                     self.data_files[i] = file_loc + str(file_prefix) + '_with_imports.owl'
@@ -305,6 +328,7 @@ def parses_resource_file(self) -> None:
         """
 
         if os.stat(self.data_path).st_size == 0:
+            logger.error('TypeError: input file: {} is empty'.format(self.data_path))
             raise TypeError('ERROR: input file: {} is empty'.format(self.data_path))
         else:
             with open(self.data_path, 'r') as file_name:
@@ -326,18 +350,21 @@ def downloads_data_from_url(self) -> None:
         self.parses_resource_file()  # check data before download
         file_loc = '/'.join(self.data_path.split('/')[:-1]) + '/edge_data/'
         print('\n*** Downloading Data: {0} to "{1}" ***\n'.format(self.data_type, file_loc))
+        logger.info('*** Downloading Data: {0} to "{1}" ***'.format(self.data_type, file_loc))
 
         for i in tqdm(self.source_list.keys()):
             source = self.source_list[i]
             file_name = re.sub('.gz|.zip|\\?.*', '', source.split('/')[-1])
             write_path = file_loc
             print('\nEdge: {edge}'.format(edge=i))
+            logger.info('Edge: {edge}'.format(edge=i))
             # if file has already been downloaded, rename it
             if any(x for x in os.listdir(write_path) if '_'.join(x.split('_')[1:]) == file_name):
                 self.data_files[i] = write_path + i + '_' + file_name
                 try:
                     shutil.copy(glob.glob(write_path + '*' + file_name)[0], write_path + i + '_' + file_name)
                 except shutil.SameFileError:
+                    logger.error('{}'.format(shutil.SameFileError))
                     pass
             else:
                 self.data_files[i] = write_path + i + '_' + file_name
diff --git a/pkt_kg/edge_list.py b/pkt_kg/edge_list.py
index 1ccf0edd3..197e1e743 100755
--- a/pkt_kg/edge_list.py
+++ b/pkt_kg/edge_list.py
@@ -3,7 +3,10 @@
 
 # import needed libraries
 import csv
+import glob
 import json
+import logging.config
+import os
 import pandas as pd  # type: ignore
 import re
 
@@ -11,6 +14,12 @@
 from tqdm import tqdm  # type: ignore
 from typing import Any, Dict, IO, List, Optional, TextIO, Tuple, Union
 
+# logging
+log_dir, log, log_config = 'builds/logs', 'pkt_build_log.log', glob.glob('**/logging.ini', recursive=True)
+if not os.path.exists(log_dir): os.mkdir(log_dir)
+logger = logging.getLogger(__name__)
+logging.config.fileConfig(log_config[0], disable_existing_loggers=False, defaults={'log_file': log_dir + '/' + log})
+
 # TODO:
 #  (1) using eval() to handle filtering of downloaded data, should consider replacing this in a future release.
 #  (2) modify data_reader to stream/chunk large data files
@@ -99,14 +108,6 @@ def data_reader(self, file_path: str, delimiter: str = 't') -> pd.DataFrame:
         with open(file_path, 'r') as input_data_r:  # type: IO[Any]
             data = input_data_r.read().splitlines()
         input_data_r.close()
-        # try:
-        #     with open(file_path, 'r') as input_data_r:  # type: IO[Any]
-        #         data = input_data_r.read().splitlines()
-        #     input_data_r.close()
-        # except ValueError:
-        #     with open(file_path, 'rb') as input_data_rb:  # type: IO[Any]
-        #         data = input_data_rb.read().decode('utf-8').splitlines()  # decode bytes to strings
-        #     input_data_rb.close()
 
         # clean up data to only keep valid rows (rows that are not empty space or metadata)
         splitter = '\t' if 't' in delimiter else r"\s+" if '' in delimiter else delimiter
@@ -261,16 +262,13 @@ def data_merger(self, node: int, mapping_data: str, edge_data: pd.DataFrame) ->
         # check if node needs to be mapped to an outside data source
         if str(node) in re.sub('(?:(?!:)\\D)*', '', mapping_data).split(':'):  # MAPPING TO OUTSIDE DATA SOURCE
             node2map = list(edge_data)[node]
-            try:
-                map_data = self.data_reader(mapping_data.split(';')[node].split(':')[1]).astype(str)
-            except IndexError:
-                map_data = self.data_reader(mapping_data.split(';')[0].split(':')[1]).astype(str)
+            try: map_data = self.data_reader(mapping_data.split(';')[node].split(':')[1]).astype(str)
+            except IndexError: map_data = self.data_reader(mapping_data.split(';')[0].split(':')[1]).astype(str)
             # process mapping data
             map_col = list(map_data)[0]
             col_to_map = str(node2map) + '_' + str(map_col) + '_mapped'
             map_data.rename(columns={list(map_data)[1]: str(col_to_map)}, inplace=True)
-            try:
-                merged_data = pd.merge(edge_data, map_data, left_on=node2map, right_on=map_col, how='inner')
+            try: merged_data = pd.merge(edge_data, map_data, left_on=node2map, right_on=map_col, how='inner')
             except ValueError:
                 # update map_data merge col to match edge_data merge col type
                 edge_data[node2map], map_data[map_col] = edge_data[node2map].astype(str), map_data[map_col].astype(str)
@@ -354,16 +352,20 @@ def creates_knowledge_graph_edges(self) -> None:
                                                'edge_list': [['CHEBI_24505', 'R-HSA-1006173'], ...]}}
         """
 
+        logger.info('*' * 10 + 'PKT STEP: GENERATING KNOWLEDGE GRAPH MASTER EDGE LIST' + '*' * 10)
+
         for edge_type in tqdm(self.source_info.keys()):
             print('\n\n### Processing Edge: {}'.format(edge_type))
+            logger.info('### Processing Edge: {}'.format(edge_type))
 
             # STEP 1: Read Data
             print('*** Reading Edge Data ***')
-            edge_data = self.data_reader(self.data_files[edge_type], self.source_info[edge_type][
-                'delimiter'])
+            logger.info('*** Reading Edge Data ***')
+            edge_data = self.data_reader(self.data_files[edge_type], self.source_info[edge_type]['delimiter'])
 
             # STEP 2: Apply Filtering and Evidence Criteria
             print('*** Applying Filtering and/or Mapping Criteria to Edge Data ***')
+            logger.info('*** Applying Filtering and/or Mapping Criteria to Edge Data ***')
             edge_data = self.filter_data(edge_data,
                                          self.source_info[edge_type]['filter_criteria'],
                                          self.source_info[edge_type]['evidence_criteria'])
@@ -373,6 +375,7 @@ def creates_knowledge_graph_edges(self) -> None:
 
             # STEP 4: Update Node Column Values
             print('*** Reformatting Node Values ***')
+            logger.info('*** Reformatting Node Values ***')
             edge_data = self.label_formatter(edge_data, self.source_info[edge_type]['source_labels'])
 
             # STEP 5: Rename Nodes
@@ -382,16 +385,20 @@ def creates_knowledge_graph_edges(self) -> None:
 
             # STEP 6: Map Identifiers
             print('*** Performing Identifier Mapping ***')
+            logger.info('*** Performing Identifier Mapping ***')
             mapped_data = self.process_mapping_data(self.source_info[edge_type]['identifier_maps'], edge_data)
             self.source_info[edge_type]['edge_list'] = [edge for edge in mapped_data if 'None' not in edge]
 
             # print Edge Statistics
             unique_edges = [list(y) for y in set([tuple(x) for x in self.source_info[edge_type]['edge_list']])]
+            unique_subjects, unique_objects = set([x[0] for x in unique_edges]), set([x[1] for x in unique_edges])
             print('\nPROCESSED EDGE: {}'.format(edge_type))
-            print('{}: Unique Node Count = {}'.format(edge_type.split('-')[0], len(set([x[0] for x in unique_edges]))))
-            print('{}: Unique Node Count = {}'.format(edge_type.split('-')[1], len(set([x[1] for x in unique_edges]))))
+            print('{}: Unique Node Count = {}'.format(edge_type.split('-')[0], len(unique_subjects)))
+            print('{}: Unique Node Count = {}'.format(edge_type.split('-')[1], len(unique_objects)))
             print('Total Unique Edge Count: {}'.format(len(unique_edges)))
-            print('Total Unique Edge Count + Inverses: {}'.format(len(unique_edges)*2))
+            res_string = 'Finished Edge: {} - ({} = {} unique nodes, {} = {} unique nodes) and {} unique edges'
+            logger.info(res_string.format(edge_type, edge_type.split('-')[0], len(unique_subjects),
+                        edge_type.split('-')[1], len(unique_objects), len(unique_edges)))
 
         # add source entity namespaces
         self.gets_entity_namespaces()
diff --git a/pkt_kg/knowledge_graph.py b/pkt_kg/knowledge_graph.py
index be0eaf767..7294d442e 100644
--- a/pkt_kg/knowledge_graph.py
+++ b/pkt_kg/knowledge_graph.py
@@ -5,6 +5,7 @@
 import copy
 import glob
 import json
+import logging.config
 import networkx  # type: ignore
 import os
 import os.path
@@ -26,6 +27,11 @@
 
 # set global attributes
 obo = Namespace('http://purl.obolibrary.org/obo/')
+# logging
+log_dir, log, log_config = 'builds/logs', 'pkt_build_log.log', glob.glob('**/logging.ini', recursive=True)
+if not os.path.exists(log_dir): os.mkdir(log_dir)
+logger = logging.getLogger(__name__)
+logging.config.fileConfig(log_config[0], disable_existing_loggers=False, defaults={'log_file': log_dir + '/' + log})
 
 
 class KGBuilder(object):
@@ -75,26 +81,40 @@ def __init__(self, construction: str, node_data: str, inverse_relations: str, de
 
         # ONTOLOGIES DATA DIRECTORY
         ontologies = glob.glob(self.res_dir + '/ontologies/*.owl')
-        if not os.path.exists(self.res_dir + '/ontologies'): raise OSError("Can't find 'ontologies/' directory")
-        elif len(ontologies) == 0: raise TypeError('The ontologies directory is empty')
+        if not os.path.exists(self.res_dir + '/ontologies'):
+            logger.error("OSError: Can't find 'ontologies/' directory")
+            raise OSError("Can't find 'ontologies/' directory")
+        elif len(ontologies) == 0:
+            logger.error('TypeError: The ontologies directory is empty')
+            raise TypeError('The ontologies directory is empty')
         else: self.ontologies: List[str] = ontologies
 
         # CONSTRUCTION APPROACH
         const = construction.lower() if isinstance(construction, str) else str(construction).lower()
-        if const not in ['subclass', 'instance']: raise ValueError('construction must be "instance" or "subclass"')
+        if const not in ['subclass', 'instance']:
+            logger.error('ValueError: Construction must be "instance" or "subclass"')
+            raise ValueError('Construction must be "instance" or "subclass"')
         else: self.construct_approach: str = const
 
         # GRAPH EDGE DATA
         edge_data = self.res_dir + '/Master_Edge_List_Dict.json'
-        if not os.path.exists(edge_data): raise OSError('The {} file does not exist!'.format(edge_data))
-        elif os.stat(edge_data).st_size == 0: raise TypeError('The input file: {} is empty'.format(edge_data))
+        if not os.path.exists(edge_data):
+            logger.error('OSError: {} file does not exist!'.format(edge_data))
+            raise OSError('The {} file does not exist!'.format(edge_data))
+        elif os.stat(edge_data).st_size == 0:
+            logger.error('TypeError: The input file {} is empty'.format(edge_data))
+            raise TypeError('The input file: {} is empty'.format(edge_data))
         else:
             with(open(edge_data, 'r')) as _file: self.edge_dict: Dict = json.load(_file)
 
         # RELATIONS DATA
         inv, rel_dir = str(inverse_relations).lower(), glob.glob(self.res_dir + '/relations_data/*.txt')
-        if inv not in ['yes', 'no']: raise ValueError('inverse_relations not "no" or "yes"')
-        elif len(rel_dir) == 0: raise TypeError('relations_data directory is empty')
+        if inv not in ['yes', 'no']:
+            logger.error('ValueError: inverse_relations not "no" or "yes"')
+            raise ValueError('inverse_relations not "no" or "yes"')
+        elif len(rel_dir) == 0:
+            logger.error('TypeError: relations_data directory is empty')
+            raise TypeError('relations_data directory is empty')
         elif inv == 'yes':
             self.inverse_relations: Optional[List] = rel_dir
             self.inverse_relations_dict: Optional[Dict] = {}
@@ -103,8 +123,12 @@ def __init__(self, construction: str, node_data: str, inverse_relations: str, de
 
         # NODE METADATA
         node_data, node_dir = str(node_data).lower(), glob.glob(self.res_dir + '/node_data/*.pkl')
-        if node_data not in ['yes', 'no']: raise ValueError('node_data not "no" or "yes"')
-        elif node_data == 'yes' and len(node_dir) == 0: raise TypeError('node_data directory is empty')
+        if node_data not in ['yes', 'no']:
+            logger.error('ValueError: node_data not "no" or "yes"')
+            raise ValueError('node_data not "no" or "yes"')
+        elif node_data == 'yes' and len(node_dir) == 0:
+            logger.error('TypeError: node_data directory is empty')
+            raise TypeError('node_data directory is empty')
         elif node_data == 'yes' and len(node_dir) != 0:
             self.node_data: Optional[List] = node_dir
             self.node_dict: Optional[Dict] = dict()
@@ -112,7 +136,9 @@ def __init__(self, construction: str, node_data: str, inverse_relations: str, de
 
         # OWL SEMANTICS
         decode_owl = str(decode_owl).lower()
-        if decode_owl not in ['yes', 'no']: raise ValueError('decode_semantics not "no" or "yes"')
+        if decode_owl not in ['yes', 'no']:
+            logger.error('ValueError: decode_semantics not "no" or "yes"')
+            raise ValueError('decode_semantics not "no" or "yes"')
         elif decode_owl == 'yes':
             self.decode_owl: Optional[str] = decode_owl
             owl_kg = '_noOWL'
@@ -133,6 +159,7 @@ def reverse_relation_processor(self) -> None:
 
         if self.inverse_relations:
             print('Loading and Processing Relation Data')
+            logging.info('Loading and Processing Relation Data')
             for data in self.inverse_relations:
                 df = pandas.read_csv(data, header=0, delimiter='\t')
                 df.drop_duplicates(keep='first', inplace=True)
@@ -157,6 +184,7 @@ def verifies_object_property(self, object_property: URIRef) -> None:
         """
 
         if not isinstance(object_property, URIRef):
+            logger.error('TypeError: object_property must be type rdflib.term.URIRef')
             raise TypeError('object_property must be type rdflib.term.URIRef')
         else:
             if object_property not in self.obj_properties:
@@ -241,6 +269,36 @@ def gets_edge_statistics(self, edge_type: str, invrel: Optional[str], results: L
         print('Unique Non-OWL Edges: {}'.format(len(edges) * 2 if invrel else len(edges)))
         print('Unique {}: {}'.format(n1, len(set([x[0] for x in self.edge_dict[edge_type]['edge_list']]))))
         print('Unique {}: {}'.format(n2, len(set([x[1] for x in self.edge_dict[edge_type]['edge_list']]))))
+        # log statistics
+        res_string = '{} Edge Statistics: {} OWL Edges, {} Non-OWL Edges, {} {} Nodes and {} {} Nodes'
+        logger.info(res_string.format(edge_type.upper(), len(set(results)), len(edges) * 2 if invrel else len(edges),
+                                      n1, len(set([x[0] for x in self.edge_dict[edge_type]['edge_list']])),
+                                      n2, len(set([x[1] for x in self.edge_dict[edge_type]['edge_list']]))))
+
+        return None
+
+    @staticmethod
+    def derives_graph_statistics(graph) -> None:
+        """Derives statistics from an input knowledge graph and prints them to the console.
+
+        Args:
+            graph:
+
+        Returns:
+            None.
+        """
+
+        # gets statistics
+        triples = len(graph)
+        nodes = len(set([x[0::2] for x in graph]))
+        classes = len(set([x for x in graph.triples((None, RDF.type, OWL.Class))]))
+        individuals = len(set([x for x in graph.triples((None, RDF.type, OWL.NamedIndividual))]))
+        object_props = len(set([x for x in graph.triples((None, RDF.type, OWL.ObjectProperty))]))
+        annotation_props = len(set([x for x in graph.triples((None, RDF.type, OWL.AnnotationProperty))]))
+
+        x = ' {} triples, {} nodes, {} classes, {} individuals, {} object properties, {} annotation properties'
+        print('Graph Stats:' + x.format(triples, nodes, classes, individuals, object_props, annotation_props))
+        logger.info('Graph Stats:' + x.format(triples, nodes, classes, individuals, object_props, annotation_props))
 
         return None
 
@@ -266,6 +324,7 @@ def creates_knowledge_graph_edges(self, node_metadata_func: Callable, ontology_a
             self.verifies_object_property(URIRef(obo + rel))  # verify object property in knowledge graph
             invrel = self.checks_for_inverse_relations(rel, edge_list) if self.inverse_relations else None
             print('\nCreating {} ({}-{}) Edges ***'.format(edge_type.upper(), n1_type, n2_type))
+            logger.info('Creating {} ({}-{}) Edges ***'.format(edge_type.upper(), n1_type, n2_type))
             for edge in tqdm(edge_list):
                 edge_info = {'n1': n1_type, 'n2': n2_type, 'rel': rel, 'inv_rel': invrel, 'uri': uri, 'edges': edge}
                 meta = node_metadata_func(ent=[''.join(x) for x in list(zip(uri, edge))], e_type=[n1_type, n2_type])
@@ -288,9 +347,11 @@ def creates_knowledge_graph_edges(self, node_metadata_func: Callable, ontology_a
         if len(edge_builder.subclass_error.keys()) > 0:  # output error logs
             log_file = glob.glob(self.res_dir + '/construction*')[0] + '/subclass_map_missing_node_log.json'
             print('\nSome edge lists nodes were missing from the subclass_dict, see log: {}'.format(log_file))
+            logger.info('Some edge lists nodes were missing from the subclass_dict, see log: {}'.format(log_file))
             outputs_dictionary_data(edge_builder.subclass_error, log_file)
         self.graph = ontology_annotator_func(self.full_kg.split('/')[-1], self.graph)
         print('\nSerializing Knowledge Graph')
+        logger.info('Serializing Knowledge Graph')
         full_kg_owl = self.full_kg.replace('noOWL', 'OWL') if self.decode_owl == 'yes' else self.full_kg
         self.graph.serialize(destination=self.write_location + full_kg_owl, format='xml')
         ontology_file_formatter(self.write_location, full_kg_owl, self.owl_tools)
@@ -338,26 +399,34 @@ def construct_knowledge_graph(self) -> None:
         """
 
         print('\n### Starting Knowledge Graph Build: PARTIAL ###')
+        logger.info('*' * 10 + 'PKT STEP: CONSTRUCTING KNOWLEDGE GRAPH' + '*' * 10)
+        logger.info('### Starting Knowledge Graph Build: PARTIAL ###')
 
         # STEP 1: PROCESS RELATION AND INVERSE RELATION DATA
         print('*** Loading Relations Data ***')
+        logger.info('*** Loading Relations Data ***')
         self.reverse_relation_processor()
 
         # STEP 2: MERGE ONTOLOGIES
         if self.merged_ont_kg in glob.glob(self.write_location + '/*.owl'):
             print('*** Loading Merged Ontologies ***')
+            logger.info('*** Loading Merged Ontologies ***')
             self.graph = Graph().parse(self.merged_ont_kg, format='xml')
         else:
             print('*** Merging Ontology Data ***')
+            logger.info('*** Merging Ontology Data ***')
             merged_ontology_location = self.merged_ont_kg.split('/')[-1]
             merges_ontologies(self.ontologies, merged_ontology_location, self.owl_tools)
             self.graph.parse(self.merged_ont_kg, format='xml')  # load the merged ontology
         # print statistics on the merged ontologies
         gets_ontology_statistics(self.merged_ont_kg, self.owl_tools)
+        self.derives_graph_statistics(self.graph)
         print('The Merged Core Ontology Knowledge Graph Contains: {} Triples'.format(len(self.graph)))
+        logger.info('The Merged Core Ontology Knowledge Graph Contains: {} Triples'.format(len(self.graph)))
 
         # STEP 3: PROCESS NODE METADATA
         print('*** Loading Node Metadata Data ***')
+        logger.info('*** Loading Node Metadata Data ***')
         metadata = Metadata(self.kg_version, self.write_location, self.full_kg, self.node_data, self.node_dict)
         if self.node_data:
             metadata.node_metadata_processor()
@@ -365,13 +434,17 @@ def construct_knowledge_graph(self) -> None:
             self.node_dict = metadata.node_dict
 
         # STEP 4: ADD EDGE DATA TO KNOWLEDGE GRAPH DATA
-        print('\n*** Building Knowledge Graph Edges ***')
+        print('*** Building Knowledge Graph Edges ***')
+        logger.info('*** Building Knowledge Graph Edges ***')
         self.ont_classes = gets_ontology_classes(self.graph)
         self.obj_properties = gets_object_properties(self.graph)
         self.creates_knowledge_graph_edges(metadata.creates_node_metadata, metadata.adds_ontology_annotations)
         full_kg_owl = self.full_kg.replace('noOWL', 'OWL') if self.decode_owl == 'yes' else self.full_kg
+        # print statistics on the updated graph
+        self.derives_graph_statistics(self.graph)
         gets_ontology_statistics(self.write_location + full_kg_owl, self.owl_tools)
         print('The Knowledge Graph Contains: {} Triples'.format(len(self.graph)))
+        logger.info('The Knowledge Graph Contains: {} Triples'.format(len(self.graph)))
 
         # clean environment
         del metadata, self.edge_dict, self.graph, self.inverse_relations_dict, self.node_dict, self.relations_dict
@@ -404,27 +477,36 @@ def construct_knowledge_graph(self) -> None:
         """
 
         print('\n### Starting Knowledge Graph Build: post-closure ###')
+        logger.info('*' * 10 + 'PKT STEP: CONSTRUCTING KNOWLEDGE GRAPH' + '*' * 10)
+        logger.info('### Starting Knowledge Graph Build: post-closure ###')
 
         # STEP 1: PROCESS RELATION AND INVERSE RELATION DATA
         print('*** Loading Relations Data ***')
+        logger.info('*** Loading Relations Data ***')
         self.reverse_relation_processor()
 
         # STEP 2: LOAD CLOSED KNOWLEDGE GRAPH
         closed_kg_location = glob.glob(self.write_location + '/*.owl')
         if len(closed_kg_location) == 0:
+            logger.error('OSError: The closed KG file does not exist!')
             raise OSError('The closed KG file does not exist!')
         elif os.stat(closed_kg_location[0]).st_size == 0:
+            logger.error('TypeError: {} is empty'.format(closed_kg_location))
             raise TypeError('input file: {} is empty'.format(closed_kg_location))
         else:
             print('*** Loading Closed Knowledge Graph ***')
+            logger.info('*** Loading Closed Knowledge Graph ***')
             os.rename(closed_kg_location[0], self.write_location + self.full_kg)  # rename closed kg file
             self.graph = Graph().parse(self.write_location + self.full_kg, format='xml')
-            # print statistics
-            gets_ontology_statistics(self.write_location + self.full_kg, self.owl_tools)
-            print('The Knowledge Graph Contains: {} Triples'.format(len(self.graph)))
+        # print statistics on the updated graph
+        self.derives_graph_statistics(self.graph)
+        gets_ontology_statistics(self.write_location + self.full_kg, self.owl_tools)
+        print('The Knowledge Graph Contains: {} Triples'.format(len(self.graph)))
+        logger.info('The Knowledge Graph Contains: {} Triples'.format(len(self.graph)))
 
         # STEP 3: PROCESS NODE METADATA
         print('*** Loading Node Metadata Data ***')
+        logger.info('*** Loading Node Metadata Data ***')
         metadata = Metadata(self.kg_version, self.write_location, self.full_kg, self.node_data, self.node_dict)
         if self.node_data:
             metadata.node_metadata_processor()
@@ -435,23 +517,35 @@ def construct_knowledge_graph(self) -> None:
         results: Tuple = tuple([self.graph, None])
         if self.decode_owl:
             print('\n*** Running OWL-NETS - Decoding OWL-Encoded Classes and Removing OWL Semantics ***')
+            logger.info('*** Running OWL-NETS - Decoding OWL-Encoded Classes and Removing OWL Semantics ***')
             owl_nets = OwlNets(self.graph, self.write_location, self.full_kg, self.construct_approach, self.owl_tools)
             results = owl_nets.run_owl_nets()
         else:
+            logger.info('*** Converting  Knowledge Graph to Networkx MultiDiGraph ***')
             converts_rdflib_to_networkx(self.write_location, self.full_kg[:-4], self.graph)
+        # print statistics on the updated graph
+        self.derives_graph_statistics(self.graph)
+        gets_ontology_statistics(self.write_location + self.full_kg, self.owl_tools)
+        print('The Knowledge Graph Contains: {} Triples'.format(len(self.graph)))
+        logger.info('The Knowledge Graph Contains: {} Triples'.format(len(self.graph)))
 
         # STEP 5: WRITE OUT KNOWLEDGE GRAPH DATA AND CREATE EDGE LISTS
         print('\n*** Writing Knowledge Graph Edge Lists ***')
+        logger.info('*** Writing Knowledge Graph Edge Lists ***')
         f_prefix = ('', '_' + self.construct_approach.upper() + '_purified') if len(results) == 2 else ('', '')
         for graph in results:
             if isinstance(graph, Graph):
                 self.graph = graph
+                self.derives_graph_statistics(self.graph)
                 triple_list_file = self.full_kg[:-4] + f_prefix[results.index(graph)] + '_Triples_Integers.txt'
                 triple_map = self.full_kg[:-4] + f_prefix[results.index(graph)] + '_Triples_Integer_Identifier_Map.json'
+                logger.info('Create Entity-Integer Map')
                 node_int_map = maps_node_ids_to_integers(self.graph, self.write_location, triple_list_file, triple_map)
 
                 # STEP 6: EXTRACT AND WRITE NODE METADATA
                 print('\n*** Processing Knowledge Graph Metadata ***')
+                logger.info('*** Processing Knowledge Graph Metadata ***')
+                metadata.full_kg = self.full_kg[:-4] + f_prefix[results.index(graph)] + '.owl'
                 if self.node_data: metadata.output_knowledge_graph_metadata(node_int_map)
 
         # clean environment
@@ -482,26 +576,34 @@ def construct_knowledge_graph(self) -> None:
         """
 
         print('\n### Starting Knowledge Graph Build: FULL ###')
+        logger.info('*' * 10 + 'PKT STEP: CONSTRUCTING KNOWLEDGE GRAPH' + '*' * 10)
+        logger.info('### Starting Knowledge Graph Build: FULL ###')
 
         # STEP 1: PROCESS RELATION AND INVERSE RELATION DATA
         print('*** Loading Relations Data ***')
+        logger.info('*** Loading Relations Data ***')
         self.reverse_relation_processor()
 
         # STEP 2: MERGE ONTOLOGIES
         if self.merged_ont_kg in glob.glob(self.write_location + '/*.owl'):
             print('*** Loading Merged Ontologies ***')
+            logger.info('*** Loading Merged Ontologies ***')
             self.graph = Graph().parse(self.merged_ont_kg, format='xml')
         else:
             print('*** Merging Ontology Data ***')
+            logger.info('*** Merging Ontology Data ***')
             merged_ontology_location = self.merged_ont_kg.split('/')[-1]
             merges_ontologies(self.ontologies, merged_ontology_location, self.owl_tools)
             self.graph.parse(self.merged_ont_kg, format='xml')  # load the merged ontology
         # print statistics on the merged ontologies
+        self.derives_graph_statistics(self.graph)
         gets_ontology_statistics(self.merged_ont_kg, self.owl_tools)
         print('The Merged Core Ontology Knowledge Graph Contains: {} Triples'.format(len(self.graph)))
+        logger.info('The Merged Core Ontology Knowledge Graph Contains: {} Triples'.format(len(self.graph)))
 
         # STEP 3: PROCESS NODE METADATA
         print('*** Loading Node Metadata Data ***')
+        logger.info('*** Loading Node Metadata Data ***')
         metadata = Metadata(self.kg_version, self.write_location, self.full_kg, self.node_data, self.node_dict)
         if self.node_data:
             metadata.node_metadata_processor()
@@ -510,33 +612,45 @@ def construct_knowledge_graph(self) -> None:
 
         # STEP 4: ADD EDGE DATA TO KNOWLEDGE GRAPH DATA
         print('\n*** Building Knowledge Graph Edges ***')
+        logger.info('*** Building Knowledge Graph Edges ***')
         self.ont_classes = gets_ontology_classes(self.graph)
         self.obj_properties = gets_object_properties(self.graph)
         self.creates_knowledge_graph_edges(metadata.creates_node_metadata, metadata.adds_ontology_annotations)
         full_kg_owl = self.full_kg.replace('noOWL', 'OWL') if self.decode_owl == 'yes' else self.full_kg
+        # print statistics on the updated graph
+        self.derives_graph_statistics(self.graph)
         gets_ontology_statistics(self.write_location + full_kg_owl, self.owl_tools)
         print('The Knowledge Graph Contains: {} Triples'.format(len(self.graph)))
+        logger.info('The Knowledge Graph Contains: {} Triples'.format(len(self.graph)))
 
         # STEP 5: DECODE OWL SEMANTICS
         results: Tuple = tuple([self.graph, None])
         if self.decode_owl:
             print('\n*** Running OWL-NETS - Decoding OWL-Encoded Classes and Removing OWL Semantics ***')
+            logger.info('*** Running OWL-NETS - Decoding OWL-Encoded Classes and Removing OWL Semantics ***')
             owl_nets = OwlNets(self.graph, self.write_location, self.full_kg, self.construct_approach, self.owl_tools)
             results = owl_nets.run_owl_nets()
-        else: converts_rdflib_to_networkx(self.write_location, self.full_kg[:-4], self.graph)
+        else:
+            logger.info('*** Converting  Knowledge Graph to Networkx MultiDiGraph ***')
+            converts_rdflib_to_networkx(self.write_location, self.full_kg[:-4], self.graph)
 
         # STEP 6: WRITE OUT KNOWLEDGE GRAPH DATA AND CREATE EDGE LISTS
         print('\n*** Writing Knowledge Graph Edge Lists ***')
+        logger.info('*** Writing Knowledge Graph Edge Lists ***')
         f_prefix = ('', '_' + self.construct_approach.upper() + '_purified') if len(results) == 2 else ('', '')
         for graph in results:
             if isinstance(graph, Graph):
                 self.graph = graph
+                self.derives_graph_statistics(self.graph)
                 triple_list_file = self.full_kg[:-4] + f_prefix[results.index(graph)] + '_Triples_Integers.txt'
                 triple_map = self.full_kg[:-4] + f_prefix[results.index(graph)] + '_Triples_Integer_Identifier_Map.json'
+                logger.info('Create Entity-Integer Map')
                 node_int_map = maps_node_ids_to_integers(self.graph, self.write_location, triple_list_file, triple_map)
 
                 # STEP 7: EXTRACT AND WRITE NODE METADATA
                 print('\n*** Processing Knowledge Graph Metadata ***')
+                logger.info('*** Processing Knowledge Graph Metadata ***')
+                metadata.full_kg = self.full_kg[:-4] + f_prefix[results.index(graph)] + '.owl'
                 if self.node_data: metadata.output_knowledge_graph_metadata(node_int_map)
 
         # clean environment
diff --git a/pkt_kg/metadata.py b/pkt_kg/metadata.py
index 623f0f5f7..fb977c4e7 100644
--- a/pkt_kg/metadata.py
+++ b/pkt_kg/metadata.py
@@ -4,6 +4,7 @@
 # import needed libraries
 import glob
 import json
+import logging.config
 import os
 import os.path
 import pandas  # type: ignore
@@ -22,6 +23,11 @@
 # set environmental variables
 oboinowl = Namespace('http://www.geneontology.org/formats/oboInOwl#')
 obo = Namespace('http://purl.obolibrary.org/obo/')
+# logging
+log_dir, log, log_config = 'builds/logs', 'pkt_build_log.log', glob.glob('**/logging.ini', recursive=True)
+if not os.path.exists(log_dir): os.mkdir(log_dir)
+logger = logging.getLogger(__name__)
+logging.config.fileConfig(log_config[0], disable_existing_loggers=False, defaults={'log_file': log_dir + '/' + log})
 
 
 class Metadata(object):
@@ -68,7 +74,7 @@ def node_metadata_processor(self) -> None:
 
         if self.node_data:
             print('Loading and Processing Node Metadata')
-
+            logger.info('Loading and Processing Node Metadata')
             with open(self.node_data[0], 'rb') as out:
                 self.node_dict = pickle.load(out, encoding="utf8")
 
@@ -89,7 +95,8 @@ def extracts_class_metadata(self, graph: Graph) -> None:
             None.
         """
 
-        print('\nExtracting Class Metadata')
+        print('\nExtracting Class and Relation Metadata')
+        logger.info('Extracting Class and Relation Metadata')
 
         if self.node_dict:
             domains = [
@@ -98,11 +105,11 @@ def extracts_class_metadata(self, graph: Graph) -> None:
                            and (OWL.Class in i[2] or OWL.NamedIndividual in i[2])
                            and ('#' not in str(i[0]) or '#' not in str(i[2]))]),
                 ('relations', [i[0] for i in list(graph.triples((None, RDF.type, None)))
-                               if isinstance(i[0], URIRef) and i[2] != OWL.AnnotationProperty])
+                               if isinstance(i[0], URIRef) and i[2] == OWL.ObjectProperty])
             ]
-            for x in domains:
-                key, temp_dict = 'nodes' if x[0] == 'nodes' else 'relations', dict()
-                for i in tqdm(x[1]):
+            for key, entities in domains:
+                temp_dict = dict()
+                for i in tqdm(entities):
                     labels = [x for x in list(graph.triples((i, RDFS.label, None)))]
                     descriptions = [x for x in list(graph.triples((i, obo.IAO_0000115, None)))]
                     synonyms = [x for x in list(graph.triples((i, None, None))) if 'synonym' in str(x[1]).lower()]
@@ -115,6 +122,14 @@ def extracts_class_metadata(self, graph: Graph) -> None:
                         }
                 self.node_dict[key] = {**self.node_dict[key], **temp_dict}
 
+            # add rdfs:subclassof and rdf:type
+            self.node_dict['relations'] = {
+                **self.node_dict['relations'],
+                **{'http://www.w3.org/2000/01/rdf-schema#subClassOf': {
+                    'Label': 'subClassOf', 'Description': 'The subject is a subclass of a class.', 'Synonym': 'None'},
+                    'http://www.w3.org/1999/02/22-rdf-syntax-ns#type': {
+                        'Label': 'type', 'Description': 'The subject is an instance of a class.', 'Synonym': 'None'}}}
+
             if self.node_data:
                 with open(self.node_data[0], 'wb') as out:
                     pickle.dump(self.node_dict, out)
@@ -170,7 +185,8 @@ def adds_ontology_annotations(self, filename: str, graph: Graph) -> Graph:
             graph: An rdflib graph object with edited ontology annotations.
         """
 
-        print('\n*** Adding Ontology Annotations ***')
+        print('\nAdding Ontology Annotations')
+        logger.info('Adding Ontology Annotations')
 
         # set annotation variables
         authors = 'Authors: Tiffany J. Callahan, William A. Baumgartner, Ignacio Tripodi, Adrianne L. Stefanski'
@@ -220,18 +236,19 @@ def output_knowledge_graph_metadata(self, node_integer_map: Dict) -> None:
 
         if self.node_dict:
             print('\nWriting Class Metadata')
+            logger.info('Writing Class Metadata')
             filename = self.full_kg[:-4] + '_NodeLabels.txt'
             with open(self.write_location + filename, 'w', encoding='utf-8') as out:
-                out.write('entity_type' + '\t' + 'integer_id' + '\t' + 'node_id' + '\t' + 'label' + '\t' +
+                out.write('entity_type' + '\t' + 'integer_id' + '\t' + 'entity_uri' + '\t' + 'label' + '\t' +
                           'description/definition' + '\t' + 'synonym' + '\n')
                 for nid, nint in tqdm(node_integer_map.items()):
-                    if nid not in self.node_dict['nodes'].keys() or nid not in self.node_dict['relations'].keys():
-                        etyp, lab, dsc, syn = 'NA', 'NA', 'NA', 'NA'
+                    if nid in self.node_dict['nodes'].keys():
+                        etyp, meta = 'NODES', self.node_dict['nodes'][nid]
+                    elif nid in self.node_dict['relations'].keys():
+                        etyp, meta = 'RELATIONS', self.node_dict['relations'][nid]
                     else:
-                        if nid in self.node_dict['nodes'].keys():
-                            etyp, meta = 'NODES', self.node_dict['nodes'][nid]
-                        if nid in self.node_dict['relations'].keys():
-                            etyp, meta = 'RELATIONS', self.node_dict['relations'][nid]
+                        meta, etyp, lab, dsc, syn = None, 'NA', 'NA', 'NA', 'NA'
+                    if meta is not None:
                         lab = meta['Label'] if meta['Label'] is not None else 'None'
                         dsc = meta['Description'] if meta['Description'] is not None else 'None'
                         syn = meta['Synonym'] if meta['Synonym'] is not None else 'None'
diff --git a/pkt_kg/owlnets.py b/pkt_kg/owlnets.py
index 802094d93..22eef08b6 100644
--- a/pkt_kg/owlnets.py
+++ b/pkt_kg/owlnets.py
@@ -3,6 +3,7 @@
 
 # import needed libraries
 import glob
+import logging.config
 import networkx  # type: ignore
 import os
 import os.path
@@ -19,6 +20,11 @@
 # add global variables
 obo = Namespace('http://purl.obolibrary.org/obo/')
 pkt = Namespace('https://github.com/callahantiff/PheKnowLator/pkt/')
+# logging
+log_dir, log, log_config = 'builds/logs', 'pkt_build_log.log', glob.glob('**/logging.ini', recursive=True)
+if not os.path.exists(log_dir): os.mkdir(log_dir)
+logger = logging.getLogger(__name__)
+logging.config.fileConfig(log_config[0], disable_existing_loggers=False, defaults={'log_file': log_dir + '/' + log})
 
 
 class OwlNets(object):
@@ -63,8 +69,10 @@ def __init__(self, graph: Union[Graph, str], write_location: str, filename: str,
 
         # VERIFY INPUT GRAPH
         if not isinstance(graph, Graph) and not isinstance(graph, str):
+            logger.error('TypeError: Graph must be an RDFLib Graph Object or a str.')
             raise TypeError('Graph must be an RDFLib Graph Object or a str.')
         elif isinstance(graph, Graph) and len(graph) == 0:
+            logger.error('ValueError: RDFLib Graph Object is empty.')
             raise ValueError('RDFLib Graph Object is empty.')
         else:
             self.graph = graph if isinstance(graph, Graph) else Graph().parse(graph)
@@ -83,7 +91,8 @@ def converts_rdflib_to_networkx_multidigraph(self) -> None:
             None.
         """
 
-        print('\nConverting RDFLib Graph to Networkx MultiDiGraph. Note, this process can take up to 60 minutes.')
+        print('Converting RDFLib Graph to Networkx MultiDiGraph. Note, this process can take up to 60 minutes.')
+        logger.info('Converting RDFLib Graph to Networkx MultiDiGraph. Note, this process can take up to 60 minutes.')
 
         for s, p, o in tqdm(self.graph):
             self.nx_mdg.add_edge(s, o, **{'key': p})
@@ -98,12 +107,12 @@ def removes_disjoint_with_axioms(self) -> None:
         """
 
         print('Removing owl:disjointWith Axioms')
+        logger.info('Removing owl:disjointWith Axioms')
 
         disjoint_elements = [x[0] for x in self.graph.triples((None, None, OWL.disjointWith))]
         for x in disjoint_elements:
             triples = list(self.graph.triples((x, None, None)))
             self.owl_nets_dict['disjointWith'][str(x)] = {tuple([(str(x[0]), str(x[1]), str(x[2])) for x in triples])}
-
             # remove axioms from graph
             for triple in triples:
                 self.graph.remove(triple)
@@ -124,6 +133,7 @@ def updates_class_instance_identifiers(self) -> None:
         """
 
         print('Re-mapping Instances of Classes to Class Identifiers')
+        logger.info('Re-mapping Instances of Classes to Class Identifiers')
 
         # get all class individuals in pheknowlator namespace and save as dict with value as original ontology class
         pkts = [x[0] for x in list(self.graph.triples((None, RDF.type, OWL.NamedIndividual))) if 'pkt' in str(x[0])]
@@ -162,7 +172,8 @@ def removes_edges_with_owl_semantics(self) -> Graph:
             filtered_graph: An RDFLib graph that only contains clinically and biologically meaningful triples.
         """
 
-        print('\nFiltering Triples')
+        print('Filtering Triples')
+        logger.info('Filtering Triples')
 
         keep_predicates, filtered_triples = set(), set()
         for x in tqdm(self.graph):
@@ -463,7 +474,8 @@ def cleans_owl_encoded_classes(self) -> Graph:
              An rdflib.Graph object that has been updated to only include triples owl decoded triples.
         """
 
-        print('\nDecoding OWL Constructors and Restrictions')
+        print('Decoding OWL Constructors and Restrictions')
+        logger.info('Decoding OWL Constructors and Restrictions')
 
         decoded_graph: Graph = Graph()
         cleaned_nodes: Set = set()
@@ -505,12 +517,21 @@ def cleans_owl_encoded_classes(self) -> Graph:
         print('=' * 75 + '\nDecoded {} owl-encoded classes. Note the following:'.format(len(cleaned_nodes)))
         print('{} cardinality elements'.format(len(list(self.owl_nets_dict['owl_nets']['cardinality'].keys()))))
         print('ignored {} misc elements'.format(len(list(self.owl_nets_dict['owl_nets']['misc'].keys()))))
-        print('{} owl:complementOf removed'.format(len(list(self.owl_nets_dict['owl_nets']['complementOf'].keys()))))
+        print('removed {} owl:complementOf'.format(len(list(self.owl_nets_dict['owl_nets']['complementOf'].keys()))))
         print('{} negation elements\n'.format(len(list(self.owl_nets_dict['owl_nets']['negation'].keys()))) + '=' * 75)
         del self.nx_mdg  # clean up environment
         self.graph = decoded_graph  # pass decoded classes through triple cleaner
         cleaned_decoded_graph = self.removes_edges_with_owl_semantics()
 
+        # update logging
+        res_string = 'Decoded {} classes; partially decoded {} cardinality elements; ignored {} misc elements; ' \
+                     'removed {} owl:complementOf and {} negation elements'
+        logger.info(res_string.format(
+            len(cleaned_nodes), len(list(self.owl_nets_dict['owl_nets']['cardinality'].keys())),
+            len(list(self.owl_nets_dict['owl_nets']['misc'].keys())),
+            len(list(self.owl_nets_dict['owl_nets']['complementOf'].keys())),
+            len(list(self.owl_nets_dict['owl_nets']['negation'].keys()))))
+
         return cleaned_decoded_graph
 
     def purifies_graph_build(self) -> None:
@@ -527,6 +548,7 @@ def purifies_graph_build(self) -> None:
         """
 
         print('Purifying Graph Based on Knowledge Graph Construction Approach')
+        logger.info('Purifying Graph Based on Knowledge Graph Construction Approach')
 
         # get original and purification relation
         org_rel = RDF.type if self.kg_construct_approach == 'subclass' else RDFS.subClassOf
@@ -565,7 +587,8 @@ def write_out_results(self, graph: Graph, kg_construction_approach: Optional[str
         """
 
         # write out owl-nets graph
-        print('\nSerializing OWL-NETS Graph')
+        print('Serializing OWL-NETS Graph')
+        logger.info('Serializing OWL-NETS Graph')
         f_name_lab = '_' + kg_construction_approach.upper() + '_purified' if kg_construction_approach else ''
         f_name = [self.filename[:-4] + f_name_lab if '.owl' in self.filename
                   else '.'.join(self.filename.split('.')[:-1]) + f_name_lab if '.' in self.filename
@@ -596,6 +619,9 @@ def run_owl_nets(self) -> Tuple:
         constructor and all restrictions; (5) removes non-OBO namespace classes; and (6) purifies decoded graph to
         input construction approach (i.e. None, subclass-based or instance-based).
 
+        NOTE: Need to update workflow to remove instance UUIDs differently if never adding more complex relations to
+        the KG (i.e. those with OWL constructors).
+
         Returns:
             not_purified_graph: An rdflib.Graph object that has been updated to only include triples owl decoded
                 triples.
@@ -603,7 +629,8 @@ def run_owl_nets(self) -> Tuple:
                 additional to being purified according to the kg_construct_approach.
         """
 
-        print('\nCreating OWL-NETS graph')
+        print('Creating OWL-NETS graph')
+        logger.info('Creating OWL-NETS graph')
 
         # run graph through OWL-NETS steps
         self.removes_disjoint_with_axioms()
diff --git a/pkt_kg/utils/kg_utils.py b/pkt_kg/utils/kg_utils.py
index 0c504f5a9..3ea501656 100644
--- a/pkt_kg/utils/kg_utils.py
+++ b/pkt_kg/utils/kg_utils.py
@@ -346,6 +346,34 @@ def finds_node_type(edge_info: Dict) -> Dict:
     return nodes
 
 
+def gets_class_ancestors(graph: Graph, class_uris: Set[Union[URIRef, str]], class_list: Optional[Set] = None) -> Set:
+    """A method that recursively searches an ontology hierarchy to pull all ancestor concepts for an input class.
+
+    Args:
+        graph: An RDFLib graph object assumed to contain ontology data.
+        class_uris: A list of at least one ontology class RDFLib URIRef object.
+        class_list: A list of URIs representing the ancestor classes found for the input class_uris.
+
+    Returns:
+        A list of ontology class ordered by the ontology hierarchy.
+    """
+
+    # instantiate list object if none passed to function
+    class_list = set() if class_list is None else class_list
+    class_list = set([x if isinstance(x, URIRef) else URIRef(obo + x) for x in class_list])
+
+    # check class uris are formatted correctly and get their ancestors
+    class_uris = set([x if isinstance(x, URIRef) else URIRef(obo + x) for x in class_uris])
+    ancestors = set([j for k in [list(graph.objects(x, RDFS.subClassOf)) for x in set(class_uris)] for j in k])
+
+    if len(ancestors) == 0 or len(ancestors.difference(class_list)) == 0:
+        return set([str(x) for x in class_list])
+    else:
+        class_uris = ancestors.difference(class_list)
+        class_list |= ancestors.difference(class_list)
+        return gets_class_ancestors(graph, class_uris, class_list)
+
+
 def maps_node_ids_to_integers(graph: Graph, write_location: str, output_ints: str, output_ints_map: str) -> Dict:
     """Loops over the knowledge graph in order to create three different types of files:
         - Integers: a tab-delimited `.txt` file containing three columns, one for each part of a triple (i.e.
@@ -469,31 +497,3 @@ def converts_rdflib_to_networkx(write_location: str, full_kg: str, graph: Option
     del nx_mdg   # clean up environment
 
     return None
-
-
-def gets_class_ancestors(graph: Graph, class_uris: Set[Union[URIRef, str]], class_list: Optional[Set] = None) -> Set:
-    """A method that recursively searches an ontology hierarchy to pull all ancestor concepts for an input class.
-
-    Args:
-        graph: An RDFLib graph object assumed to contain ontology data.
-        class_uris: A list of at least one ontology class RDFLib URIRef object.
-        class_list: A list of URIs representing the ancestor classes found for the input class_uris.
-
-    Returns:
-        A list of ontology class ordered by the ontology hierarchy.
-    """
-
-    # instantiate list object if none passed to function
-    class_list = set() if class_list is None else class_list
-    class_list = set([x if isinstance(x, URIRef) else URIRef(obo + x) for x in class_list])
-
-    # check class uris are formatted correctly and get their ancestors
-    class_uris = set([x if isinstance(x, URIRef) else URIRef(obo + x) for x in class_uris])
-    ancestors = set([j for k in [list(graph.objects(x, RDFS.subClassOf)) for x in set(class_uris)] for j in k])
-
-    if len(ancestors) == 0 or len(ancestors.difference(class_list)) == 0:
-        return set([str(x) for x in class_list])
-    else:
-        class_uris = ancestors.difference(class_list)
-        class_list |= ancestors.difference(class_list)
-        return gets_class_ancestors(graph, class_uris, class_list)
diff --git a/resources/construction_approach/README.md b/resources/construction_approach/README.md
index f4c690929..59ebd8c17 100644
--- a/resources/construction_approach/README.md
+++ b/resources/construction_approach/README.md
@@ -41,7 +41,7 @@ Instance-Instance | Relations + Inverse Relations | **`HGNC_1234567`, `REL`, `HG
 
 <br>
 
-*Note.* When a `UUDID` is used in the instance-based builds it is created as a md5 hash of the original `owl:Class` and added to the `pkt` namespace (i.e. `https://github.com/callahantiff/PheKnowLator/pkt/`).
+*Note.* When a `UUDID` is used in the instance-based builds it is created as a md5 hash of the original `owl:Class` and added to the `pkt` namespace (i.e. `https://github.com/callahantiff/PheKnowLator/pkt/`). This is so we have a way to track non-ontology class nodes that are added to the ontology and easily remove the `UUID` when running `OWL-NETS`.
 
 <br><br>
 
@@ -71,6 +71,8 @@ Class-Instance | Relations + Inverse Relations | **`GO_1234567`, `REL`, `HGNC_12
 Instance-Instance | Relations Only | **`HGNC_1234567`, `REL`, `HGNC_7654321`**<br><br>`HGNC_1234567`, `rdfs:subClassOf`, `subclass_dict[HGNC_1234567]`<br>`HGNC_1234567`, `rdf:type`, `owl:Class`<br>`HGNC_7654321`, `rdfs:subClassOf`, `subclass_dict[HGNC_7654321]`<br>`HGNC_7654321`, `rdf:type`, `owl:Class`<br>`HGNC_1234567`, `rdfs:subClassOf`, `UUID`<br>`UUID`, `rdf:type`, `owl:Restriction`<br>`UUID`, `owl:someValuesFrom`, `HGNC_7654321`<br>`UUID`, `owl:onProperty`, `REL`
 Instance-Instance | Relations + Inverse Relations | **`HGNC_1234567`, `REL`, `HGNC_7654321`<br>`HGNC_7654321`, `INV_REL`, `HGNC_1234567`**<br><br>`HGNC_1234567`, `rdfs:subClassOf`, `subclass_dict[HGNC_1234567]`<br>`HGNC_1234567`, `rdf:type`, `owl:Class`<br>`HGNC_7654321`, `rdfs:subClassOf`, `subclass_dict[HGNC_7654321]`<br>`HGNC_7654321`, `rdf:type`, `owl:Class`<br>`HGNC_1234567`, `rdfs:subClassOf`, `UUID1`<br>`UUID1`, `rdf:type`, `owl:Restriction`<br>`UUID1`, `owl:someValuesFrom`, `HGNC_7654321`<br>`UUID1`, `owl:onProperty`, `REL`<br>`HGNC_7654321`, `rdfs:subClassOf`, `UUID2`<br>`UUID2`, `rdf:type`, `owl:Restriction`<br>`UUID2`, `owl:someValuesFrom`, `HGNC_1234567`<br>`UUID2`, `owl:onProperty`, `INV_REL`
 
+*Note.* When `UUID` is used within the subclass-based construction approach it is actually a `BNode` and it is not placed within the `pkt` namespace. We don't use the same approach as for the instance-based construction becasue all non-ontology classes added to the knowledge graph are added as `owl.SubClassOf` an existing ontology class.
+
 <br>
 
 ***
diff --git a/resources/edge_data/edge_source_metadata.txt b/resources/edge_data/edge_source_metadata.txt
index 2d661653e..76969079f 100644
--- a/resources/edge_data/edge_source_metadata.txt
+++ b/resources/edge_data/edge_source_metadata.txt
@@ -1,5 +1,5 @@
 ===================================
-#Sun Jan 10 07:51:19 UTC 2021 
+#Wed Jan 27 04:45:45 UTC 2021 
 ===================================
 
 EDGE: chemical-disease
@@ -8,9 +8,9 @@ DATA PROCESSING INFO
   - FILTERING CRITERIA = None
   - EVIDENCE CRITERIA = data[5]!=''
 DATA INFO
-  - DOWNLOAD_URL = https://storage.googleapis.com/pheknowlator/release_v2.0.0/current_build/data/original_data/CTD_chemicals_diseases.tsv
-  - DOWNLOAD_DATE = 01/10/2021
-  - FILE_SIZE_IN_BYTES = 619999701
+  - DOWNLOAD_URL = https://storage.googleapis.com/pheknowlator/current_build/data/original_data/CTD_chemicals_diseases.tsv
+  - DOWNLOAD_DATE = 01/26/2021
+  - FILE_SIZE_IN_BYTES = 662525557
   - DOWNLOADED_FILE_LOCATION = resources/edge_data/chemical-disease_CTD_chemicals_diseases.tsv
 
 EDGE: chemical-gene
@@ -19,9 +19,9 @@ DATA PROCESSING INFO
   - FILTERING CRITERIA = data[6]==Homo sapiens | data[5].startswith('gene')
   - EVIDENCE CRITERIA = None
 DATA INFO
-  - DOWNLOAD_URL = https://storage.googleapis.com/pheknowlator/release_v2.0.0/current_build/data/original_data/CTD_chem_gene_ixns.tsv
-  - DOWNLOAD_DATE = 01/10/2021
-  - FILE_SIZE_IN_BYTES = 383684091
+  - DOWNLOAD_URL = https://storage.googleapis.com/pheknowlator/current_build/data/original_data/CTD_chem_gene_ixns.tsv
+  - DOWNLOAD_DATE = 01/26/2021
+  - FILE_SIZE_IN_BYTES = 416240734
   - DOWNLOADED_FILE_LOCATION = resources/edge_data/chemical-gene_CTD_chem_gene_ixns.tsv
 
 EDGE: chemical-gobp
@@ -30,9 +30,9 @@ DATA PROCESSING INFO
   - FILTERING CRITERIA = data[3]==Biological Process
   - EVIDENCE CRITERIA = data[8]<0.0001
 DATA INFO
-  - DOWNLOAD_URL = https://storage.googleapis.com/pheknowlator/release_v2.0.0/current_build/data/original_data/CTD_chem_go_enriched.tsv
-  - DOWNLOAD_DATE = 01/10/2021
-  - FILE_SIZE_IN_BYTES = 771242902
+  - DOWNLOAD_URL = https://storage.googleapis.com/pheknowlator/current_build/data/original_data/CTD_chem_go_enriched.tsv
+  - DOWNLOAD_DATE = 01/26/2021
+  - FILE_SIZE_IN_BYTES = 788894015
   - DOWNLOADED_FILE_LOCATION = resources/edge_data/chemical-gobp_CTD_chem_go_enriched.tsv
 
 EDGE: chemical-gocc
@@ -41,9 +41,9 @@ DATA PROCESSING INFO
   - FILTERING CRITERIA = data[3]==Cellular Component
   - EVIDENCE CRITERIA = data[8]<0.0001
 DATA INFO
-  - DOWNLOAD_URL = https://storage.googleapis.com/pheknowlator/release_v2.0.0/current_build/data/original_data/CTD_chem_go_enriched.tsv
-  - DOWNLOAD_DATE = 01/10/2021
-  - FILE_SIZE_IN_BYTES = 771242902
+  - DOWNLOAD_URL = https://storage.googleapis.com/pheknowlator/current_build/data/original_data/CTD_chem_go_enriched.tsv
+  - DOWNLOAD_DATE = 01/26/2021
+  - FILE_SIZE_IN_BYTES = 788894015
   - DOWNLOADED_FILE_LOCATION = resources/edge_data/chemical-gocc_CTD_chem_go_enriched.tsv
 
 EDGE: chemical-gomf
@@ -52,9 +52,9 @@ DATA PROCESSING INFO
   - FILTERING CRITERIA = data[3]==Molecular Function
   - EVIDENCE CRITERIA = data[8]<0.0001
 DATA INFO
-  - DOWNLOAD_URL = https://storage.googleapis.com/pheknowlator/release_v2.0.0/current_build/data/original_data/CTD_chem_go_enriched.tsv
-  - DOWNLOAD_DATE = 01/10/2021
-  - FILE_SIZE_IN_BYTES = 771242902
+  - DOWNLOAD_URL = https://storage.googleapis.com/pheknowlator/current_build/data/original_data/CTD_chem_go_enriched.tsv
+  - DOWNLOAD_DATE = 01/26/2021
+  - FILE_SIZE_IN_BYTES = 788894015
   - DOWNLOADED_FILE_LOCATION = resources/edge_data/chemical-gomf_CTD_chem_go_enriched.tsv
 
 EDGE: chemical-pathway
@@ -63,9 +63,9 @@ DATA PROCESSING INFO
   - FILTERING CRITERIA = data[5]==Homo sapiens
   - EVIDENCE CRITERIA = None
 DATA INFO
-  - DOWNLOAD_URL = https://storage.googleapis.com/pheknowlator/release_v2.0.0/current_build/data/original_data/ChEBI2Reactome_All_Levels.txt
-  - DOWNLOAD_DATE = 01/10/2021
-  - FILE_SIZE_IN_BYTES = 30255399
+  - DOWNLOAD_URL = https://storage.googleapis.com/pheknowlator/current_build/data/original_data/ChEBI2Reactome_All_Levels.txt
+  - DOWNLOAD_DATE = 01/26/2021
+  - FILE_SIZE_IN_BYTES = 29342364
   - DOWNLOADED_FILE_LOCATION = resources/edge_data/chemical-pathway_ChEBI2Reactome_All_Levels.txt
 
 EDGE: chemical-phenotype
@@ -74,9 +74,9 @@ DATA PROCESSING INFO
   - FILTERING CRITERIA = None
   - EVIDENCE CRITERIA = data[5]!=''
 DATA INFO
-  - DOWNLOAD_URL = https://storage.googleapis.com/pheknowlator/release_v2.0.0/current_build/data/original_data/CTD_chemicals_diseases.tsv
-  - DOWNLOAD_DATE = 01/10/2021
-  - FILE_SIZE_IN_BYTES = 619999701
+  - DOWNLOAD_URL = https://storage.googleapis.com/pheknowlator/current_build/data/original_data/CTD_chemicals_diseases.tsv
+  - DOWNLOAD_DATE = 01/26/2021
+  - FILE_SIZE_IN_BYTES = 662525557
   - DOWNLOADED_FILE_LOCATION = resources/edge_data/chemical-phenotype_CTD_chemicals_diseases.tsv
 
 EDGE: chemical-protein
@@ -85,9 +85,9 @@ DATA PROCESSING INFO
   - FILTERING CRITERIA = data[6]==Homo sapiens | data[5] .startswith('protein') 
   - EVIDENCE CRITERIA = None
 DATA INFO
-  - DOWNLOAD_URL = https://storage.googleapis.com/pheknowlator/release_v2.0.0/current_build/data/original_data/CTD_chem_gene_ixns.tsv
-  - DOWNLOAD_DATE = 01/10/2021
-  - FILE_SIZE_IN_BYTES = 383684091
+  - DOWNLOAD_URL = https://storage.googleapis.com/pheknowlator/current_build/data/original_data/CTD_chem_gene_ixns.tsv
+  - DOWNLOAD_DATE = 01/26/2021
+  - FILE_SIZE_IN_BYTES = 416240734
   - DOWNLOADED_FILE_LOCATION = resources/edge_data/chemical-protein_CTD_chem_gene_ixns.tsv
 
 EDGE: chemical-rna
@@ -96,9 +96,9 @@ DATA PROCESSING INFO
   - FILTERING CRITERIA = data[6]==Homo sapiens | data[5].startswith('mRNA')
   - EVIDENCE CRITERIA = None
 DATA INFO
-  - DOWNLOAD_URL = https://storage.googleapis.com/pheknowlator/release_v2.0.0/current_build/data/original_data/CTD_chem_gene_ixns.tsv
-  - DOWNLOAD_DATE = 01/10/2021
-  - FILE_SIZE_IN_BYTES = 383684091
+  - DOWNLOAD_URL = https://storage.googleapis.com/pheknowlator/current_build/data/original_data/CTD_chem_gene_ixns.tsv
+  - DOWNLOAD_DATE = 01/26/2021
+  - FILE_SIZE_IN_BYTES = 416240734
   - DOWNLOADED_FILE_LOCATION = resources/edge_data/chemical-rna_CTD_chem_gene_ixns.tsv
 
 EDGE: disease-phenotype
@@ -107,9 +107,9 @@ DATA PROCESSING INFO
   - FILTERING CRITERIA = None
   - EVIDENCE CRITERIA = None
 DATA INFO
-  - DOWNLOAD_URL = https://storage.googleapis.com/pheknowlator/release_v2.0.0/current_build/data/original_data/phenotype_annotation.tab
-  - DOWNLOAD_DATE = 01/10/2021
-  - FILE_SIZE_IN_BYTES = 7369779
+  - DOWNLOAD_URL = https://storage.googleapis.com/pheknowlator/current_build/data/original_data/phenotype_annotation.tab
+  - DOWNLOAD_DATE = 01/26/2021
+  - FILE_SIZE_IN_BYTES = 25605489
   - DOWNLOADED_FILE_LOCATION = resources/edge_data/disease-phenotype_phenotype_annotation.tab
 
 EDGE: gene-disease
@@ -118,9 +118,9 @@ DATA PROCESSING INFO
   - FILTERING CRITERIA = None
   - EVIDENCE CRITERIA = data[10]>=0.70
 DATA INFO
-  - DOWNLOAD_URL = https://storage.googleapis.com/pheknowlator/release_v2.0.0/current_build/data/original_data/curated_gene_disease_associations.tsv
-  - DOWNLOAD_DATE = 01/10/2021
-  - FILE_SIZE_IN_BYTES = 11055467
+  - DOWNLOAD_URL = https://storage.googleapis.com/pheknowlator/current_build/data/original_data/curated_gene_disease_associations.tsv
+  - DOWNLOAD_DATE = 01/26/2021
+  - FILE_SIZE_IN_BYTES = 11542996
   - DOWNLOADED_FILE_LOCATION = resources/edge_data/gene-disease_curated_gene_disease_associations.tsv
 
 EDGE: gene-gene
@@ -129,8 +129,8 @@ DATA PROCESSING INFO
   - FILTERING CRITERIA = None
   - EVIDENCE CRITERIA = data[2]>=0.00012
 DATA INFO
-  - DOWNLOAD_URL = https://storage.googleapis.com/pheknowlator/release_v2.0.0/current_build/data/original_data/COMBINED.DEFAULT_NETWORKS.BP_COMBINING.txt
-  - DOWNLOAD_DATE = 01/10/2021
+  - DOWNLOAD_URL = https://storage.googleapis.com/pheknowlator/current_build/data/original_data/COMBINED.DEFAULT_NETWORKS.BP_COMBINING.txt
+  - DOWNLOAD_DATE = 01/26/2021
   - FILE_SIZE_IN_BYTES = 270656653
   - DOWNLOADED_FILE_LOCATION = resources/edge_data/gene-gene_COMBINED.DEFAULT_NETWORKS.BP_COMBINING.txt
 
@@ -140,9 +140,9 @@ DATA PROCESSING INFO
   - FILTERING CRITERIA = data[3].startswith('REACT:R-HSA-')
   - EVIDENCE CRITERIA = None
 DATA INFO
-  - DOWNLOAD_URL = https://storage.googleapis.com/pheknowlator/release_v2.0.0/current_build/data/original_data/CTD_genes_pathways.tsv
-  - DOWNLOAD_DATE = 01/10/2021
-  - FILE_SIZE_IN_BYTES = 8193942
+  - DOWNLOAD_URL = https://storage.googleapis.com/pheknowlator/current_build/data/original_data/CTD_genes_pathways.tsv
+  - DOWNLOAD_DATE = 01/26/2021
+  - FILE_SIZE_IN_BYTES = 8194289
   - DOWNLOADED_FILE_LOCATION = resources/edge_data/gene-pathway_CTD_genes_pathways.tsv
 
 EDGE: gene-phenotype
@@ -151,9 +151,9 @@ DATA PROCESSING INFO
   - FILTERING CRITERIA = None
   - EVIDENCE CRITERIA = data[10]>=0.70
 DATA INFO
-  - DOWNLOAD_URL = https://storage.googleapis.com/pheknowlator/release_v2.0.0/current_build/data/original_data/curated_gene_disease_associations.tsv
-  - DOWNLOAD_DATE = 01/10/2021
-  - FILE_SIZE_IN_BYTES = 11055467
+  - DOWNLOAD_URL = https://storage.googleapis.com/pheknowlator/current_build/data/original_data/curated_gene_disease_associations.tsv
+  - DOWNLOAD_DATE = 01/26/2021
+  - FILE_SIZE_IN_BYTES = 11542996
   - DOWNLOADED_FILE_LOCATION = resources/edge_data/gene-phenotype_curated_gene_disease_associations.tsv
 
 EDGE: gene-protein
@@ -162,9 +162,9 @@ DATA PROCESSING INFO
   - FILTERING CRITERIA = data[2]==protein-coding
   - EVIDENCE CRITERIA = None
 DATA INFO
-  - DOWNLOAD_URL = https://storage.googleapis.com/pheknowlator/release_v2.0.0/current_build/data/processed_data/ENTREZ_GENE_PRO_ONTOLOGY_MAP.txt
-  - DOWNLOAD_DATE = 01/10/2021
-  - FILE_SIZE_IN_BYTES = 2536394
+  - DOWNLOAD_URL = https://storage.googleapis.com/pheknowlator/current_build/data/processed_data/ENTREZ_GENE_PRO_ONTOLOGY_MAP.txt
+  - DOWNLOAD_DATE = 01/26/2021
+  - FILE_SIZE_IN_BYTES = 2433182
   - DOWNLOADED_FILE_LOCATION = resources/edge_data/gene-protein_ENTREZ_GENE_PRO_ONTOLOGY_MAP.txt
 
 EDGE: gene-rna
@@ -173,9 +173,9 @@ DATA PROCESSING INFO
   - FILTERING CRITERIA = None
   - EVIDENCE CRITERIA = None
 DATA INFO
-  - DOWNLOAD_URL = https://storage.googleapis.com/pheknowlator/release_v2.0.0/current_build/data/processed_data/ENTREZ_GENE_ENSEMBL_TRANSCRIPT_MAP.txt
-  - DOWNLOAD_DATE = 01/10/2021
-  - FILE_SIZE_IN_BYTES = 16378762
+  - DOWNLOAD_URL = https://storage.googleapis.com/pheknowlator/current_build/data/processed_data/ENTREZ_GENE_ENSEMBL_TRANSCRIPT_MAP.txt
+  - DOWNLOAD_DATE = 01/26/2021
+  - FILE_SIZE_IN_BYTES = 12996836
   - DOWNLOADED_FILE_LOCATION = resources/edge_data/gene-rna_ENTREZ_GENE_ENSEMBL_TRANSCRIPT_MAP.txt
 
 EDGE: gobp-pathway
@@ -184,9 +184,9 @@ DATA PROCESSING INFO
   - FILTERING CRITERIA = data[8]==P | data[12]==taxon:9606 | data[5].startswith('REACTOME')
   - EVIDENCE CRITERIA = None
 DATA INFO
-  - DOWNLOAD_URL = https://storage.googleapis.com/pheknowlator/release_v2.0.0/current_build/data/original_data/gene_association.reactome
-  - DOWNLOAD_DATE = 01/10/2021
-  - FILE_SIZE_IN_BYTES = 12558216
+  - DOWNLOAD_URL = https://storage.googleapis.com/pheknowlator/current_build/data/original_data/gene_association.reactome
+  - DOWNLOAD_DATE = 01/26/2021
+  - FILE_SIZE_IN_BYTES = 12615150
   - DOWNLOADED_FILE_LOCATION = resources/edge_data/gobp-pathway_gene_association.reactome
 
 EDGE: pathway-gocc
@@ -195,9 +195,9 @@ DATA PROCESSING INFO
   - FILTERING CRITERIA = data[8]==C | data[12]==taxon:9606 | data[5].startswith('REACTOME')
   - EVIDENCE CRITERIA = None
 DATA INFO
-  - DOWNLOAD_URL = https://storage.googleapis.com/pheknowlator/release_v2.0.0/current_build/data/original_data/gene_association.reactome
-  - DOWNLOAD_DATE = 01/10/2021
-  - FILE_SIZE_IN_BYTES = 12558216
+  - DOWNLOAD_URL = https://storage.googleapis.com/pheknowlator/current_build/data/original_data/gene_association.reactome
+  - DOWNLOAD_DATE = 01/26/2021
+  - FILE_SIZE_IN_BYTES = 12615150
   - DOWNLOADED_FILE_LOCATION = resources/edge_data/pathway-gocc_gene_association.reactome
 
 EDGE: pathway-gomf
@@ -206,9 +206,9 @@ DATA PROCESSING INFO
   - FILTERING CRITERIA = data[8]==F | data[12]==taxon:9606 | data[5].startswith('REACTOME')
   - EVIDENCE CRITERIA = None
 DATA INFO
-  - DOWNLOAD_URL = https://storage.googleapis.com/pheknowlator/release_v2.0.0/current_build/data/original_data/gene_association.reactome
-  - DOWNLOAD_DATE = 01/10/2021
-  - FILE_SIZE_IN_BYTES = 12558216
+  - DOWNLOAD_URL = https://storage.googleapis.com/pheknowlator/current_build/data/original_data/gene_association.reactome
+  - DOWNLOAD_DATE = 01/26/2021
+  - FILE_SIZE_IN_BYTES = 12615150
   - DOWNLOADED_FILE_LOCATION = resources/edge_data/pathway-gomf_gene_association.reactome
 
 EDGE: protein-anatomy
@@ -217,9 +217,9 @@ DATA PROCESSING INFO
   - FILTERING CRITERIA = data[3]==Evidence at protein level | data[4]==anatomy
   - EVIDENCE CRITERIA = None
 DATA INFO
-  - DOWNLOAD_URL = https://storage.googleapis.com/pheknowlator/release_v2.0.0/current_build/data/processed_data/HPA_GTEX_RNA_GENE_PROTEIN_EDGES.txt
-  - DOWNLOAD_DATE = 01/10/2021
-  - FILE_SIZE_IN_BYTES = 20467831
+  - DOWNLOAD_URL = https://storage.googleapis.com/pheknowlator/current_build/data/processed_data/HPA_GTEX_RNA_GENE_PROTEIN_EDGES.txt
+  - DOWNLOAD_DATE = 01/26/2021
+  - FILE_SIZE_IN_BYTES = 20611429
   - DOWNLOADED_FILE_LOCATION = resources/edge_data/protein-anatomy_HPA_GTEX_RNA_GENE_PROTEIN_EDGES.txt
 
 EDGE: protein-catalyst
@@ -228,9 +228,9 @@ DATA PROCESSING INFO
   - FILTERING CRITERIA = None
   - EVIDENCE CRITERIA = None
 DATA INFO
-  - DOWNLOAD_URL = https://storage.googleapis.com/pheknowlator/release_v2.0.0/current_build/data/processed_data/UNIPROT_PROTEIN_CATALYST.txt
-  - DOWNLOAD_DATE = 01/10/2021
-  - FILE_SIZE_IN_BYTES = 1092346
+  - DOWNLOAD_URL = https://storage.googleapis.com/pheknowlator/current_build/data/processed_data/UNIPROT_PROTEIN_CATALYST.txt
+  - DOWNLOAD_DATE = 01/26/2021
+  - FILE_SIZE_IN_BYTES = 1583158
   - DOWNLOADED_FILE_LOCATION = resources/edge_data/protein-catalyst_UNIPROT_PROTEIN_CATALYST.txt
 
 EDGE: protein-cell
@@ -239,9 +239,9 @@ DATA PROCESSING INFO
   - FILTERING CRITERIA = data[3]==Evidence at protein level | data[4]==cell line
   - EVIDENCE CRITERIA = None
 DATA INFO
-  - DOWNLOAD_URL = https://storage.googleapis.com/pheknowlator/release_v2.0.0/current_build/data/processed_data/HPA_GTEX_RNA_GENE_PROTEIN_EDGES.txt
-  - DOWNLOAD_DATE = 01/10/2021
-  - FILE_SIZE_IN_BYTES = 20467831
+  - DOWNLOAD_URL = https://storage.googleapis.com/pheknowlator/current_build/data/processed_data/HPA_GTEX_RNA_GENE_PROTEIN_EDGES.txt
+  - DOWNLOAD_DATE = 01/26/2021
+  - FILE_SIZE_IN_BYTES = 20611429
   - DOWNLOADED_FILE_LOCATION = resources/edge_data/protein-cell_HPA_GTEX_RNA_GENE_PROTEIN_EDGES.txt
 
 EDGE: protein-cofactor
@@ -250,9 +250,9 @@ DATA PROCESSING INFO
   - FILTERING CRITERIA = None
   - EVIDENCE CRITERIA = None
 DATA INFO
-  - DOWNLOAD_URL = https://storage.googleapis.com/pheknowlator/release_v2.0.0/current_build/data/processed_data/UNIPROT_PROTEIN_COFACTOR.txt
-  - DOWNLOAD_DATE = 01/10/2021
-  - FILE_SIZE_IN_BYTES = 101341
+  - DOWNLOAD_URL = https://storage.googleapis.com/pheknowlator/current_build/data/processed_data/UNIPROT_PROTEIN_COFACTOR.txt
+  - DOWNLOAD_DATE = 01/26/2021
+  - FILE_SIZE_IN_BYTES = 174816
   - DOWNLOADED_FILE_LOCATION = resources/edge_data/protein-cofactor_UNIPROT_PROTEIN_COFACTOR.txt
 
 EDGE: protein-gobp
@@ -261,9 +261,9 @@ DATA PROCESSING INFO
   - FILTERING CRITERIA = data[8]==P | data[12]==taxon:9606
   - EVIDENCE CRITERIA = None
 DATA INFO
-  - DOWNLOAD_URL = https://storage.googleapis.com/pheknowlator/release_v2.0.0/current_build/data/original_data/goa_human.gaf
-  - DOWNLOAD_DATE = 01/10/2021
-  - FILE_SIZE_IN_BYTES = 84936865
+  - DOWNLOAD_URL = https://storage.googleapis.com/pheknowlator/current_build/data/original_data/goa_human.gaf
+  - DOWNLOAD_DATE = 01/26/2021
+  - FILE_SIZE_IN_BYTES = 103225127
   - DOWNLOADED_FILE_LOCATION = resources/edge_data/protein-gobp_goa_human.gaf
 
 EDGE: protein-gocc
@@ -272,9 +272,9 @@ DATA PROCESSING INFO
   - FILTERING CRITERIA = data[8]==C | data[12]==taxon:9606
   - EVIDENCE CRITERIA = None
 DATA INFO
-  - DOWNLOAD_URL = https://storage.googleapis.com/pheknowlator/release_v2.0.0/current_build/data/original_data/goa_human.gaf
-  - DOWNLOAD_DATE = 01/10/2021
-  - FILE_SIZE_IN_BYTES = 84936865
+  - DOWNLOAD_URL = https://storage.googleapis.com/pheknowlator/current_build/data/original_data/goa_human.gaf
+  - DOWNLOAD_DATE = 01/26/2021
+  - FILE_SIZE_IN_BYTES = 103225127
   - DOWNLOADED_FILE_LOCATION = resources/edge_data/protein-gocc_goa_human.gaf
 
 EDGE: protein-gomf
@@ -283,9 +283,9 @@ DATA PROCESSING INFO
   - FILTERING CRITERIA = data[8]==F | data[12]==taxon:9606
   - EVIDENCE CRITERIA = None
 DATA INFO
-  - DOWNLOAD_URL = https://storage.googleapis.com/pheknowlator/release_v2.0.0/current_build/data/original_data/goa_human.gaf
-  - DOWNLOAD_DATE = 01/10/2021
-  - FILE_SIZE_IN_BYTES = 84936865
+  - DOWNLOAD_URL = https://storage.googleapis.com/pheknowlator/current_build/data/original_data/goa_human.gaf
+  - DOWNLOAD_DATE = 01/26/2021
+  - FILE_SIZE_IN_BYTES = 103225127
   - DOWNLOADED_FILE_LOCATION = resources/edge_data/protein-gomf_goa_human.gaf
 
 EDGE: protein-pathway
@@ -294,9 +294,9 @@ DATA PROCESSING INFO
   - FILTERING CRITERIA = data[5]==Homo sapiens
   - EVIDENCE CRITERIA = None
 DATA INFO
-  - DOWNLOAD_URL = https://storage.googleapis.com/pheknowlator/release_v2.0.0/current_build/data/original_data/UniProt2Reactome_All_Levels.txt
-  - DOWNLOAD_DATE = 01/10/2021
-  - FILE_SIZE_IN_BYTES = 109193926
+  - DOWNLOAD_URL = https://storage.googleapis.com/pheknowlator/current_build/data/original_data/UniProt2Reactome_All_Levels.txt
+  - DOWNLOAD_DATE = 01/26/2021
+  - FILE_SIZE_IN_BYTES = 102386977
   - DOWNLOADED_FILE_LOCATION = resources/edge_data/protein-pathway_UniProt2Reactome_All_Levels.txt
 
 EDGE: protein-protein
@@ -305,8 +305,8 @@ DATA PROCESSING INFO
   - FILTERING CRITERIA = None
   - EVIDENCE CRITERIA = data[2]>=700
 DATA INFO
-  - DOWNLOAD_URL = https://storage.googleapis.com/pheknowlator/release_v2.0.0/current_build/data/original_data/9606.protein.links.v11.0.txt
-  - DOWNLOAD_DATE = 01/10/2021
+  - DOWNLOAD_URL = https://storage.googleapis.com/pheknowlator/current_build/data/original_data/9606.protein.links.v11.0.txt
+  - DOWNLOAD_DATE = 01/26/2021
   - FILE_SIZE_IN_BYTES = 540934917
   - DOWNLOADED_FILE_LOCATION = resources/edge_data/protein-protein_9606.protein.links.v11.0.txt
 
@@ -316,9 +316,9 @@ DATA PROCESSING INFO
   - FILTERING CRITERIA = data[3]==Evidence at transcript level | data[4]==anatomy
   - EVIDENCE CRITERIA = None
 DATA INFO
-  - DOWNLOAD_URL = https://storage.googleapis.com/pheknowlator/release_v2.0.0/current_build/data/processed_data/HPA_GTEX_RNA_GENE_PROTEIN_EDGES.txt
-  - DOWNLOAD_DATE = 01/10/2021
-  - FILE_SIZE_IN_BYTES = 20467831
+  - DOWNLOAD_URL = https://storage.googleapis.com/pheknowlator/current_build/data/processed_data/HPA_GTEX_RNA_GENE_PROTEIN_EDGES.txt
+  - DOWNLOAD_DATE = 01/26/2021
+  - FILE_SIZE_IN_BYTES = 20611429
   - DOWNLOADED_FILE_LOCATION = resources/edge_data/rna-anatomy_HPA_GTEX_RNA_GENE_PROTEIN_EDGES.txt
 
 EDGE: rna-cell
@@ -327,9 +327,9 @@ DATA PROCESSING INFO
   - FILTERING CRITERIA = data[3]==Evidence at transcript level | data[4]==cell line
   - EVIDENCE CRITERIA = None
 DATA INFO
-  - DOWNLOAD_URL = https://storage.googleapis.com/pheknowlator/release_v2.0.0/current_build/data/processed_data/HPA_GTEX_RNA_GENE_PROTEIN_EDGES.txt
-  - DOWNLOAD_DATE = 01/10/2021
-  - FILE_SIZE_IN_BYTES = 20467831
+  - DOWNLOAD_URL = https://storage.googleapis.com/pheknowlator/current_build/data/processed_data/HPA_GTEX_RNA_GENE_PROTEIN_EDGES.txt
+  - DOWNLOAD_DATE = 01/26/2021
+  - FILE_SIZE_IN_BYTES = 20611429
   - DOWNLOADED_FILE_LOCATION = resources/edge_data/rna-cell_HPA_GTEX_RNA_GENE_PROTEIN_EDGES.txt
 
 EDGE: rna-protein
@@ -338,9 +338,9 @@ DATA PROCESSING INFO
   - FILTERING CRITERIA = data[2]==protein-coding
   - EVIDENCE CRITERIA = None
 DATA INFO
-  - DOWNLOAD_URL = https://storage.googleapis.com/pheknowlator/release_v2.0.0/current_build/data/processed_data/ENSEMBL_TRANSCRIPT_PROTEIN_ONTOLOGY_MAP.txt
-  - DOWNLOAD_DATE = 01/10/2021
-  - FILE_SIZE_IN_BYTES = 28862488
+  - DOWNLOAD_URL = https://storage.googleapis.com/pheknowlator/current_build/data/processed_data/ENSEMBL_TRANSCRIPT_PROTEIN_ONTOLOGY_MAP.txt
+  - DOWNLOAD_DATE = 01/26/2021
+  - FILE_SIZE_IN_BYTES = 5970076
   - DOWNLOADED_FILE_LOCATION = resources/edge_data/rna-protein_ENSEMBL_TRANSCRIPT_PROTEIN_ONTOLOGY_MAP.txt
 
 EDGE: variant-disease
@@ -349,9 +349,9 @@ DATA PROCESSING INFO
   - FILTERING CRITERIA = data[9]!=-1 | data[16]==GRCh38 | data[8-9]dedupdesc
   - EVIDENCE CRITERIA = data[24] in ["criteria provided, multiple submitters, no conflicts", "reviewed by expert panel", "practice guideline", "criteria provided, single submitter"] | data[7]==1
 DATA INFO
-  - DOWNLOAD_URL = https://storage.googleapis.com/pheknowlator/release_v2.0.0/current_build/data/processed_data/CLINVAR_VARIANT_GENE_DISEASE_PHENOTYPE_EDGES.txt
-  - DOWNLOAD_DATE = 01/10/2021
-  - FILE_SIZE_IN_BYTES = 1437554430
+  - DOWNLOAD_URL = https://storage.googleapis.com/pheknowlator/current_build/data/processed_data/CLINVAR_VARIANT_GENE_DISEASE_PHENOTYPE_EDGES.txt
+  - DOWNLOAD_DATE = 01/26/2021
+  - FILE_SIZE_IN_BYTES = 0
   - DOWNLOADED_FILE_LOCATION = resources/edge_data/variant-disease_CLINVAR_VARIANT_GENE_DISEASE_PHENOTYPE_EDGES.txt
 
 EDGE: variant-gene
@@ -360,9 +360,9 @@ DATA PROCESSING INFO
   - FILTERING CRITERIA = data[9]!=-1 | data[16]==GRCh38 | data[8-9]dedupdesc
   - EVIDENCE CRITERIA = data[24] in ["criteria provided, multiple submitters, no conflicts", "reviewed by expert panel", "practice guideline", "criteria provided, single submitter"]
 DATA INFO
-  - DOWNLOAD_URL = https://storage.googleapis.com/pheknowlator/release_v2.0.0/current_build/data/processed_data/CLINVAR_VARIANT_GENE_DISEASE_PHENOTYPE_EDGES.txt
-  - DOWNLOAD_DATE = 01/10/2021
-  - FILE_SIZE_IN_BYTES = 1437554430
+  - DOWNLOAD_URL = https://storage.googleapis.com/pheknowlator/current_build/data/processed_data/CLINVAR_VARIANT_GENE_DISEASE_PHENOTYPE_EDGES.txt
+  - DOWNLOAD_DATE = 01/26/2021
+  - FILE_SIZE_IN_BYTES = 0
   - DOWNLOADED_FILE_LOCATION = resources/edge_data/variant-gene_CLINVAR_VARIANT_GENE_DISEASE_PHENOTYPE_EDGES.txt
 
 EDGE: variant-phenotype
@@ -371,8 +371,8 @@ DATA PROCESSING INFO
   - FILTERING CRITERIA = data[9]!=-1 | data[16]==GRCh38 | data[8-9]dedupdesc
   - EVIDENCE CRITERIA = data[24] in ["criteria provided, multiple submitters, no conflicts", "reviewed by expert panel", "practice guideline", "criteria provided, single submitter"] | data[7]==1
 DATA INFO
-  - DOWNLOAD_URL = https://storage.googleapis.com/pheknowlator/release_v2.0.0/current_build/data/processed_data/CLINVAR_VARIANT_GENE_DISEASE_PHENOTYPE_EDGES.txt
-  - DOWNLOAD_DATE = 01/10/2021
-  - FILE_SIZE_IN_BYTES = 1437554430
+  - DOWNLOAD_URL = https://storage.googleapis.com/pheknowlator/current_build/data/processed_data/CLINVAR_VARIANT_GENE_DISEASE_PHENOTYPE_EDGES.txt
+  - DOWNLOAD_DATE = 01/26/2021
+  - FILE_SIZE_IN_BYTES = 0
   - DOWNLOADED_FILE_LOCATION = resources/edge_data/variant-phenotype_CLINVAR_VARIANT_GENE_DISEASE_PHENOTYPE_EDGES.txt
 
diff --git a/resources/edge_source_list.txt b/resources/edge_source_list.txt
index 84f553cf9..cd959bc58 100644
--- a/resources/edge_source_list.txt
+++ b/resources/edge_source_list.txt
@@ -1,34 +1,34 @@
-chemical-disease, https://storage.googleapis.com/pheknowlator/release_v2.0.0/current_build/data/original_data/CTD_chemicals_diseases.tsv
-chemical-gene, https://storage.googleapis.com/pheknowlator/release_v2.0.0/current_build/data/original_data/CTD_chem_gene_ixns.tsv
-chemical-gobp, https://storage.googleapis.com/pheknowlator/release_v2.0.0/current_build/data/original_data/CTD_chem_go_enriched.tsv
-chemical-gocc, https://storage.googleapis.com/pheknowlator/release_v2.0.0/current_build/data/original_data/CTD_chem_go_enriched.tsv
-chemical-gomf, https://storage.googleapis.com/pheknowlator/release_v2.0.0/current_build/data/original_data/CTD_chem_go_enriched.tsv
-chemical-pathway, https://storage.googleapis.com/pheknowlator/release_v2.0.0/current_build/data/original_data/ChEBI2Reactome_All_Levels.txt
-chemical-phenotype, https://storage.googleapis.com/pheknowlator/release_v2.0.0/current_build/data/original_data/CTD_chemicals_diseases.tsv
-chemical-protein, https://storage.googleapis.com/pheknowlator/release_v2.0.0/current_build/data/original_data/CTD_chem_gene_ixns.tsv
-chemical-rna, https://storage.googleapis.com/pheknowlator/release_v2.0.0/current_build/data/original_data/CTD_chem_gene_ixns.tsv
-disease-phenotype, https://storage.googleapis.com/pheknowlator/release_v2.0.0/current_build/data/original_data/phenotype_annotation.tab
-gene-disease, https://storage.googleapis.com/pheknowlator/release_v2.0.0/current_build/data/original_data/curated_gene_disease_associations.tsv
-gene-gene, https://storage.googleapis.com/pheknowlator/release_v2.0.0/current_build/data/original_data/COMBINED.DEFAULT_NETWORKS.BP_COMBINING.txt
-gene-pathway, https://storage.googleapis.com/pheknowlator/release_v2.0.0/current_build/data/original_data/CTD_genes_pathways.tsv
-gene-phenotype, https://storage.googleapis.com/pheknowlator/release_v2.0.0/current_build/data/original_data/curated_gene_disease_associations.tsv
-gene-protein, https://storage.googleapis.com/pheknowlator/release_v2.0.0/current_build/data/processed_data/ENTREZ_GENE_PRO_ONTOLOGY_MAP.txt
-gene-rna, https://storage.googleapis.com/pheknowlator/release_v2.0.0/current_build/data/processed_data/ENTREZ_GENE_ENSEMBL_TRANSCRIPT_MAP.txt
-gobp-pathway, https://storage.googleapis.com/pheknowlator/release_v2.0.0/current_build/data/original_data/gene_association.reactome
-pathway-gocc, https://storage.googleapis.com/pheknowlator/release_v2.0.0/current_build/data/original_data/gene_association.reactome
-pathway-gomf, https://storage.googleapis.com/pheknowlator/release_v2.0.0/current_build/data/original_data/gene_association.reactome
-protein-anatomy, https://storage.googleapis.com/pheknowlator/release_v2.0.0/current_build/data/processed_data/HPA_GTEX_RNA_GENE_PROTEIN_EDGES.txt
-protein-catalyst, https://storage.googleapis.com/pheknowlator/release_v2.0.0/current_build/data/processed_data/UNIPROT_PROTEIN_CATALYST.txt
-protein-cell, https://storage.googleapis.com/pheknowlator/release_v2.0.0/current_build/data/processed_data/HPA_GTEX_RNA_GENE_PROTEIN_EDGES.txt
-protein-cofactor, https://storage.googleapis.com/pheknowlator/release_v2.0.0/current_build/data/processed_data/UNIPROT_PROTEIN_COFACTOR.txt
-protein-gobp, https://storage.googleapis.com/pheknowlator/release_v2.0.0/current_build/data/original_data/goa_human.gaf
-protein-gocc, https://storage.googleapis.com/pheknowlator/release_v2.0.0/current_build/data/original_data/goa_human.gaf
-protein-gomf, https://storage.googleapis.com/pheknowlator/release_v2.0.0/current_build/data/original_data/goa_human.gaf
-protein-pathway, https://storage.googleapis.com/pheknowlator/release_v2.0.0/current_build/data/original_data/UniProt2Reactome_All_Levels.txt
-protein-protein, https://storage.googleapis.com/pheknowlator/release_v2.0.0/current_build/data/original_data/9606.protein.links.v11.0.txt
-rna-anatomy, https://storage.googleapis.com/pheknowlator/release_v2.0.0/current_build/data/processed_data/HPA_GTEX_RNA_GENE_PROTEIN_EDGES.txt
-rna-cell, https://storage.googleapis.com/pheknowlator/release_v2.0.0/current_build/data/processed_data/HPA_GTEX_RNA_GENE_PROTEIN_EDGES.txt
-rna-protein, https://storage.googleapis.com/pheknowlator/release_v2.0.0/current_build/data/processed_data/ENSEMBL_TRANSCRIPT_PROTEIN_ONTOLOGY_MAP.txt
-variant-disease, https://storage.googleapis.com/pheknowlator/release_v2.0.0/current_build/data/processed_data/CLINVAR_VARIANT_GENE_DISEASE_PHENOTYPE_EDGES.txt
-variant-gene, https://storage.googleapis.com/pheknowlator/release_v2.0.0/current_build/data/processed_data/CLINVAR_VARIANT_GENE_DISEASE_PHENOTYPE_EDGES.txt
-variant-phenotype, https://storage.googleapis.com/pheknowlator/release_v2.0.0/current_build/data/processed_data/CLINVAR_VARIANT_GENE_DISEASE_PHENOTYPE_EDGES.txt
\ No newline at end of file
+chemical-disease, https://storage.googleapis.com/pheknowlator/current_build/data/original_data/CTD_chemicals_diseases.tsv
+chemical-gene, https://storage.googleapis.com/pheknowlator/current_build/data/original_data/CTD_chem_gene_ixns.tsv
+chemical-gobp, https://storage.googleapis.com/pheknowlator/current_build/data/original_data/CTD_chem_go_enriched.tsv
+chemical-gocc, https://storage.googleapis.com/pheknowlator/current_build/data/original_data/CTD_chem_go_enriched.tsv
+chemical-gomf, https://storage.googleapis.com/pheknowlator/current_build/data/original_data/CTD_chem_go_enriched.tsv
+chemical-pathway, https://storage.googleapis.com/pheknowlator/current_build/data/original_data/ChEBI2Reactome_All_Levels.txt
+chemical-phenotype, https://storage.googleapis.com/pheknowlator/current_build/data/original_data/CTD_chemicals_diseases.tsv
+chemical-protein, https://storage.googleapis.com/pheknowlator/current_build/data/original_data/CTD_chem_gene_ixns.tsv
+chemical-rna, https://storage.googleapis.com/pheknowlator/current_build/data/original_data/CTD_chem_gene_ixns.tsv
+disease-phenotype, https://storage.googleapis.com/pheknowlator/current_build/data/original_data/phenotype_annotation.tab
+gene-disease, https://storage.googleapis.com/pheknowlator/current_build/data/original_data/curated_gene_disease_associations.tsv
+gene-gene, https://storage.googleapis.com/pheknowlator/current_build/data/original_data/COMBINED.DEFAULT_NETWORKS.BP_COMBINING.txt
+gene-pathway, https://storage.googleapis.com/pheknowlator/current_build/data/original_data/CTD_genes_pathways.tsv
+gene-phenotype, https://storage.googleapis.com/pheknowlator/current_build/data/original_data/curated_gene_disease_associations.tsv
+gene-protein, https://storage.googleapis.com/pheknowlator/current_build/data/processed_data/ENTREZ_GENE_PRO_ONTOLOGY_MAP.txt
+gene-rna, https://storage.googleapis.com/pheknowlator/current_build/data/processed_data/ENTREZ_GENE_ENSEMBL_TRANSCRIPT_MAP.txt
+gobp-pathway, https://storage.googleapis.com/pheknowlator/current_build/data/original_data/gene_association.reactome
+pathway-gocc, https://storage.googleapis.com/pheknowlator/current_build/data/original_data/gene_association.reactome
+pathway-gomf, https://storage.googleapis.com/pheknowlator/current_build/data/original_data/gene_association.reactome
+protein-anatomy, https://storage.googleapis.com/pheknowlator/current_build/data/processed_data/HPA_GTEX_RNA_GENE_PROTEIN_EDGES.txt
+protein-catalyst, https://storage.googleapis.com/pheknowlator/current_build/data/processed_data/UNIPROT_PROTEIN_CATALYST.txt
+protein-cell, https://storage.googleapis.com/pheknowlator/current_build/data/processed_data/HPA_GTEX_RNA_GENE_PROTEIN_EDGES.txt
+protein-cofactor, https://storage.googleapis.com/pheknowlator/current_build/data/processed_data/UNIPROT_PROTEIN_COFACTOR.txt
+protein-gobp, https://storage.googleapis.com/pheknowlator/current_build/data/original_data/goa_human.gaf
+protein-gocc, https://storage.googleapis.com/pheknowlator/current_build/data/original_data/goa_human.gaf
+protein-gomf, https://storage.googleapis.com/pheknowlator/current_build/data/original_data/goa_human.gaf
+protein-pathway, https://storage.googleapis.com/pheknowlator/current_build/data/original_data/UniProt2Reactome_All_Levels.txt
+protein-protein, https://storage.googleapis.com/pheknowlator/current_build/data/original_data/9606.protein.links.v11.0.txt
+rna-anatomy, https://storage.googleapis.com/pheknowlator/current_build/data/processed_data/HPA_GTEX_RNA_GENE_PROTEIN_EDGES.txt
+rna-cell, https://storage.googleapis.com/pheknowlator/current_build/data/processed_data/HPA_GTEX_RNA_GENE_PROTEIN_EDGES.txt
+rna-protein, https://storage.googleapis.com/pheknowlator/current_build/data/processed_data/ENSEMBL_TRANSCRIPT_PROTEIN_ONTOLOGY_MAP.txt
+variant-disease, https://storage.googleapis.com/pheknowlator/current_build/data/processed_data/CLINVAR_VARIANT_GENE_DISEASE_PHENOTYPE_EDGES.txt
+variant-gene, https://storage.googleapis.com/pheknowlator/current_build/data/processed_data/CLINVAR_VARIANT_GENE_DISEASE_PHENOTYPE_EDGES.txt
+variant-phenotype, https://storage.googleapis.com/pheknowlator/current_build/data/processed_data/CLINVAR_VARIANT_GENE_DISEASE_PHENOTYPE_EDGES.txt
\ No newline at end of file
diff --git a/resources/ontology_source_list.txt b/resources/ontology_source_list.txt
index 7e8ede9bc..d901f1eb4 100644
--- a/resources/ontology_source_list.txt
+++ b/resources/ontology_source_list.txt
@@ -1,11 +1,11 @@
-phenotype, http://purl.obolibrary.org/obo/hp.owl
-go, http://purl.obolibrary.org/obo/go.owl
-disease, http://purl.obolibrary.org/obo/mondo.owl
-vaccine, http://purl.obolibrary.org/obo/vo.owl
-chemical, http://purl.obolibrary.org/obo/chebi.owl
-anatomy, http://purl.obolibrary.org/obo/uberon/ext.owl
-cell, http://purl.obolibrary.org/obo/clo.owl
-protein, http://purl.obolibrary.org/obo/pr.owl
-genomic, http://purl.obolibrary.org/obo/so.owl
-pathway, http://purl.obolibrary.org/obo/pw.owl
-relation, http://purl.obolibrary.org/obo/ro.owl
\ No newline at end of file
+phenotype, https://storage.googleapis.com/pheknowlator/current_build/data/processed_data/hp_with_imports.owl
+go, https://storage.googleapis.com/pheknowlator/current_build/data/processed_data/go_with_imports.owl
+disease, https://storage.googleapis.com/pheknowlator/current_build/data/processed_data/mondo_with_imports.owl
+vaccine, https://storage.googleapis.com/pheknowlator/current_build/data/processed_data/vo_with_imports.owl
+chemical, https://storage.googleapis.com/pheknowlator/current_build/data/processed_data/chebi_with_imports.owl
+anatomy, https://storage.googleapis.com/pheknowlator/current_build/data/processed_data/ext_with_imports.owl
+cell, https://storage.googleapis.com/pheknowlator/current_build/data/processed_data/clo_with_imports.owl
+protein, https://storage.googleapis.com/pheknowlator/current_build/data/processed_data/pr_with_imports.owl
+genomic, https://storage.googleapis.com/pheknowlator/current_build/data/processed_data/so_with_imports.owl
+pathway, https://storage.googleapis.com/pheknowlator/current_build/data/processed_data/pw_with_imports.owl
+relation, https://storage.googleapis.com/pheknowlator/current_build/data/processed_data/ro_with_imports.owl
\ No newline at end of file
diff --git a/setup.py b/setup.py
index f97871d4a..9adbd2a3e 100644
--- a/setup.py
+++ b/setup.py
@@ -58,10 +58,10 @@ def find_version(*file_paths):
     include_package_data=True,
     classifiers=[
         'Development Status :: 3 - Alpha',
-        'License :: OSI Approved :: Apache 2.0 License',
+        'License :: OSI Approved :: Apache Software License',
         'Programming Language :: Python :: 3'
     ],
-    packages=find_packages(exclude=['contrib', 'docs', 'tests*']),
+    packages=find_packages(exclude=['contrib', 'docs', 'tests*', 'builds']),
     tests_require=test_deps,
 
     entry_points={
@@ -70,11 +70,13 @@ def find_version(*file_paths):
 
     # Add here the package dependencies
     install_requires=['argparse>=1.4.0',
+                      'click==7.1.2',
                       'Cython==0.29.14',
                       'networkx==2.4',
                       'numpy==1.18.1',
                       'openpyxl==3.0.3',
                       'pandas==1.0.5',
+                      'python-json-logger==2.0.1',
                       'rdflib==4.2.2',
                       'reactome2py>=0.0.8',
                       'requests==2.22.0',


Test patch:
diff --git a/tests/builds/logging.ini b/tests/builds/logging.ini
new file mode 100755
index 000000000..bb8e684de
--- /dev/null
+++ b/tests/builds/logging.ini
@@ -0,0 +1,32 @@
+;source: https://www.datadoghq.com/blog/python-logging-best-practices/
+[loggers]
+keys=root
+
+[handlers]
+keys=fileHandler, consoleHandler
+
+[formatters]
+keys=simpleFormatter,json
+
+[logger_root]
+level=INFO
+handlers=fileHandler
+
+[handler_consoleHandler]
+class=StreamHandler
+level=DEBUG
+formatter=simpleFormatter
+args=(sys.stdout,)
+
+[handler_fileHandler]
+class=FileHandler
+level=INFO
+formatter=json
+args=('%(log_file)s',)
+
+[formatter_json]
+class=pythonjsonlogger.jsonlogger.JsonFormatter
+format=%(asctime)s : %(levelname)s : %(name)s : %(module)s : %(funcName)s : %(lineno)d : %(message)s
+
+[formatter_simpleFormatter]
+format=%(asctime)s : %(levelname)s : %(name)s : %(module)s : %(funcName)s : %(message)s
\ No newline at end of file
diff --git a/tests/test_construction_approaches.py b/tests/test_construction_approaches.py
index 7aaf99d5a..39f4a6bc3 100644
--- a/tests/test_construction_approaches.py
+++ b/tests/test_construction_approaches.py
@@ -1,5 +1,6 @@
 import glob
 import json
+import logging
 import os
 import os.path
 import pandas
@@ -30,6 +31,11 @@ def setUp(self):
         os.mkdir(self.dir_loc_resources)
         os.mkdir(self.dir_loc_resources + '/construction_approach')
 
+        # handle logging
+        self.logs = os.path.abspath(current_directory + '/builds/logs')
+        logging.disable(logging.CRITICAL)
+        if len(glob.glob(self.logs + '/*.log')) > 0: os.remove(glob.glob(self.logs + '/*.log')[0])
+
         # copy data
         # empty master edges
         shutil.copyfile(self.dir_loc + '/Master_Edge_List_Dict_empty.json',
diff --git a/tests/test_downloads_linked_data.py b/tests/test_downloads_linked_data.py
index 8ea47fb1b..9f8007c10 100644
--- a/tests/test_downloads_linked_data.py
+++ b/tests/test_downloads_linked_data.py
@@ -3,6 +3,7 @@
 
 import os.path
 import glob
+import logging
 
 from unittest import TestCase
 
@@ -19,6 +20,11 @@ def setUp(self):
         self.dir_loc = os.path.abspath(dir_loc)
         self.data = LinkedData(self.dir_loc + '/edge_source_list.txt', self.dir_loc + '/resource_info.txt')
 
+        # handle logging
+        self.logs = os.path.abspath(current_directory + '/builds/logs')
+        logging.disable(logging.CRITICAL)
+        if len(glob.glob(self.logs + '/*.log')) > 0: os.remove(glob.glob(self.logs + '/*.log')[0])
+
         return None
 
     def test_initialization_data_path(self):
diff --git a/tests/test_downloads_ontology_data.py b/tests/test_downloads_ontology_data.py
index 02f4edd1b..6fce5bf1e 100644
--- a/tests/test_downloads_ontology_data.py
+++ b/tests/test_downloads_ontology_data.py
@@ -3,6 +3,7 @@
 
 import os.path
 import glob
+import logging
 
 from unittest import TestCase
 
@@ -13,7 +14,6 @@ class TestOntData(TestCase):
     """Class to test functions used when downloading ontology data sources."""
 
     def setUp(self):
-
         # initialize OntData instance
         current_directory = os.path.dirname(__file__)
         dir_loc = os.path.join(current_directory, 'data')
@@ -24,6 +24,11 @@ def setUp(self):
         dir_loc2 = os.path.join(current_directory, 'utils/owltools')
         self.owltools_location = os.path.abspath(dir_loc2)
 
+        # handle logging
+        self.logs = os.path.abspath(current_directory + '/builds/logs')
+        logging.disable(logging.CRITICAL)
+        if len(glob.glob(self.logs + '/*.log')) > 0: os.remove(glob.glob(self.logs + '/*.log')[0])
+
         return None
 
     def test_initialization_data_path(self):
diff --git a/tests/test_edge_list.py b/tests/test_edge_list.py
index d6f02f2d7..583116ca5 100644
--- a/tests/test_edge_list.py
+++ b/tests/test_edge_list.py
@@ -1,3 +1,5 @@
+import glob
+import logging
 import os.path
 import pandas
 import re
@@ -12,12 +14,17 @@ class TestCreatesEdgeList(unittest.TestCase):
     """Class to test functions used when processing edge data sources."""
 
     def setUp(self):
-
+        logging.disable(logging.CRITICAL)
         # initialize file location
         current_directory = os.path.dirname(__file__)
         dir_loc = os.path.join(current_directory, 'data')
         self.dir_loc = os.path.abspath(dir_loc)
 
+        # handle logging
+        self.logs = os.path.abspath(current_directory + '/builds/logs')
+        logging.disable(logging.CRITICAL)
+        if len(glob.glob(self.logs + '/*.log')) > 0: os.remove(glob.glob(self.logs + '/*.log')[0])
+
         # create dictionary to store data
         file_loc1 = self.dir_loc + '/edge_data/chemical-disease_CTD_chemicals_diseases.tsv'
         file_loc2 = self.dir_loc + '/edge_data/gene-disease_curated_gene_disease_associations.tsv'
diff --git a/tests/test_knowledge_graph.py b/tests/test_knowledge_graph.py
index 5500cd17f..e49853287 100644
--- a/tests/test_knowledge_graph.py
+++ b/tests/test_knowledge_graph.py
@@ -1,5 +1,6 @@
 import glob
 import json
+import logging
 import os
 import os.path
 import pandas
@@ -37,6 +38,11 @@ def setUp(self):
         os.mkdir(self.dir_loc_resources + '/ontologies')
         os.mkdir(self.dir_loc_resources + '/construction_approach')
 
+        # handle logging
+        self.logs = os.path.abspath(current_directory + '/builds/logs')
+        logging.disable(logging.CRITICAL)
+        if len(glob.glob(self.logs + '/*.log')) > 0: os.remove(glob.glob(self.logs + '/*.log')[0])
+
         # copy needed data data
         # node metadata
         shutil.copyfile(self.dir_loc + '/node_data/node_metadata_dict.pkl',
@@ -486,6 +492,21 @@ def test_checks_for_inverse_relations(self):
 
         return None
 
+    @patch('builtins.print')
+    def test_derives_graph_statistics(self, mock_print):
+        """Tests the derives_graph_statistics method."""
+
+        # generate stats from existing ontology
+        graph = Graph().parse(self.dir_loc + '/ontologies/so_with_imports.owl')
+        res_str = 'Graph Stats: 42237 triples, 42181 nodes, 2793 classes, 0 individuals, 50 object properties, ' \
+            '39 annotation properties'
+
+        # test method
+        self.kg_subclass.derives_graph_statistics(graph)
+        mock_print.assert_called_with(res_str)
+
+        return None
+
     def test_creates_knowledge_graph_edges_not_adding_metadata_to_kg(self):
         """Tests the creates_knowledge_graph_edges method without adding node metadata to the KG."""
 
diff --git a/tests/test_knowledge_graph_full.py b/tests/test_knowledge_graph_full.py
index 19486b3ef..28dff9ebd 100644
--- a/tests/test_knowledge_graph_full.py
+++ b/tests/test_knowledge_graph_full.py
@@ -1,5 +1,6 @@
 import glob
 import json
+import logging
 import os
 import os.path
 import pandas
@@ -30,6 +31,11 @@ def setUp(self):
         os.mkdir(self.dir_loc_resources + '/construction_approach')
         os.mkdir(self.dir_loc_resources + '/owl_decoding')
 
+        # handle logging
+        self.logs = os.path.abspath(current_directory + '/builds/logs')
+        logging.disable(logging.CRITICAL)
+        if len(glob.glob(self.logs + '/*.log')) > 0: os.remove(glob.glob(self.logs + '/*.log')[0])
+
         # copy needed data data
         # node metadata
         shutil.copyfile(self.dir_loc + '/node_data/node_metadata_dict.pkl',
diff --git a/tests/test_knowledge_graph_partial.py b/tests/test_knowledge_graph_partial.py
index 035a68b52..fd2dede1f 100644
--- a/tests/test_knowledge_graph_partial.py
+++ b/tests/test_knowledge_graph_partial.py
@@ -1,5 +1,6 @@
 import glob
 import json
+import logging
 import os
 import os.path
 import pandas
@@ -30,6 +31,11 @@ def setUp(self):
         os.mkdir(self.dir_loc_resources + '/ontologies')
         os.mkdir(self.dir_loc_resources + '/construction_approach')
 
+        # handle logging
+        self.logs = os.path.abspath(current_directory + '/builds/logs')
+        logging.disable(logging.CRITICAL)
+        if len(glob.glob(self.logs + '/*.log')) > 0: os.remove(glob.glob(self.logs + '/*.log')[0])
+
         # copy needed data data
         # node metadata
         shutil.copyfile(self.dir_loc + '/node_data/node_metadata_dict.pkl',
diff --git a/tests/test_knowledge_graph_post_closure.py b/tests/test_knowledge_graph_post_closure.py
index fc9b96889..f2a132235 100644
--- a/tests/test_knowledge_graph_post_closure.py
+++ b/tests/test_knowledge_graph_post_closure.py
@@ -1,5 +1,6 @@
 import glob
 import json
+import logging
 import os
 import os.path
 import pandas
@@ -33,6 +34,11 @@ def setUp(self):
         os.mkdir(self.dir_loc_resources + '/construction_approach')
         os.mkdir(self.dir_loc_resources + '/owl_decoding')
 
+        # handle logging
+        self.logs = os.path.abspath(current_directory + '/builds/logs')
+        logging.disable(logging.CRITICAL)
+        if len(glob.glob(self.logs + '/*.log')) > 0: os.remove(glob.glob(self.logs + '/*.log')[0])
+
         # copy needed data data
         # node metadata
         shutil.copyfile(self.dir_loc + '/node_data/node_metadata_dict.pkl',
diff --git a/tests/test_metadata.py b/tests/test_metadata.py
index 7787d98b8..63a0421c3 100644
--- a/tests/test_metadata.py
+++ b/tests/test_metadata.py
@@ -1,4 +1,5 @@
 import glob
+import logging
 import os
 import os.path
 import pickle
@@ -19,6 +20,11 @@ def setUp(self):
         dir_loc = os.path.join(current_directory, 'data')
         self.dir_loc = os.path.abspath(dir_loc)
 
+        # handle logging
+        self.logs = os.path.abspath(current_directory + '/builds/logs')
+        logging.disable(logging.CRITICAL)
+        if len(glob.glob(self.logs + '/*.log')) > 0: os.remove(glob.glob(self.logs + '/*.log')[0])
+
         # pointer to owltools
         dir_loc2 = os.path.join(current_directory, 'utils/owltools')
         self.owltools_location = os.path.abspath(dir_loc2)
@@ -228,7 +234,7 @@ def test_extracts_class_metadata(self):
         self.assertIn('Synonym', self.metadata.node_dict['nodes'][node_key])
         # relations
         relation_key = 'http://purl.obolibrary.org/obo/so#genome_of'
-        self.assertTrue(len(self.metadata.node_dict['relations']) == 2511)
+        self.assertTrue(len(self.metadata.node_dict['relations']) == 72)
         self.assertIn('Label', self.metadata.node_dict['relations'][relation_key])
         self.assertIn('Description', self.metadata.node_dict['relations'][relation_key])
         self.assertIn('Synonym', self.metadata.node_dict['relations'][relation_key])
diff --git a/tests/test_owlnets.py b/tests/test_owlnets.py
index defa927ca..2145f25a9 100644
--- a/tests/test_owlnets.py
+++ b/tests/test_owlnets.py
@@ -1,4 +1,5 @@
 import glob
+import logging
 import networkx
 import os
 import shutil
@@ -27,6 +28,11 @@ def setUp(self):
         os.mkdir(self.dir_loc_resources + '/knowledge_graphs')
         os.mkdir(self.dir_loc_resources + '/owl_decoding')
 
+        # handle logging
+        self.logs = os.path.abspath(current_directory + '/builds/logs')
+        logging.disable(logging.CRITICAL)
+        if len(glob.glob(self.logs + '/*.log')) > 0: os.remove(glob.glob(self.logs + '/*.log')[0])
+
         # copy data
         # ontology data
         shutil.copyfile(self.dir_loc + '/ontologies/so_with_imports.owl',