Repository: Sage-Bionetworks/schematic (language: python)

Issue:
add functionality to set requirements based per component
Allow users to be able to set attribute requirements per component, using a combination of component based rule pairs in conjunction with the new "rule" `required`.

Like such:
`#Patient unique warning^^#Biospecimen unique required error`

This rule indicates that the attribute is required for the Biospecimen manifest and not the Patient Manifest. To work properly the Required field should also be set to FALSE.

Extra tests were added to cover a variety of test cases.

fixes [FDS-1626]

[FDS-1626]: https://sagebionetworks.jira.com/browse/FDS-1626?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ


Discussion / hints:

Fix patch:
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 000000000..623446ced
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,13 @@
+repos:
+  # Using this mirror lets us use mypyc-compiled black, which is about 2x faster
+  # This is recommended by psf/black: https://github.com/psf/black/blob/main/.pre-commit-hooks.yaml
+  - repo: https://github.com/psf/black-pre-commit-mirror
+    rev: 23.7.0
+    hooks:
+      - id: black
+        # It is recommended to specify the latest version of Python
+        # supported by your project here, or alternatively use
+        # pre-commit's default_language_version, see
+        # https://pre-commit.com/#top_level-default_language_version
+        language_version: python3.10
+        files: schematic/
\ No newline at end of file
diff --git a/README.md b/README.md
index dd7263940..228e0e779 100644
--- a/README.md
+++ b/README.md
@@ -20,9 +20,10 @@ SCHEMATIC is an acronym for _Schema Engine for Manifest Ingress and Curation_. T
 
 # Installation
 ## Installation Requirements
-* Python version 3.9.0≤x<3.11.0 
+* Python version 3.9.0≤x<3.11.0
+* You need to be a registered and certified user on [`synapse.org`](https://www.synapse.org/)
 
-Note: You need to be a registered and certified user on [`synapse.org`](https://www.synapse.org/), and also have the right permissions to download the Google credentials files from Synapse.
+Note: Our credential policy for Google credentials in order to create Google sheet files from Schematic, see tutorial ['HERE'](https://scribehow.com/shared/Get_Credentials_for_Google_Drive_and_Google_Sheets_APIs_to_use_with_schematicpy__yqfcJz_rQVeyTcg0KQCINA). If you plan to use `config.yml`, please ensure that the path of `schematic_service_account_creds.json` is indicated there (see `google_sheets > service_account_creds` section)
 
 
 ## Installation guide for data curator app
@@ -169,13 +170,10 @@ On the CLI in your virtual environment, run the following command:
 ```
 synapse login -u <synapse username> -p <synapse password> --rememberMe
 ```
-Please make sure that you run the command before running `schematic init` below
 
 7. Obtain Google credential Files
-To obtain  ``schematic_service_account_creds.json``, please run: 
-```
-schematic init --config ~/path/to/config.yml
-```
+Running `schematic init` is no longer supported due to security concerns. To obtain  `schematic_service_account_creds.json`, please follow the instructions [here](https://scribehow.com/shared/Enable_Google_Drive_and_Google_Sheets_APIs_for_project__yqfcJz_rQVeyTcg0KQCINA). 
+
 > As v22.12.1 version of schematic, using `token` mode of authentication (in other words, using `token.pickle` and `credentials.json`) is no longer supported due to Google's decision to move away from using OAuth out-of-band (OOB) flow. Click [here](https://developers.google.com/identity/protocols/oauth2/resources/oob-migration) to learn more. 
 
 *Notes*: Use the ``schematic_service_account_creds.json`` file for the service
@@ -189,6 +187,13 @@ Most Google sheet functionality could be authenticated with service account. How
 requires token-based authentication. As browser support that requires the token-based authentication diminishes, we are hoping to deprecate
 token-based authentication and keep only service account authentication in the future. 
 
+8. Set up pre-commit hooks
+
+This repository is configured to utilize pre-commit hooks as part of the development process. To enable these hooks, please run the following command and look for the following success message:
+```
+$ pre-commit install
+pre-commit installed at .git/hooks/pre-commit
+```
 
 ### Development process instruction
 
diff --git a/poetry.lock b/poetry.lock
index 5673df9a4..7314c46cb 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -35,13 +35,13 @@ dev = ["black", "docutils", "flake8", "ipython", "m2r", "mistune (<2.0.0)", "pyt
 
 [[package]]
 name = "anyio"
-version = "4.2.0"
+version = "4.3.0"
 description = "High level compatibility layer for multiple asynchronous event loop implementations"
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "anyio-4.2.0-py3-none-any.whl", hash = "sha256:745843b39e829e108e518c489b31dc757de7d2131d53fac32bd8df268227bfee"},
-    {file = "anyio-4.2.0.tar.gz", hash = "sha256:e1875bb4b4e2de1669f4bc7869b6d3f54231cdced71605e6e64c9be77e3be50f"},
+    {file = "anyio-4.3.0-py3-none-any.whl", hash = "sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8"},
+    {file = "anyio-4.3.0.tar.gz", hash = "sha256:f75253795a87df48568485fd18cdd2a3fa5c4f7c5be8e5e36637733fce06fed6"},
 ]
 
 [package.dependencies]
@@ -321,13 +321,13 @@ css = ["tinycss2 (>=1.1.0,<1.3)"]
 
 [[package]]
 name = "cachetools"
-version = "5.3.2"
+version = "5.3.3"
 description = "Extensible memoizing collections and decorators"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "cachetools-5.3.2-py3-none-any.whl", hash = "sha256:861f35a13a451f94e301ce2bec7cac63e881232ccce7ed67fab9b5df4d3beaa1"},
-    {file = "cachetools-5.3.2.tar.gz", hash = "sha256:086ee420196f7b2ab9ca2db2520aca326318b68fe5ba8bc4d49cca91add450f2"},
+    {file = "cachetools-5.3.3-py3-none-any.whl", hash = "sha256:0abad1021d3f8325b2fc1d2e9c8b9c9d57b04c3932657a72465447332c24d945"},
+    {file = "cachetools-5.3.3.tar.gz", hash = "sha256:ba29e2dfa0b8b556606f097407ed1aa62080ee108ab0dc5ec9d6a723a007d105"},
 ]
 
 [[package]]
@@ -405,6 +405,17 @@ files = [
 [package.dependencies]
 pycparser = "*"
 
+[[package]]
+name = "cfgv"
+version = "3.4.0"
+description = "Validate configuration and produce human readable error messages."
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"},
+    {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"},
+]
+
 [[package]]
 name = "charset-normalizer"
 version = "3.3.2"
@@ -577,18 +588,18 @@ test = ["pytest"]
 
 [[package]]
 name = "connexion"
-version = "2.14.2"
+version = "2.14.1"
 description = "Connexion - API first applications with OpenAPI/Swagger and Flask"
 optional = true
 python-versions = ">=3.6"
 files = [
-    {file = "connexion-2.14.2-py2.py3-none-any.whl", hash = "sha256:a73b96a0e07b16979a42cde7c7e26afe8548099e352cf350f80c57185e0e0b36"},
-    {file = "connexion-2.14.2.tar.gz", hash = "sha256:dbc06f52ebeebcf045c9904d570f24377e8bbd5a6521caef15a06f634cf85646"},
+    {file = "connexion-2.14.1-py2.py3-none-any.whl", hash = "sha256:f343717241b4c4802a694c38fee66fb1693c897fe4ea5a957fa9b3b07caf6394"},
+    {file = "connexion-2.14.1.tar.gz", hash = "sha256:99aa5781e70a7b94f8ffae8cf89f309d49cdb811bbd65a8e2f2546f3b19a01e6"},
 ]
 
 [package.dependencies]
 clickclick = ">=1.2,<21"
-flask = ">=1.0.4,<2.3"
+flask = ">=1.0.4,<3"
 inflection = ">=0.3.1,<0.6"
 itsdangerous = ">=0.24"
 jsonschema = ">=2.5.1,<5"
@@ -596,74 +607,74 @@ packaging = ">=20"
 PyYAML = ">=5.1,<7"
 requests = ">=2.9.1,<3"
 swagger-ui-bundle = {version = ">=0.0.2,<0.1", optional = true, markers = "extra == \"swagger-ui\""}
-werkzeug = ">=1.0,<2.3"
+werkzeug = ">=1.0,<3"
 
 [package.extras]
 aiohttp = ["MarkupSafe (>=0.23)", "aiohttp (>=2.3.10,<4)", "aiohttp-jinja2 (>=0.14.0,<2)"]
 docs = ["sphinx-autoapi (==1.8.1)"]
-flask = ["flask (>=1.0.4,<2.3)", "itsdangerous (>=0.24)"]
+flask = ["flask (>=1.0.4,<3)", "itsdangerous (>=0.24)"]
 swagger-ui = ["swagger-ui-bundle (>=0.0.2,<0.1)"]
-tests = ["MarkupSafe (>=0.23)", "aiohttp (>=2.3.10,<4)", "aiohttp-jinja2 (>=0.14.0,<2)", "aiohttp-remotes", "decorator (>=5,<6)", "flask (>=1.0.4,<2.3)", "itsdangerous (>=0.24)", "pytest (>=6,<7)", "pytest-aiohttp", "pytest-cov (>=2,<3)", "swagger-ui-bundle (>=0.0.2,<0.1)", "testfixtures (>=6,<7)"]
+tests = ["MarkupSafe (>=0.23)", "aiohttp (>=2.3.10,<4)", "aiohttp-jinja2 (>=0.14.0,<2)", "aiohttp-remotes", "decorator (>=5,<6)", "flask (>=1.0.4,<3)", "itsdangerous (>=0.24)", "pytest (>=6,<7)", "pytest-aiohttp", "pytest-cov (>=2,<3)", "swagger-ui-bundle (>=0.0.2,<0.1)", "testfixtures (>=6,<7)"]
 
 [[package]]
 name = "coverage"
-version = "7.4.1"
+version = "7.4.3"
 description = "Code coverage measurement for Python"
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "coverage-7.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:077d366e724f24fc02dbfe9d946534357fda71af9764ff99d73c3c596001bbd7"},
-    {file = "coverage-7.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0193657651f5399d433c92f8ae264aff31fc1d066deee4b831549526433f3f61"},
-    {file = "coverage-7.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d17bbc946f52ca67adf72a5ee783cd7cd3477f8f8796f59b4974a9b59cacc9ee"},
-    {file = "coverage-7.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a3277f5fa7483c927fe3a7b017b39351610265308f5267ac6d4c2b64cc1d8d25"},
-    {file = "coverage-7.4.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6dceb61d40cbfcf45f51e59933c784a50846dc03211054bd76b421a713dcdf19"},
-    {file = "coverage-7.4.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:6008adeca04a445ea6ef31b2cbaf1d01d02986047606f7da266629afee982630"},
-    {file = "coverage-7.4.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:c61f66d93d712f6e03369b6a7769233bfda880b12f417eefdd4f16d1deb2fc4c"},
-    {file = "coverage-7.4.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b9bb62fac84d5f2ff523304e59e5c439955fb3b7f44e3d7b2085184db74d733b"},
-    {file = "coverage-7.4.1-cp310-cp310-win32.whl", hash = "sha256:f86f368e1c7ce897bf2457b9eb61169a44e2ef797099fb5728482b8d69f3f016"},
-    {file = "coverage-7.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:869b5046d41abfea3e381dd143407b0d29b8282a904a19cb908fa24d090cc018"},
-    {file = "coverage-7.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b8ffb498a83d7e0305968289441914154fb0ef5d8b3157df02a90c6695978295"},
-    {file = "coverage-7.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3cacfaefe6089d477264001f90f55b7881ba615953414999c46cc9713ff93c8c"},
-    {file = "coverage-7.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d6850e6e36e332d5511a48a251790ddc545e16e8beaf046c03985c69ccb2676"},
-    {file = "coverage-7.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18e961aa13b6d47f758cc5879383d27b5b3f3dcd9ce8cdbfdc2571fe86feb4dd"},
-    {file = "coverage-7.4.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dfd1e1b9f0898817babf840b77ce9fe655ecbe8b1b327983df485b30df8cc011"},
-    {file = "coverage-7.4.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:6b00e21f86598b6330f0019b40fb397e705135040dbedc2ca9a93c7441178e74"},
-    {file = "coverage-7.4.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:536d609c6963c50055bab766d9951b6c394759190d03311f3e9fcf194ca909e1"},
-    {file = "coverage-7.4.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7ac8f8eb153724f84885a1374999b7e45734bf93a87d8df1e7ce2146860edef6"},
-    {file = "coverage-7.4.1-cp311-cp311-win32.whl", hash = "sha256:f3771b23bb3675a06f5d885c3630b1d01ea6cac9e84a01aaf5508706dba546c5"},
-    {file = "coverage-7.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:9d2f9d4cc2a53b38cabc2d6d80f7f9b7e3da26b2f53d48f05876fef7956b6968"},
-    {file = "coverage-7.4.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f68ef3660677e6624c8cace943e4765545f8191313a07288a53d3da188bd8581"},
-    {file = "coverage-7.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:23b27b8a698e749b61809fb637eb98ebf0e505710ec46a8aa6f1be7dc0dc43a6"},
-    {file = "coverage-7.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e3424c554391dc9ef4a92ad28665756566a28fecf47308f91841f6c49288e66"},
-    {file = "coverage-7.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e0860a348bf7004c812c8368d1fc7f77fe8e4c095d661a579196a9533778e156"},
-    {file = "coverage-7.4.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe558371c1bdf3b8fa03e097c523fb9645b8730399c14fe7721ee9c9e2a545d3"},
-    {file = "coverage-7.4.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3468cc8720402af37b6c6e7e2a9cdb9f6c16c728638a2ebc768ba1ef6f26c3a1"},
-    {file = "coverage-7.4.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:02f2edb575d62172aa28fe00efe821ae31f25dc3d589055b3fb64d51e52e4ab1"},
-    {file = "coverage-7.4.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ca6e61dc52f601d1d224526360cdeab0d0712ec104a2ce6cc5ccef6ed9a233bc"},
-    {file = "coverage-7.4.1-cp312-cp312-win32.whl", hash = "sha256:ca7b26a5e456a843b9b6683eada193fc1f65c761b3a473941efe5a291f604c74"},
-    {file = "coverage-7.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:85ccc5fa54c2ed64bd91ed3b4a627b9cce04646a659512a051fa82a92c04a448"},
-    {file = "coverage-7.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8bdb0285a0202888d19ec6b6d23d5990410decb932b709f2b0dfe216d031d218"},
-    {file = "coverage-7.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:918440dea04521f499721c039863ef95433314b1db00ff826a02580c1f503e45"},
-    {file = "coverage-7.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:379d4c7abad5afbe9d88cc31ea8ca262296480a86af945b08214eb1a556a3e4d"},
-    {file = "coverage-7.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b094116f0b6155e36a304ff912f89bbb5067157aff5f94060ff20bbabdc8da06"},
-    {file = "coverage-7.4.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2f5968608b1fe2a1d00d01ad1017ee27efd99b3437e08b83ded9b7af3f6f766"},
-    {file = "coverage-7.4.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:10e88e7f41e6197ea0429ae18f21ff521d4f4490aa33048f6c6f94c6045a6a75"},
-    {file = "coverage-7.4.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a4a3907011d39dbc3e37bdc5df0a8c93853c369039b59efa33a7b6669de04c60"},
-    {file = "coverage-7.4.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6d224f0c4c9c98290a6990259073f496fcec1b5cc613eecbd22786d398ded3ad"},
-    {file = "coverage-7.4.1-cp38-cp38-win32.whl", hash = "sha256:23f5881362dcb0e1a92b84b3c2809bdc90db892332daab81ad8f642d8ed55042"},
-    {file = "coverage-7.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:a07f61fc452c43cd5328b392e52555f7d1952400a1ad09086c4a8addccbd138d"},
-    {file = "coverage-7.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8e738a492b6221f8dcf281b67129510835461132b03024830ac0e554311a5c54"},
-    {file = "coverage-7.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:46342fed0fff72efcda77040b14728049200cbba1279e0bf1188f1f2078c1d70"},
-    {file = "coverage-7.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9641e21670c68c7e57d2053ddf6c443e4f0a6e18e547e86af3fad0795414a628"},
-    {file = "coverage-7.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aeb2c2688ed93b027eb0d26aa188ada34acb22dceea256d76390eea135083950"},
-    {file = "coverage-7.4.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d12c923757de24e4e2110cf8832d83a886a4cf215c6e61ed506006872b43a6d1"},
-    {file = "coverage-7.4.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0491275c3b9971cdbd28a4595c2cb5838f08036bca31765bad5e17edf900b2c7"},
-    {file = "coverage-7.4.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:8dfc5e195bbef80aabd81596ef52a1277ee7143fe419efc3c4d8ba2754671756"},
-    {file = "coverage-7.4.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:1a78b656a4d12b0490ca72651fe4d9f5e07e3c6461063a9b6265ee45eb2bdd35"},
-    {file = "coverage-7.4.1-cp39-cp39-win32.whl", hash = "sha256:f90515974b39f4dea2f27c0959688621b46d96d5a626cf9c53dbc653a895c05c"},
-    {file = "coverage-7.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:64e723ca82a84053dd7bfcc986bdb34af8d9da83c521c19d6b472bc6880e191a"},
-    {file = "coverage-7.4.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:32a8d985462e37cfdab611a6f95b09d7c091d07668fdc26e47a725ee575fe166"},
-    {file = "coverage-7.4.1.tar.gz", hash = "sha256:1ed4b95480952b1a26d863e546fa5094564aa0065e1e5f0d4d0041f293251d04"},
+    {file = "coverage-7.4.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8580b827d4746d47294c0e0b92854c85a92c2227927433998f0d3320ae8a71b6"},
+    {file = "coverage-7.4.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:718187eeb9849fc6cc23e0d9b092bc2348821c5e1a901c9f8975df0bc785bfd4"},
+    {file = "coverage-7.4.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:767b35c3a246bcb55b8044fd3a43b8cd553dd1f9f2c1eeb87a302b1f8daa0524"},
+    {file = "coverage-7.4.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae7f19afe0cce50039e2c782bff379c7e347cba335429678450b8fe81c4ef96d"},
+    {file = "coverage-7.4.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba3a8aaed13770e970b3df46980cb068d1c24af1a1968b7818b69af8c4347efb"},
+    {file = "coverage-7.4.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ee866acc0861caebb4f2ab79f0b94dbfbdbfadc19f82e6e9c93930f74e11d7a0"},
+    {file = "coverage-7.4.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:506edb1dd49e13a2d4cac6a5173317b82a23c9d6e8df63efb4f0380de0fbccbc"},
+    {file = "coverage-7.4.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd6545d97c98a192c5ac995d21c894b581f1fd14cf389be90724d21808b657e2"},
+    {file = "coverage-7.4.3-cp310-cp310-win32.whl", hash = "sha256:f6a09b360d67e589236a44f0c39218a8efba2593b6abdccc300a8862cffc2f94"},
+    {file = "coverage-7.4.3-cp310-cp310-win_amd64.whl", hash = "sha256:18d90523ce7553dd0b7e23cbb28865db23cddfd683a38fb224115f7826de78d0"},
+    {file = "coverage-7.4.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cbbe5e739d45a52f3200a771c6d2c7acf89eb2524890a4a3aa1a7fa0695d2a47"},
+    {file = "coverage-7.4.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:489763b2d037b164846ebac0cbd368b8a4ca56385c4090807ff9fad817de4113"},
+    {file = "coverage-7.4.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:451f433ad901b3bb00184d83fd83d135fb682d780b38af7944c9faeecb1e0bfe"},
+    {file = "coverage-7.4.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fcc66e222cf4c719fe7722a403888b1f5e1682d1679bd780e2b26c18bb648cdc"},
+    {file = "coverage-7.4.3-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3ec74cfef2d985e145baae90d9b1b32f85e1741b04cd967aaf9cfa84c1334f3"},
+    {file = "coverage-7.4.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:abbbd8093c5229c72d4c2926afaee0e6e3140de69d5dcd918b2921f2f0c8baba"},
+    {file = "coverage-7.4.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:35eb581efdacf7b7422af677b92170da4ef34500467381e805944a3201df2079"},
+    {file = "coverage-7.4.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8249b1c7334be8f8c3abcaaa996e1e4927b0e5a23b65f5bf6cfe3180d8ca7840"},
+    {file = "coverage-7.4.3-cp311-cp311-win32.whl", hash = "sha256:cf30900aa1ba595312ae41978b95e256e419d8a823af79ce670835409fc02ad3"},
+    {file = "coverage-7.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:18c7320695c949de11a351742ee001849912fd57e62a706d83dfc1581897fa2e"},
+    {file = "coverage-7.4.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b51bfc348925e92a9bd9b2e48dad13431b57011fd1038f08316e6bf1df107d10"},
+    {file = "coverage-7.4.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d6cdecaedea1ea9e033d8adf6a0ab11107b49571bbb9737175444cea6eb72328"},
+    {file = "coverage-7.4.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3b2eccb883368f9e972e216c7b4c7c06cabda925b5f06dde0650281cb7666a30"},
+    {file = "coverage-7.4.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6c00cdc8fa4e50e1cc1f941a7f2e3e0f26cb2a1233c9696f26963ff58445bac7"},
+    {file = "coverage-7.4.3-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b9a4a8dd3dcf4cbd3165737358e4d7dfbd9d59902ad11e3b15eebb6393b0446e"},
+    {file = "coverage-7.4.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:062b0a75d9261e2f9c6d071753f7eef0fc9caf3a2c82d36d76667ba7b6470003"},
+    {file = "coverage-7.4.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:ebe7c9e67a2d15fa97b77ea6571ce5e1e1f6b0db71d1d5e96f8d2bf134303c1d"},
+    {file = "coverage-7.4.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:c0a120238dd71c68484f02562f6d446d736adcc6ca0993712289b102705a9a3a"},
+    {file = "coverage-7.4.3-cp312-cp312-win32.whl", hash = "sha256:37389611ba54fd6d278fde86eb2c013c8e50232e38f5c68235d09d0a3f8aa352"},
+    {file = "coverage-7.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:d25b937a5d9ffa857d41be042b4238dd61db888533b53bc76dc082cb5a15e914"},
+    {file = "coverage-7.4.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:28ca2098939eabab044ad68850aac8f8db6bf0b29bc7f2887d05889b17346454"},
+    {file = "coverage-7.4.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:280459f0a03cecbe8800786cdc23067a8fc64c0bd51dc614008d9c36e1659d7e"},
+    {file = "coverage-7.4.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c0cdedd3500e0511eac1517bf560149764b7d8e65cb800d8bf1c63ebf39edd2"},
+    {file = "coverage-7.4.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9a9babb9466fe1da12417a4aed923e90124a534736de6201794a3aea9d98484e"},
+    {file = "coverage-7.4.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dec9de46a33cf2dd87a5254af095a409ea3bf952d85ad339751e7de6d962cde6"},
+    {file = "coverage-7.4.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:16bae383a9cc5abab9bb05c10a3e5a52e0a788325dc9ba8499e821885928968c"},
+    {file = "coverage-7.4.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:2c854ce44e1ee31bda4e318af1dbcfc929026d12c5ed030095ad98197eeeaed0"},
+    {file = "coverage-7.4.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ce8c50520f57ec57aa21a63ea4f325c7b657386b3f02ccaedeccf9ebe27686e1"},
+    {file = "coverage-7.4.3-cp38-cp38-win32.whl", hash = "sha256:708a3369dcf055c00ddeeaa2b20f0dd1ce664eeabde6623e516c5228b753654f"},
+    {file = "coverage-7.4.3-cp38-cp38-win_amd64.whl", hash = "sha256:1bf25fbca0c8d121a3e92a2a0555c7e5bc981aee5c3fdaf4bb7809f410f696b9"},
+    {file = "coverage-7.4.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3b253094dbe1b431d3a4ac2f053b6d7ede2664ac559705a704f621742e034f1f"},
+    {file = "coverage-7.4.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:77fbfc5720cceac9c200054b9fab50cb2a7d79660609200ab83f5db96162d20c"},
+    {file = "coverage-7.4.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6679060424faa9c11808598504c3ab472de4531c571ab2befa32f4971835788e"},
+    {file = "coverage-7.4.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4af154d617c875b52651dd8dd17a31270c495082f3d55f6128e7629658d63765"},
+    {file = "coverage-7.4.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8640f1fde5e1b8e3439fe482cdc2b0bb6c329f4bb161927c28d2e8879c6029ee"},
+    {file = "coverage-7.4.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:69b9f6f66c0af29642e73a520b6fed25ff9fd69a25975ebe6acb297234eda501"},
+    {file = "coverage-7.4.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:0842571634f39016a6c03e9d4aba502be652a6e4455fadb73cd3a3a49173e38f"},
+    {file = "coverage-7.4.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a78ed23b08e8ab524551f52953a8a05d61c3a760781762aac49f8de6eede8c45"},
+    {file = "coverage-7.4.3-cp39-cp39-win32.whl", hash = "sha256:c0524de3ff096e15fcbfe8f056fdb4ea0bf497d584454f344d59fce069d3e6e9"},
+    {file = "coverage-7.4.3-cp39-cp39-win_amd64.whl", hash = "sha256:0209a6369ccce576b43bb227dc8322d8ef9e323d089c6f3f26a597b09cb4d2aa"},
+    {file = "coverage-7.4.3-pp38.pp39.pp310-none-any.whl", hash = "sha256:7cbde573904625509a3f37b6fecea974e363460b556a627c60dc2f47e2fffa51"},
+    {file = "coverage-7.4.3.tar.gz", hash = "sha256:276f6077a5c61447a48d133ed13e759c09e62aff0dc84274a68dc18660104d52"},
 ]
 
 [package.dependencies]
@@ -853,6 +864,17 @@ files = [
 graph = ["objgraph (>=1.7.2)"]
 profile = ["gprof2dot (>=2022.7.29)"]
 
+[[package]]
+name = "distlib"
+version = "0.3.8"
+description = "Distribution utilities"
+optional = false
+python-versions = "*"
+files = [
+    {file = "distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784"},
+    {file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"},
+]
+
 [[package]]
 name = "docutils"
 version = "0.20.1"
@@ -942,6 +964,22 @@ files = [
 [package.extras]
 devel = ["colorama", "json-spec", "jsonschema", "pylint", "pytest", "pytest-benchmark", "pytest-cache", "validictory"]
 
+[[package]]
+name = "filelock"
+version = "3.13.1"
+description = "A platform independent file lock."
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "filelock-3.13.1-py3-none-any.whl", hash = "sha256:57dbda9b35157b05fb3e58ee91448612eb674172fab98ee235ccb0b5bee19a1c"},
+    {file = "filelock-3.13.1.tar.gz", hash = "sha256:521f5f56c50f8426f5e03ad3b281b490a87ef15bc6c526f168290f0c7148d44e"},
+]
+
+[package.extras]
+docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.24)"]
+testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)"]
+typing = ["typing-extensions (>=4.8)"]
+
 [[package]]
 name = "flake8"
 version = "6.1.0"
@@ -1008,13 +1046,13 @@ files = [
 
 [[package]]
 name = "google-api-core"
-version = "2.17.0"
+version = "2.17.1"
 description = "Google API client core library"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "google-api-core-2.17.0.tar.gz", hash = "sha256:de7ef0450faec7c75e0aea313f29ac870fdc44cfaec9d6499a9a17305980ef66"},
-    {file = "google_api_core-2.17.0-py3-none-any.whl", hash = "sha256:08ed79ed8e93e329de5e3e7452746b734e6bf8438d8d64dd3319d21d3164890c"},
+    {file = "google-api-core-2.17.1.tar.gz", hash = "sha256:9df18a1f87ee0df0bc4eea2770ebc4228392d8cc4066655b320e2cfccb15db95"},
+    {file = "google_api_core-2.17.1-py3-none-any.whl", hash = "sha256:610c5b90092c360736baccf17bd3efbcb30dd380e7a6dc28a71059edb8bd0d8e"},
 ]
 
 [package.dependencies]
@@ -1030,13 +1068,13 @@ grpcio-gcp = ["grpcio-gcp (>=0.2.2,<1.0.dev0)"]
 
 [[package]]
 name = "google-api-python-client"
-version = "2.117.0"
+version = "2.121.0"
 description = "Google API Client Library for Python"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "google-api-python-client-2.117.0.tar.gz", hash = "sha256:b38cd1477ee3c341a0d2f7427326499b416f36c44e9b20d1da229df8be0c596e"},
-    {file = "google_api_python_client-2.117.0-py2.py3-none-any.whl", hash = "sha256:bd6d393d0eaa7ea1fa13aefb44be787d1ebdc068ab8255f1c3f1d8b486f46afd"},
+    {file = "google-api-python-client-2.121.0.tar.gz", hash = "sha256:df863ece4db8b36ce1053ebd983e43fbc5b664209eed78e82cc84ae56ddac6c0"},
+    {file = "google_api_python_client-2.121.0-py2.py3-none-any.whl", hash = "sha256:bb4da677150dd16c45818620baca8a63208c6f4180a0691ad1c1708b384c10be"},
 ]
 
 [package.dependencies]
@@ -1048,13 +1086,13 @@ uritemplate = ">=3.0.1,<5"
 
 [[package]]
 name = "google-auth"
-version = "2.27.0"
+version = "2.28.1"
 description = "Google Authentication Library"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "google-auth-2.27.0.tar.gz", hash = "sha256:e863a56ccc2d8efa83df7a80272601e43487fa9a728a376205c86c26aaefa821"},
-    {file = "google_auth-2.27.0-py2.py3-none-any.whl", hash = "sha256:8e4bad367015430ff253fe49d500fdc3396c1a434db5740828c728e45bcce245"},
+    {file = "google-auth-2.28.1.tar.gz", hash = "sha256:34fc3046c257cedcf1622fc4b31fc2be7923d9b4d44973d481125ecc50d83885"},
+    {file = "google_auth-2.28.1-py2.py3-none-any.whl", hash = "sha256:25141e2d7a14bfcba945f5e9827f98092716e99482562f15306e5b026e21aa72"},
 ]
 
 [package.dependencies]
@@ -1293,13 +1331,13 @@ files = [
 
 [[package]]
 name = "httpcore"
-version = "1.0.2"
+version = "1.0.4"
 description = "A minimal low-level HTTP client."
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "httpcore-1.0.2-py3-none-any.whl", hash = "sha256:096cc05bca73b8e459a1fc3dcf585148f63e534eae4339559c9b8a8d6399acc7"},
-    {file = "httpcore-1.0.2.tar.gz", hash = "sha256:9fc092e4799b26174648e54b74ed5f683132a464e95643b226e00c2ed2fa6535"},
+    {file = "httpcore-1.0.4-py3-none-any.whl", hash = "sha256:ac418c1db41bade2ad53ae2f3834a3a0f5ae76b56cf5aa497d2d033384fc7d73"},
+    {file = "httpcore-1.0.4.tar.gz", hash = "sha256:cb2839ccfcba0d2d3c1131d3c3e26dfc327326fbe7a5dc0dbfe9f6c9151bb022"},
 ]
 
 [package.dependencies]
@@ -1310,7 +1348,7 @@ h11 = ">=0.13,<0.15"
 asyncio = ["anyio (>=4.0,<5.0)"]
 http2 = ["h2 (>=3,<5)"]
 socks = ["socksio (==1.*)"]
-trio = ["trio (>=0.22.0,<0.23.0)"]
+trio = ["trio (>=0.22.0,<0.25.0)"]
 
 [[package]]
 name = "httplib2"
@@ -1328,13 +1366,13 @@ pyparsing = {version = ">=2.4.2,<3.0.0 || >3.0.0,<3.0.1 || >3.0.1,<3.0.2 || >3.0
 
 [[package]]
 name = "httpx"
-version = "0.26.0"
+version = "0.27.0"
 description = "The next generation HTTP client."
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "httpx-0.26.0-py3-none-any.whl", hash = "sha256:8915f5a3627c4d47b73e8202457cb28f1266982d1159bd5779d86a80c0eab1cd"},
-    {file = "httpx-0.26.0.tar.gz", hash = "sha256:451b55c30d5185ea6b23c2c793abf9bb237d2a7dfb901ced6ff69ad37ec1dfaf"},
+    {file = "httpx-0.27.0-py3-none-any.whl", hash = "sha256:71d5465162c13681bff01ad59b2cc68dd838ea1f10e51574bac27103f00c91a5"},
+    {file = "httpx-0.27.0.tar.gz", hash = "sha256:a0cb88a46f32dc874e04ee956e4c2764aba2aa228f650b06788ba6bda2962ab5"},
 ]
 
 [package.dependencies]
@@ -1350,6 +1388,20 @@ cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"]
 http2 = ["h2 (>=3,<5)"]
 socks = ["socksio (==1.*)"]
 
+[[package]]
+name = "identify"
+version = "2.5.35"
+description = "File identification library for Python"
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "identify-2.5.35-py2.py3-none-any.whl", hash = "sha256:c4de0081837b211594f8e877a6b4fad7ca32bbfc1a9307fdd61c28bfe923f13e"},
+    {file = "identify-2.5.35.tar.gz", hash = "sha256:10a7ca245cfcd756a554a7288159f72ff105ad233c7c4b9c6f0f4d108f5f6791"},
+]
+
+[package.extras]
+license = ["ukkonen"]
+
 [[package]]
 name = "idna"
 version = "3.6"
@@ -1440,13 +1492,13 @@ tests = ["pytest", "pytest-cov", "pytest-mock"]
 
 [[package]]
 name = "ipykernel"
-version = "6.29.2"
+version = "6.29.3"
 description = "IPython Kernel for Jupyter"
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "ipykernel-6.29.2-py3-none-any.whl", hash = "sha256:50384f5c577a260a1d53f1f59a828c7266d321c9b7d00d345693783f66616055"},
-    {file = "ipykernel-6.29.2.tar.gz", hash = "sha256:3bade28004e3ff624ed57974948116670604ac5f676d12339693f3142176d3f0"},
+    {file = "ipykernel-6.29.3-py3-none-any.whl", hash = "sha256:5aa086a4175b0229d4eca211e181fb473ea78ffd9869af36ba7694c947302a21"},
+    {file = "ipykernel-6.29.3.tar.gz", hash = "sha256:e14c250d1f9ea3989490225cc1a542781b095a18a19447fcf2b5eaf7d0ac5bd2"},
 ]
 
 [package.dependencies]
@@ -1469,7 +1521,7 @@ cov = ["coverage[toml]", "curio", "matplotlib", "pytest-cov", "trio"]
 docs = ["myst-parser", "pydata-sphinx-theme", "sphinx", "sphinx-autodoc-typehints", "sphinxcontrib-github-alt", "sphinxcontrib-spelling", "trio"]
 pyqt5 = ["pyqt5"]
 pyside6 = ["pyside6"]
-test = ["flaky", "ipyparallel", "pre-commit", "pytest (>=7.0)", "pytest-asyncio (==0.23.4)", "pytest-cov", "pytest-timeout"]
+test = ["flaky", "ipyparallel", "pre-commit", "pytest (>=7.0)", "pytest-asyncio (>=0.23.5)", "pytest-cov", "pytest-timeout"]
 
 [[package]]
 name = "ipython"
@@ -1601,21 +1653,6 @@ docs = ["Jinja2 (==2.11.3)", "MarkupSafe (==1.1.1)", "Pygments (==2.8.1)", "alab
 qa = ["flake8 (==5.0.4)", "mypy (==0.971)", "types-setuptools (==67.2.0.1)"]
 testing = ["Django", "attrs", "colorama", "docopt", "pytest (<7.0.0)"]
 
-[[package]]
-name = "jeepney"
-version = "0.8.0"
-description = "Low-level, pure Python DBus protocol wrapper."
-optional = false
-python-versions = ">=3.7"
-files = [
-    {file = "jeepney-0.8.0-py3-none-any.whl", hash = "sha256:c0a454ad016ca575060802ee4d590dd912e35c122fa04e70306de3d076cce755"},
-    {file = "jeepney-0.8.0.tar.gz", hash = "sha256:5efe48d255973902f6badc3ce55e2aa6c5c3b3bc642059ef3a91247bcfcc5806"},
-]
-
-[package.extras]
-test = ["async-timeout", "pytest", "pytest-asyncio (>=0.17)", "pytest-trio", "testpath", "trio"]
-trio = ["async_generator", "trio"]
-
 [[package]]
 name = "jinja2"
 version = "3.1.3"
@@ -1635,13 +1672,13 @@ i18n = ["Babel (>=2.7)"]
 
 [[package]]
 name = "json5"
-version = "0.9.14"
+version = "0.9.22"
 description = "A Python implementation of the JSON5 data format."
 optional = false
-python-versions = "*"
+python-versions = ">=3.8"
 files = [
-    {file = "json5-0.9.14-py2.py3-none-any.whl", hash = "sha256:740c7f1b9e584a468dbb2939d8d458db3427f2c93ae2139d05f47e453eae964f"},
-    {file = "json5-0.9.14.tar.gz", hash = "sha256:9ed66c3a6ca3510a976a9ef9b8c0787de24802724ab1860bc0153c7fdd589b02"},
+    {file = "json5-0.9.22-py3-none-any.whl", hash = "sha256:6621007c70897652f8b5d03885f732771c48d1925591ad989aa80c7e0e5ad32f"},
+    {file = "json5-0.9.22.tar.gz", hash = "sha256:b729bde7650b2196a35903a597d2b704b8fdf8648bfb67368cfb79f1174a17bd"},
 ]
 
 [package.extras]
@@ -1785,13 +1822,13 @@ test = ["click", "pre-commit", "pytest (>=7.0)", "pytest-asyncio (>=0.19.0)", "p
 
 [[package]]
 name = "jupyter-lsp"
-version = "2.2.2"
+version = "2.2.4"
 description = "Multi-Language Server WebSocket proxy for Jupyter Notebook/Lab server"
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "jupyter-lsp-2.2.2.tar.gz", hash = "sha256:256d24620542ae4bba04a50fc1f6ffe208093a07d8e697fea0a8d1b8ca1b7e5b"},
-    {file = "jupyter_lsp-2.2.2-py3-none-any.whl", hash = "sha256:3b95229e4168355a8c91928057c1621ac3510ba98b2a925e82ebd77f078b1aa5"},
+    {file = "jupyter-lsp-2.2.4.tar.gz", hash = "sha256:5e50033149344065348e688608f3c6d654ef06d9856b67655bd7b6bac9ee2d59"},
+    {file = "jupyter_lsp-2.2.4-py3-none-any.whl", hash = "sha256:da61cb63a16b6dff5eac55c2699cc36eac975645adee02c41bdfc03bf4802e77"},
 ]
 
 [package.dependencies]
@@ -1800,13 +1837,13 @@ jupyter-server = ">=1.1.2"
 
 [[package]]
 name = "jupyter-server"
-version = "2.12.5"
+version = "2.13.0"
 description = "The backend—i.e. core services, APIs, and REST endpoints—to Jupyter web applications."
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "jupyter_server-2.12.5-py3-none-any.whl", hash = "sha256:184a0f82809a8522777cfb6b760ab6f4b1bb398664c5860a27cec696cb884923"},
-    {file = "jupyter_server-2.12.5.tar.gz", hash = "sha256:0edb626c94baa22809be1323f9770cf1c00a952b17097592e40d03e6a3951689"},
+    {file = "jupyter_server-2.13.0-py3-none-any.whl", hash = "sha256:77b2b49c3831fbbfbdb5048cef4350d12946191f833a24e5f83e5f8f4803e97b"},
+    {file = "jupyter_server-2.13.0.tar.gz", hash = "sha256:c80bfb049ea20053c3d9641c2add4848b38073bf79f1729cea1faed32fc1c78e"},
 ]
 
 [package.dependencies]
@@ -1832,7 +1869,7 @@ websocket-client = "*"
 
 [package.extras]
 docs = ["ipykernel", "jinja2", "jupyter-client", "jupyter-server", "myst-parser", "nbformat", "prometheus-client", "pydata-sphinx-theme", "send2trash", "sphinx-autodoc-typehints", "sphinxcontrib-github-alt", "sphinxcontrib-openapi (>=0.8.0)", "sphinxcontrib-spelling", "sphinxemoji", "tornado", "typing-extensions"]
-test = ["flaky", "ipykernel", "pre-commit", "pytest (>=7.0)", "pytest-console-scripts", "pytest-jupyter[server] (>=0.4)", "pytest-timeout", "requests"]
+test = ["flaky", "ipykernel", "pre-commit", "pytest (>=7.0)", "pytest-console-scripts", "pytest-jupyter[server] (>=0.7)", "pytest-timeout", "requests"]
 
 [[package]]
 name = "jupyter-server-terminals"
@@ -1855,13 +1892,13 @@ test = ["jupyter-server (>=2.0.0)", "pytest (>=7.0)", "pytest-jupyter[server] (>
 
 [[package]]
 name = "jupyterlab"
-version = "4.1.0"
+version = "4.1.4"
 description = "JupyterLab computational environment"
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "jupyterlab-4.1.0-py3-none-any.whl", hash = "sha256:5380e85fb4f11a227ed2db13103e513cfea274d1011f6210e62d611e92e0369d"},
-    {file = "jupyterlab-4.1.0.tar.gz", hash = "sha256:92cdfd86c53e163fb9e91e14497901153536c5a889c9225dade270f6107a077f"},
+    {file = "jupyterlab-4.1.4-py3-none-any.whl", hash = "sha256:f92c3f2b12b88efcf767205f49be9b2f86b85544f9c4f342bb5e9904a16cf931"},
+    {file = "jupyterlab-4.1.4.tar.gz", hash = "sha256:e03c82c124ad8a0892e498b9dde79c50868b2c267819aca3f55ce47c57ebeb1d"},
 ]
 
 [package.dependencies]
@@ -1881,7 +1918,7 @@ tornado = ">=6.2.0"
 traitlets = "*"
 
 [package.extras]
-dev = ["build", "bump2version", "coverage", "hatch", "pre-commit", "pytest-cov", "ruff (==0.1.15)"]
+dev = ["build", "bump2version", "coverage", "hatch", "pre-commit", "pytest-cov", "ruff (==0.2.0)"]
 docs = ["jsx-lexer", "myst-parser", "pydata-sphinx-theme (>=0.13.0)", "pytest", "pytest-check-links", "pytest-jupyter", "sphinx (>=1.8,<7.3.0)", "sphinx-copybutton"]
 docs-screenshots = ["altair (==5.2.0)", "ipython (==8.16.1)", "ipywidgets (==8.1.1)", "jupyterlab-geojson (==3.4.0)", "jupyterlab-language-pack-zh-cn (==4.0.post6)", "matplotlib (==3.8.2)", "nbconvert (>=7.0.0)", "pandas (==2.2.0)", "scipy (==1.12.0)", "vega-datasets (==0.9.0)"]
 test = ["coverage", "pytest (>=7.0)", "pytest-check-links (>=0.7)", "pytest-console-scripts", "pytest-cov", "pytest-jupyter (>=0.5.3)", "pytest-timeout", "pytest-tornasync", "requests", "requests-cache", "virtualenv"]
@@ -1899,13 +1936,13 @@ files = [
 
 [[package]]
 name = "jupyterlab-server"
-version = "2.25.2"
+version = "2.25.3"
 description = "A set of server components for JupyterLab and JupyterLab like applications."
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "jupyterlab_server-2.25.2-py3-none-any.whl", hash = "sha256:5b1798c9cc6a44f65c757de9f97fc06fc3d42535afbf47d2ace5e964ab447aaf"},
-    {file = "jupyterlab_server-2.25.2.tar.gz", hash = "sha256:bd0ec7a99ebcedc8bcff939ef86e52c378e44c2707e053fcd81d046ce979ee63"},
+    {file = "jupyterlab_server-2.25.3-py3-none-any.whl", hash = "sha256:c48862519fded9b418c71645d85a49b2f0ec50d032ba8316738e9276046088c1"},
+    {file = "jupyterlab_server-2.25.3.tar.gz", hash = "sha256:846f125a8a19656611df5b03e5912c8393cea6900859baa64fa515eb64a8dc40"},
 ]
 
 [package.dependencies]
@@ -1934,45 +1971,6 @@ files = [
     {file = "jupyterlab_widgets-3.0.10.tar.gz", hash = "sha256:04f2ac04976727e4f9d0fa91cdc2f1ab860f965e504c29dbd6a65c882c9d04c0"},
 ]
 
-[[package]]
-name = "keyring"
-version = "23.4.1"
-description = "Store and access your passwords safely."
-optional = false
-python-versions = ">=3.6"
-files = [
-    {file = "keyring-23.4.1-py3-none-any.whl", hash = "sha256:17e49fb0d6883c2b4445359434dba95aad84aabb29bbff044ad0ed7100232eca"},
-    {file = "keyring-23.4.1.tar.gz", hash = "sha256:89cbd74d4683ed164c8082fb38619341097741323b3786905c6dac04d6915a55"},
-]
-
-[package.dependencies]
-importlib-metadata = ">=3.6"
-jeepney = {version = ">=0.4.2", markers = "sys_platform == \"linux\""}
-pywin32-ctypes = {version = "<0.1.0 || >0.1.0,<0.1.1 || >0.1.1", markers = "sys_platform == \"win32\""}
-SecretStorage = {version = ">=3.2", markers = "sys_platform == \"linux\""}
-
-[package.extras]
-docs = ["jaraco.packaging (>=8.2)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx"]
-testing = ["pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.0.1)", "pytest-flake8", "pytest-mypy"]
-
-[[package]]
-name = "keyrings-alt"
-version = "3.1"
-description = "Alternate keyring implementations"
-optional = false
-python-versions = ">=2.7"
-files = [
-    {file = "keyrings.alt-3.1-py2.py3-none-any.whl", hash = "sha256:6a00fa799baf1385cf9620bd01bcc815aa56e6970342a567bcfea0c4d21abe5f"},
-    {file = "keyrings.alt-3.1.tar.gz", hash = "sha256:b59c86b67b9027a86e841a49efc41025bcc3b1b0308629617b66b7011e52db5a"},
-]
-
-[package.dependencies]
-six = "*"
-
-[package.extras]
-docs = ["jaraco.packaging (>=3.2)", "rst.linker (>=1.9)", "sphinx"]
-testing = ["backports.unittest-mock", "collective.checkdocs", "fs (>=0.5,<2)", "gdata", "keyring[test] (>=10.3.1)", "pycrypto", "pytest (>=3.5)", "pytest-flake8", "pytest-sugar (>=0.9.1)", "python-keyczar"]
-
 [[package]]
 name = "lazy-object-proxy"
 version = "1.10.0"
@@ -2032,71 +2030,90 @@ files = [
 
 [[package]]
 name = "markupsafe"
-version = "2.1.0"
+version = "2.1.5"
 description = "Safely add untrusted strings to HTML/XML markup."
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "MarkupSafe-2.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3028252424c72b2602a323f70fbf50aa80a5d3aa616ea6add4ba21ae9cc9da4c"},
-    {file = "MarkupSafe-2.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:290b02bab3c9e216da57c1d11d2ba73a9f73a614bbdcc027d299a60cdfabb11a"},
-    {file = "MarkupSafe-2.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e104c0c2b4cd765b4e83909cde7ec61a1e313f8a75775897db321450e928cce"},
-    {file = "MarkupSafe-2.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24c3be29abb6b34052fd26fc7a8e0a49b1ee9d282e3665e8ad09a0a68faee5b3"},
-    {file = "MarkupSafe-2.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:204730fd5fe2fe3b1e9ccadb2bd18ba8712b111dcabce185af0b3b5285a7c989"},
-    {file = "MarkupSafe-2.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d3b64c65328cb4cd252c94f83e66e3d7acf8891e60ebf588d7b493a55a1dbf26"},
-    {file = "MarkupSafe-2.1.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:96de1932237abe0a13ba68b63e94113678c379dca45afa040a17b6e1ad7ed076"},
-    {file = "MarkupSafe-2.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:75bb36f134883fdbe13d8e63b8675f5f12b80bb6627f7714c7d6c5becf22719f"},
-    {file = "MarkupSafe-2.1.0-cp310-cp310-win32.whl", hash = "sha256:4056f752015dfa9828dce3140dbadd543b555afb3252507348c493def166d454"},
-    {file = "MarkupSafe-2.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:d4e702eea4a2903441f2735799d217f4ac1b55f7d8ad96ab7d4e25417cb0827c"},
-    {file = "MarkupSafe-2.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:f0eddfcabd6936558ec020130f932d479930581171368fd728efcfb6ef0dd357"},
-    {file = "MarkupSafe-2.1.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ddea4c352a488b5e1069069f2f501006b1a4362cb906bee9a193ef1245a7a61"},
-    {file = "MarkupSafe-2.1.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:09c86c9643cceb1d87ca08cdc30160d1b7ab49a8a21564868921959bd16441b8"},
-    {file = "MarkupSafe-2.1.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a0a0abef2ca47b33fb615b491ce31b055ef2430de52c5b3fb19a4042dbc5cadb"},
-    {file = "MarkupSafe-2.1.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:736895a020e31b428b3382a7887bfea96102c529530299f426bf2e636aacec9e"},
-    {file = "MarkupSafe-2.1.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:679cbb78914ab212c49c67ba2c7396dc599a8479de51b9a87b174700abd9ea49"},
-    {file = "MarkupSafe-2.1.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:84ad5e29bf8bab3ad70fd707d3c05524862bddc54dc040982b0dbcff36481de7"},
-    {file = "MarkupSafe-2.1.0-cp37-cp37m-win32.whl", hash = "sha256:8da5924cb1f9064589767b0f3fc39d03e3d0fb5aa29e0cb21d43106519bd624a"},
-    {file = "MarkupSafe-2.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:454ffc1cbb75227d15667c09f164a0099159da0c1f3d2636aa648f12675491ad"},
-    {file = "MarkupSafe-2.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:142119fb14a1ef6d758912b25c4e803c3ff66920635c44078666fe7cc3f8f759"},
-    {file = "MarkupSafe-2.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b2a5a856019d2833c56a3dcac1b80fe795c95f401818ea963594b345929dffa7"},
-    {file = "MarkupSafe-2.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d1fb9b2eec3c9714dd936860850300b51dbaa37404209c8d4cb66547884b7ed"},
-    {file = "MarkupSafe-2.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:62c0285e91414f5c8f621a17b69fc0088394ccdaa961ef469e833dbff64bd5ea"},
-    {file = "MarkupSafe-2.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fc3150f85e2dbcf99e65238c842d1cfe69d3e7649b19864c1cc043213d9cd730"},
-    {file = "MarkupSafe-2.1.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f02cf7221d5cd915d7fa58ab64f7ee6dd0f6cddbb48683debf5d04ae9b1c2cc1"},
-    {file = "MarkupSafe-2.1.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:d5653619b3eb5cbd35bfba3c12d575db2a74d15e0e1c08bf1db788069d410ce8"},
-    {file = "MarkupSafe-2.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:7d2f5d97fcbd004c03df8d8fe2b973fe2b14e7bfeb2cfa012eaa8759ce9a762f"},
-    {file = "MarkupSafe-2.1.0-cp38-cp38-win32.whl", hash = "sha256:3cace1837bc84e63b3fd2dfce37f08f8c18aeb81ef5cf6bb9b51f625cb4e6cd8"},
-    {file = "MarkupSafe-2.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:fabbe18087c3d33c5824cb145ffca52eccd053061df1d79d4b66dafa5ad2a5ea"},
-    {file = "MarkupSafe-2.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:023af8c54fe63530545f70dd2a2a7eed18d07a9a77b94e8bf1e2ff7f252db9a3"},
-    {file = "MarkupSafe-2.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d66624f04de4af8bbf1c7f21cc06649c1c69a7f84109179add573ce35e46d448"},
-    {file = "MarkupSafe-2.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c532d5ab79be0199fa2658e24a02fce8542df196e60665dd322409a03db6a52c"},
-    {file = "MarkupSafe-2.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e67ec74fada3841b8c5f4c4f197bea916025cb9aa3fe5abf7d52b655d042f956"},
-    {file = "MarkupSafe-2.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30c653fde75a6e5eb814d2a0a89378f83d1d3f502ab710904ee585c38888816c"},
-    {file = "MarkupSafe-2.1.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:961eb86e5be7d0973789f30ebcf6caab60b844203f4396ece27310295a6082c7"},
-    {file = "MarkupSafe-2.1.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:598b65d74615c021423bd45c2bc5e9b59539c875a9bdb7e5f2a6b92dfcfc268d"},
-    {file = "MarkupSafe-2.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:599941da468f2cf22bf90a84f6e2a65524e87be2fce844f96f2dd9a6c9d1e635"},
-    {file = "MarkupSafe-2.1.0-cp39-cp39-win32.whl", hash = "sha256:e6f7f3f41faffaea6596da86ecc2389672fa949bd035251eab26dc6697451d05"},
-    {file = "MarkupSafe-2.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:b8811d48078d1cf2a6863dafb896e68406c5f513048451cd2ded0473133473c7"},
-    {file = "MarkupSafe-2.1.0.tar.gz", hash = "sha256:80beaf63ddfbc64a0452b841d8036ca0611e049650e20afcb882f5d3c266d65f"},
+    {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc"},
+    {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5"},
+    {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46"},
+    {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f"},
+    {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900"},
+    {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff"},
+    {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad"},
+    {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd"},
+    {file = "MarkupSafe-2.1.5-cp310-cp310-win32.whl", hash = "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4"},
+    {file = "MarkupSafe-2.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5"},
+    {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f"},
+    {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2"},
+    {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced"},
+    {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5"},
+    {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c"},
+    {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f"},
+    {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a"},
+    {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f"},
+    {file = "MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906"},
+    {file = "MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617"},
+    {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1"},
+    {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4"},
+    {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee"},
+    {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5"},
+    {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b"},
+    {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a"},
+    {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f"},
+    {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169"},
+    {file = "MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad"},
+    {file = "MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb"},
+    {file = "MarkupSafe-2.1.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f"},
+    {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf"},
+    {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a"},
+    {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52"},
+    {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9"},
+    {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df"},
+    {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50"},
+    {file = "MarkupSafe-2.1.5-cp37-cp37m-win32.whl", hash = "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371"},
+    {file = "MarkupSafe-2.1.5-cp37-cp37m-win_amd64.whl", hash = "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2"},
+    {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a"},
+    {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46"},
+    {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532"},
+    {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab"},
+    {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68"},
+    {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0"},
+    {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4"},
+    {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3"},
+    {file = "MarkupSafe-2.1.5-cp38-cp38-win32.whl", hash = "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff"},
+    {file = "MarkupSafe-2.1.5-cp38-cp38-win_amd64.whl", hash = "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029"},
+    {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf"},
+    {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2"},
+    {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8"},
+    {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3"},
+    {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465"},
+    {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e"},
+    {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea"},
+    {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6"},
+    {file = "MarkupSafe-2.1.5-cp39-cp39-win32.whl", hash = "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf"},
+    {file = "MarkupSafe-2.1.5-cp39-cp39-win_amd64.whl", hash = "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5"},
+    {file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"},
 ]
 
 [[package]]
 name = "marshmallow"
-version = "3.20.2"
+version = "3.21.1"
 description = "A lightweight library for converting complex datatypes to and from native Python datatypes."
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "marshmallow-3.20.2-py3-none-any.whl", hash = "sha256:c21d4b98fee747c130e6bc8f45c4b3199ea66bc00c12ee1f639f0aeca034d5e9"},
-    {file = "marshmallow-3.20.2.tar.gz", hash = "sha256:4c1daff273513dc5eb24b219a8035559dc573c8f322558ef85f5438ddd1236dd"},
+    {file = "marshmallow-3.21.1-py3-none-any.whl", hash = "sha256:f085493f79efb0644f270a9bf2892843142d80d7174bbbd2f3713f2a589dc633"},
+    {file = "marshmallow-3.21.1.tar.gz", hash = "sha256:4e65e9e0d80fc9e609574b9983cf32579f305c718afb30d7233ab818571768c3"},
 ]
 
 [package.dependencies]
 packaging = ">=17.0"
 
 [package.extras]
-dev = ["pre-commit (>=2.4,<4.0)", "pytest", "pytz", "simplejson", "tox"]
-docs = ["alabaster (==0.7.15)", "autodocsumm (==0.2.12)", "sphinx (==7.2.6)", "sphinx-issues (==3.0.1)", "sphinx-version-warning (==1.1.2)"]
-lint = ["pre-commit (>=2.4,<4.0)"]
+dev = ["marshmallow[tests]", "pre-commit (>=3.5,<4.0)", "tox"]
+docs = ["alabaster (==0.7.16)", "autodocsumm (==0.2.12)", "sphinx (==7.2.6)", "sphinx-issues (==4.0.0)", "sphinx-version-warning (==1.1.2)"]
 tests = ["pytest", "pytz", "simplejson"]
 
 [[package]]
@@ -2217,13 +2234,13 @@ test = ["flaky", "ipykernel (>=6.19.3)", "ipython", "ipywidgets", "nbconvert (>=
 
 [[package]]
 name = "nbconvert"
-version = "7.16.0"
-description = "Converting Jupyter Notebooks"
+version = "7.16.2"
+description = "Converting Jupyter Notebooks (.ipynb files) to other formats.  Output formats include asciidoc, html, latex, markdown, pdf, py, rst, script.  nbconvert can be used both as a Python library (`import nbconvert`) or as a command line tool (invoked as `jupyter nbconvert ...`)."
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "nbconvert-7.16.0-py3-none-any.whl", hash = "sha256:ad3dc865ea6e2768d31b7eb6c7ab3be014927216a5ece3ef276748dd809054c7"},
-    {file = "nbconvert-7.16.0.tar.gz", hash = "sha256:813e6553796362489ae572e39ba1bff978536192fb518e10826b0e8cadf03ec8"},
+    {file = "nbconvert-7.16.2-py3-none-any.whl", hash = "sha256:0c01c23981a8de0220255706822c40b751438e32467d6a686e26be08ba784382"},
+    {file = "nbconvert-7.16.2.tar.gz", hash = "sha256:8310edd41e1c43947e4ecf16614c61469ebc024898eb808cce0999860fc9fb16"},
 ]
 
 [package.dependencies]
@@ -2303,20 +2320,34 @@ doc = ["nb2plots (>=0.6)", "numpydoc (>=1.5)", "pillow (>=9.2)", "pydata-sphinx-
 extra = ["lxml (>=4.6)", "pydot (>=1.4.2)", "pygraphviz (>=1.9)", "sympy (>=1.10)"]
 test = ["codecov (>=2.1)", "pytest (>=7.2)", "pytest-cov (>=4.0)"]
 
+[[package]]
+name = "nodeenv"
+version = "1.8.0"
+description = "Node.js virtual environment builder"
+optional = false
+python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*"
+files = [
+    {file = "nodeenv-1.8.0-py2.py3-none-any.whl", hash = "sha256:df865724bb3c3adc86b3876fa209771517b0cfe596beff01a92700e0e8be4cec"},
+    {file = "nodeenv-1.8.0.tar.gz", hash = "sha256:d51e0c37e64fbf47d017feac3145cdbb58836d7eee8c6f6d3b6880c5456227d2"},
+]
+
+[package.dependencies]
+setuptools = "*"
+
 [[package]]
 name = "notebook"
-version = "7.0.7"
+version = "7.1.1"
 description = "Jupyter Notebook - A web-based notebook environment for interactive computing"
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "notebook-7.0.7-py3-none-any.whl", hash = "sha256:289b606d7e173f75a18beb1406ef411b43f97f7a9c55ba03efa3622905a62346"},
-    {file = "notebook-7.0.7.tar.gz", hash = "sha256:3bcff00c17b3ac142ef5f436d50637d936b274cfa0b41f6ac0175363de9b4e09"},
+    {file = "notebook-7.1.1-py3-none-any.whl", hash = "sha256:197d8e0595acabf4005851c8716e952a81b405f7aefb648067a761fbde267ce7"},
+    {file = "notebook-7.1.1.tar.gz", hash = "sha256:818e7420fa21f402e726afb9f02df7f3c10f294c02e383ed19852866c316108b"},
 ]
 
 [package.dependencies]
 jupyter-server = ">=2.4.0,<3"
-jupyterlab = ">=4.0.2,<5"
+jupyterlab = ">=4.1.1,<4.2"
 jupyterlab-server = ">=2.22.1,<3"
 notebook-shim = ">=0.2,<0.3"
 tornado = ">=6.2.0"
@@ -2328,13 +2359,13 @@ test = ["importlib-resources (>=5.0)", "ipykernel", "jupyter-server[test] (>=2.4
 
 [[package]]
 name = "notebook-shim"
-version = "0.2.3"
+version = "0.2.4"
 description = "A shim layer for notebook traits and config"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "notebook_shim-0.2.3-py3-none-any.whl", hash = "sha256:a83496a43341c1674b093bfcebf0fe8e74cbe7eda5fd2bbc56f8e39e1486c0c7"},
-    {file = "notebook_shim-0.2.3.tar.gz", hash = "sha256:f69388ac283ae008cd506dda10d0288b09a017d822d5e8c7129a152cbd3ce7e9"},
+    {file = "notebook_shim-0.2.4-py3-none-any.whl", hash = "sha256:411a5be4e9dc882a074ccbcae671eda64cceb068767e9a3419096986560e1cef"},
+    {file = "notebook_shim-0.2.4.tar.gz", hash = "sha256:b4b2cfa1b65d98307ca24361f5b30fe785b53c3fd07b7a47e89acb5e6ac638cb"},
 ]
 
 [package.dependencies]
@@ -2574,40 +2605,40 @@ doc = ["mkdocs-material"]
 
 [[package]]
 name = "pandas"
-version = "2.2.0"
+version = "2.2.1"
 description = "Powerful data structures for data analysis, time series, and statistics"
 optional = false
 python-versions = ">=3.9"
 files = [
-    {file = "pandas-2.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8108ee1712bb4fa2c16981fba7e68b3f6ea330277f5ca34fa8d557e986a11670"},
-    {file = "pandas-2.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:736da9ad4033aeab51d067fc3bd69a0ba36f5a60f66a527b3d72e2030e63280a"},
-    {file = "pandas-2.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38e0b4fc3ddceb56ec8a287313bc22abe17ab0eb184069f08fc6a9352a769b18"},
-    {file = "pandas-2.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:20404d2adefe92aed3b38da41d0847a143a09be982a31b85bc7dd565bdba0f4e"},
-    {file = "pandas-2.2.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:7ea3ee3f125032bfcade3a4cf85131ed064b4f8dd23e5ce6fa16473e48ebcaf5"},
-    {file = "pandas-2.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f9670b3ac00a387620489dfc1bca66db47a787f4e55911f1293063a78b108df1"},
-    {file = "pandas-2.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:5a946f210383c7e6d16312d30b238fd508d80d927014f3b33fb5b15c2f895430"},
-    {file = "pandas-2.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a1b438fa26b208005c997e78672f1aa8138f67002e833312e6230f3e57fa87d5"},
-    {file = "pandas-2.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8ce2fbc8d9bf303ce54a476116165220a1fedf15985b09656b4b4275300e920b"},
-    {file = "pandas-2.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2707514a7bec41a4ab81f2ccce8b382961a29fbe9492eab1305bb075b2b1ff4f"},
-    {file = "pandas-2.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85793cbdc2d5bc32620dc8ffa715423f0c680dacacf55056ba13454a5be5de88"},
-    {file = "pandas-2.2.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:cfd6c2491dc821b10c716ad6776e7ab311f7df5d16038d0b7458bc0b67dc10f3"},
-    {file = "pandas-2.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a146b9dcacc3123aa2b399df1a284de5f46287a4ab4fbfc237eac98a92ebcb71"},
-    {file = "pandas-2.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:fbc1b53c0e1fdf16388c33c3cca160f798d38aea2978004dd3f4d3dec56454c9"},
-    {file = "pandas-2.2.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:a41d06f308a024981dcaa6c41f2f2be46a6b186b902c94c2674e8cb5c42985bc"},
-    {file = "pandas-2.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:159205c99d7a5ce89ecfc37cb08ed179de7783737cea403b295b5eda8e9c56d1"},
-    {file = "pandas-2.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eb1e1f3861ea9132b32f2133788f3b14911b68102d562715d71bd0013bc45440"},
-    {file = "pandas-2.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:761cb99b42a69005dec2b08854fb1d4888fdf7b05db23a8c5a099e4b886a2106"},
-    {file = "pandas-2.2.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:a20628faaf444da122b2a64b1e5360cde100ee6283ae8effa0d8745153809a2e"},
-    {file = "pandas-2.2.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:f5be5d03ea2073627e7111f61b9f1f0d9625dc3c4d8dda72cc827b0c58a1d042"},
-    {file = "pandas-2.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:a626795722d893ed6aacb64d2401d017ddc8a2341b49e0384ab9bf7112bdec30"},
-    {file = "pandas-2.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9f66419d4a41132eb7e9a73dcec9486cf5019f52d90dd35547af11bc58f8637d"},
-    {file = "pandas-2.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:57abcaeda83fb80d447f28ab0cc7b32b13978f6f733875ebd1ed14f8fbc0f4ab"},
-    {file = "pandas-2.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e60f1f7dba3c2d5ca159e18c46a34e7ca7247a73b5dd1a22b6d59707ed6b899a"},
-    {file = "pandas-2.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb61dc8567b798b969bcc1fc964788f5a68214d333cade8319c7ab33e2b5d88a"},
-    {file = "pandas-2.2.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:52826b5f4ed658fa2b729264d63f6732b8b29949c7fd234510d57c61dbeadfcd"},
-    {file = "pandas-2.2.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:bde2bc699dbd80d7bc7f9cab1e23a95c4375de615860ca089f34e7c64f4a8de7"},
-    {file = "pandas-2.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:3de918a754bbf2da2381e8a3dcc45eede8cd7775b047b923f9006d5f876802ae"},
-    {file = "pandas-2.2.0.tar.gz", hash = "sha256:30b83f7c3eb217fb4d1b494a57a2fda5444f17834f5df2de6b2ffff68dc3c8e2"},
+    {file = "pandas-2.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8df8612be9cd1c7797c93e1c5df861b2ddda0b48b08f2c3eaa0702cf88fb5f88"},
+    {file = "pandas-2.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0f573ab277252ed9aaf38240f3b54cfc90fff8e5cab70411ee1d03f5d51f3944"},
+    {file = "pandas-2.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f02a3a6c83df4026e55b63c1f06476c9aa3ed6af3d89b4f04ea656ccdaaaa359"},
+    {file = "pandas-2.2.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c38ce92cb22a4bea4e3929429aa1067a454dcc9c335799af93ba9be21b6beb51"},
+    {file = "pandas-2.2.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:c2ce852e1cf2509a69e98358e8458775f89599566ac3775e70419b98615f4b06"},
+    {file = "pandas-2.2.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:53680dc9b2519cbf609c62db3ed7c0b499077c7fefda564e330286e619ff0dd9"},
+    {file = "pandas-2.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:94e714a1cca63e4f5939cdce5f29ba8d415d85166be3441165edd427dc9f6bc0"},
+    {file = "pandas-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f821213d48f4ab353d20ebc24e4faf94ba40d76680642fb7ce2ea31a3ad94f9b"},
+    {file = "pandas-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c70e00c2d894cb230e5c15e4b1e1e6b2b478e09cf27cc593a11ef955b9ecc81a"},
+    {file = "pandas-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e97fbb5387c69209f134893abc788a6486dbf2f9e511070ca05eed4b930b1b02"},
+    {file = "pandas-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:101d0eb9c5361aa0146f500773395a03839a5e6ecde4d4b6ced88b7e5a1a6403"},
+    {file = "pandas-2.2.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:7d2ed41c319c9fb4fd454fe25372028dfa417aacb9790f68171b2e3f06eae8cd"},
+    {file = "pandas-2.2.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:af5d3c00557d657c8773ef9ee702c61dd13b9d7426794c9dfeb1dc4a0bf0ebc7"},
+    {file = "pandas-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:06cf591dbaefb6da9de8472535b185cba556d0ce2e6ed28e21d919704fef1a9e"},
+    {file = "pandas-2.2.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:88ecb5c01bb9ca927ebc4098136038519aa5d66b44671861ffab754cae75102c"},
+    {file = "pandas-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:04f6ec3baec203c13e3f8b139fb0f9f86cd8c0b94603ae3ae8ce9a422e9f5bee"},
+    {file = "pandas-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a935a90a76c44fe170d01e90a3594beef9e9a6220021acfb26053d01426f7dc2"},
+    {file = "pandas-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c391f594aae2fd9f679d419e9a4d5ba4bce5bb13f6a989195656e7dc4b95c8f0"},
+    {file = "pandas-2.2.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9d1265545f579edf3f8f0cb6f89f234f5e44ba725a34d86535b1a1d38decbccc"},
+    {file = "pandas-2.2.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:11940e9e3056576ac3244baef2fedade891977bcc1cb7e5cc8f8cc7d603edc89"},
+    {file = "pandas-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:4acf681325ee1c7f950d058b05a820441075b0dd9a2adf5c4835b9bc056bf4fb"},
+    {file = "pandas-2.2.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9bd8a40f47080825af4317d0340c656744f2bfdb6819f818e6ba3cd24c0e1397"},
+    {file = "pandas-2.2.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:df0c37ebd19e11d089ceba66eba59a168242fc6b7155cba4ffffa6eccdfb8f16"},
+    {file = "pandas-2.2.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:739cc70eaf17d57608639e74d63387b0d8594ce02f69e7a0b046f117974b3019"},
+    {file = "pandas-2.2.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9d3558d263073ed95e46f4650becff0c5e1ffe0fc3a015de3c79283dfbdb3df"},
+    {file = "pandas-2.2.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4aa1d8707812a658debf03824016bf5ea0d516afdea29b7dc14cf687bc4d4ec6"},
+    {file = "pandas-2.2.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:76f27a809cda87e07f192f001d11adc2b930e93a2b0c4a236fde5429527423be"},
+    {file = "pandas-2.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:1ba21b1d5c0e43416218db63037dbe1a01fc101dc6e6024bcad08123e48004ab"},
+    {file = "pandas-2.2.1.tar.gz", hash = "sha256:0ab90f87093c13f3e8fa45b48ba9f39181046e8f3317d3aadb2fffbb1b978572"},
 ]
 
 [package.dependencies]
@@ -2635,6 +2666,7 @@ parquet = ["pyarrow (>=10.0.1)"]
 performance = ["bottleneck (>=1.3.6)", "numba (>=0.56.4)", "numexpr (>=2.8.4)"]
 plot = ["matplotlib (>=3.6.3)"]
 postgresql = ["SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "psycopg2 (>=2.9.6)"]
+pyarrow = ["pyarrow (>=10.0.1)"]
 spss = ["pyreadstat (>=1.2.0)"]
 sql-other = ["SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "adbc-driver-sqlite (>=0.8.0)"]
 test = ["hypothesis (>=6.46.1)", "pytest (>=7.3.2)", "pytest-xdist (>=2.2.0)"]
@@ -2740,15 +2772,33 @@ files = [
 dev = ["pre-commit", "tox"]
 testing = ["pytest", "pytest-benchmark"]
 
+[[package]]
+name = "pre-commit"
+version = "3.6.2"
+description = "A framework for managing and maintaining multi-language pre-commit hooks."
+optional = false
+python-versions = ">=3.9"
+files = [
+    {file = "pre_commit-3.6.2-py2.py3-none-any.whl", hash = "sha256:ba637c2d7a670c10daedc059f5c49b5bd0aadbccfcd7ec15592cf9665117532c"},
+    {file = "pre_commit-3.6.2.tar.gz", hash = "sha256:c3ef34f463045c88658c5b99f38c1e297abdcc0ff13f98d3370055fbbfabc67e"},
+]
+
+[package.dependencies]
+cfgv = ">=2.0.0"
+identify = ">=1.0.0"
+nodeenv = ">=0.11.1"
+pyyaml = ">=5.1"
+virtualenv = ">=20.10.0"
+
 [[package]]
 name = "prometheus-client"
-version = "0.19.0"
+version = "0.20.0"
 description = "Python client for the Prometheus monitoring system."
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "prometheus_client-0.19.0-py3-none-any.whl", hash = "sha256:c88b1e6ecf6b41cd8fb5731c7ae919bf66df6ec6fafa555cd6c0e16ca169ae92"},
-    {file = "prometheus_client-0.19.0.tar.gz", hash = "sha256:4585b0d1223148c27a225b10dbec5ae9bc4c81a99a3fa80774fa6209935324e1"},
+    {file = "prometheus_client-0.20.0-py3-none-any.whl", hash = "sha256:cde524a85bce83ca359cc837f28b8c0db5cac7aa653a588fd7e84ba061c329e7"},
+    {file = "prometheus_client-0.20.0.tar.gz", hash = "sha256:287629d00b147a32dcb2be0b9df905da599b2d82f80377083ec8463309a4bb89"},
 ]
 
 [package.extras]
@@ -2770,22 +2820,22 @@ wcwidth = "*"
 
 [[package]]
 name = "protobuf"
-version = "4.25.2"
+version = "4.25.3"
 description = ""
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "protobuf-4.25.2-cp310-abi3-win32.whl", hash = "sha256:b50c949608682b12efb0b2717f53256f03636af5f60ac0c1d900df6213910fd6"},
-    {file = "protobuf-4.25.2-cp310-abi3-win_amd64.whl", hash = "sha256:8f62574857ee1de9f770baf04dde4165e30b15ad97ba03ceac65f760ff018ac9"},
-    {file = "protobuf-4.25.2-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:2db9f8fa64fbdcdc93767d3cf81e0f2aef176284071507e3ede160811502fd3d"},
-    {file = "protobuf-4.25.2-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:10894a2885b7175d3984f2be8d9850712c57d5e7587a2410720af8be56cdaf62"},
-    {file = "protobuf-4.25.2-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:fc381d1dd0516343f1440019cedf08a7405f791cd49eef4ae1ea06520bc1c020"},
-    {file = "protobuf-4.25.2-cp38-cp38-win32.whl", hash = "sha256:33a1aeef4b1927431d1be780e87b641e322b88d654203a9e9d93f218ee359e61"},
-    {file = "protobuf-4.25.2-cp38-cp38-win_amd64.whl", hash = "sha256:47f3de503fe7c1245f6f03bea7e8d3ec11c6c4a2ea9ef910e3221c8a15516d62"},
-    {file = "protobuf-4.25.2-cp39-cp39-win32.whl", hash = "sha256:5e5c933b4c30a988b52e0b7c02641760a5ba046edc5e43d3b94a74c9fc57c1b3"},
-    {file = "protobuf-4.25.2-cp39-cp39-win_amd64.whl", hash = "sha256:d66a769b8d687df9024f2985d5137a337f957a0916cf5464d1513eee96a63ff0"},
-    {file = "protobuf-4.25.2-py3-none-any.whl", hash = "sha256:a8b7a98d4ce823303145bf3c1a8bdb0f2f4642a414b196f04ad9853ed0c8f830"},
-    {file = "protobuf-4.25.2.tar.gz", hash = "sha256:fe599e175cb347efc8ee524bcd4b902d11f7262c0e569ececcb89995c15f0a5e"},
+    {file = "protobuf-4.25.3-cp310-abi3-win32.whl", hash = "sha256:d4198877797a83cbfe9bffa3803602bbe1625dc30d8a097365dbc762e5790faa"},
+    {file = "protobuf-4.25.3-cp310-abi3-win_amd64.whl", hash = "sha256:209ba4cc916bab46f64e56b85b090607a676f66b473e6b762e6f1d9d591eb2e8"},
+    {file = "protobuf-4.25.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:f1279ab38ecbfae7e456a108c5c0681e4956d5b1090027c1de0f934dfdb4b35c"},
+    {file = "protobuf-4.25.3-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:e7cb0ae90dd83727f0c0718634ed56837bfeeee29a5f82a7514c03ee1364c019"},
+    {file = "protobuf-4.25.3-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:7c8daa26095f82482307bc717364e7c13f4f1c99659be82890dcfc215194554d"},
+    {file = "protobuf-4.25.3-cp38-cp38-win32.whl", hash = "sha256:f4f118245c4a087776e0a8408be33cf09f6c547442c00395fbfb116fac2f8ac2"},
+    {file = "protobuf-4.25.3-cp38-cp38-win_amd64.whl", hash = "sha256:c053062984e61144385022e53678fbded7aea14ebb3e0305ae3592fb219ccfa4"},
+    {file = "protobuf-4.25.3-cp39-cp39-win32.whl", hash = "sha256:19b270aeaa0099f16d3ca02628546b8baefe2955bbe23224aaf856134eccf1e4"},
+    {file = "protobuf-4.25.3-cp39-cp39-win_amd64.whl", hash = "sha256:e3c97a1555fd6388f857770ff8b9703083de6bf1f9274a002a332d65fbb56c8c"},
+    {file = "protobuf-4.25.3-py3-none-any.whl", hash = "sha256:f0700d54bcf45424477e46a9f0944155b46fb0639d69728739c0e47bab83f2b9"},
+    {file = "protobuf-4.25.3.tar.gz", hash = "sha256:25b5d0b42fd000320bd7830b349e3b696435f3b329810427a6bcce6a5492cc5c"},
 ]
 
 [[package]]
@@ -3025,7 +3075,7 @@ testutils = ["gitpython (>3)"]
 name = "pyopenssl"
 version = "23.3.0"
 description = "Python wrapper module around the OpenSSL library"
-optional = false
+optional = true
 python-versions = ">=3.7"
 files = [
     {file = "pyOpenSSL-23.3.0-py3-none-any.whl", hash = "sha256:6756834481d9ed5470f4a9393455154bc92fe7a64b7bc6ee2c804e78c52099b2"},
@@ -3041,13 +3091,13 @@ test = ["flaky", "pretend", "pytest (>=3.0.1)"]
 
 [[package]]
 name = "pyparsing"
-version = "3.1.1"
+version = "3.1.2"
 description = "pyparsing module - Classes and methods to define and execute parsing grammars"
 optional = false
 python-versions = ">=3.6.8"
 files = [
-    {file = "pyparsing-3.1.1-py3-none-any.whl", hash = "sha256:32c7c0b711493c72ff18a981d24f28aaf9c1fb7ed5e9667c9e84e3db623bdbfb"},
-    {file = "pyparsing-3.1.1.tar.gz", hash = "sha256:ede28a1a32462f5a9705e07aea48001a08f7cf81a021585011deba701581a0db"},
+    {file = "pyparsing-3.1.2-py3-none-any.whl", hash = "sha256:f9db75911801ed778fe61bb643079ff86601aca99fcae6345aa67292038fb742"},
+    {file = "pyparsing-3.1.2.tar.gz", hash = "sha256:a1bac0ce561155ecc3ed78ca94d3c9378656ad4c94c1270de543f621420f94ad"},
 ]
 
 [package.extras]
@@ -3147,13 +3197,13 @@ testing = ["filelock"]
 
 [[package]]
 name = "python-dateutil"
-version = "2.8.2"
+version = "2.9.0.post0"
 description = "Extensions to the standard Python datetime module"
 optional = false
 python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
 files = [
-    {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"},
-    {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"},
+    {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"},
+    {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"},
 ]
 
 [package.dependencies]
@@ -3218,30 +3268,19 @@ files = [
     {file = "pywin32-306-cp39-cp39-win_amd64.whl", hash = "sha256:39b61c15272833b5c329a2989999dcae836b1eed650252ab1b7bfbe1d59f30f4"},
 ]
 
-[[package]]
-name = "pywin32-ctypes"
-version = "0.2.2"
-description = "A (partial) reimplementation of pywin32 using ctypes/cffi"
-optional = false
-python-versions = ">=3.6"
-files = [
-    {file = "pywin32-ctypes-0.2.2.tar.gz", hash = "sha256:3426e063bdd5fd4df74a14fa3cf80a0b42845a87e1d1e81f6549f9daec593a60"},
-    {file = "pywin32_ctypes-0.2.2-py3-none-any.whl", hash = "sha256:bf490a1a709baf35d688fe0ecf980ed4de11d2b3e37b51e5442587a75d9957e7"},
-]
-
 [[package]]
 name = "pywinpty"
-version = "2.0.12"
+version = "2.0.13"
 description = "Pseudo terminal support for Windows from Python."
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "pywinpty-2.0.12-cp310-none-win_amd64.whl", hash = "sha256:21319cd1d7c8844fb2c970fb3a55a3db5543f112ff9cfcd623746b9c47501575"},
-    {file = "pywinpty-2.0.12-cp311-none-win_amd64.whl", hash = "sha256:853985a8f48f4731a716653170cd735da36ffbdc79dcb4c7b7140bce11d8c722"},
-    {file = "pywinpty-2.0.12-cp312-none-win_amd64.whl", hash = "sha256:1617b729999eb6713590e17665052b1a6ae0ad76ee31e60b444147c5b6a35dca"},
-    {file = "pywinpty-2.0.12-cp38-none-win_amd64.whl", hash = "sha256:189380469ca143d06e19e19ff3fba0fcefe8b4a8cc942140a6b863aed7eebb2d"},
-    {file = "pywinpty-2.0.12-cp39-none-win_amd64.whl", hash = "sha256:7520575b6546db23e693cbd865db2764097bd6d4ef5dc18c92555904cd62c3d4"},
-    {file = "pywinpty-2.0.12.tar.gz", hash = "sha256:8197de460ae8ebb7f5d1701dfa1b5df45b157bb832e92acba316305e18ca00dd"},
+    {file = "pywinpty-2.0.13-cp310-none-win_amd64.whl", hash = "sha256:697bff211fb5a6508fee2dc6ff174ce03f34a9a233df9d8b5fe9c8ce4d5eaf56"},
+    {file = "pywinpty-2.0.13-cp311-none-win_amd64.whl", hash = "sha256:b96fb14698db1284db84ca38c79f15b4cfdc3172065b5137383910567591fa99"},
+    {file = "pywinpty-2.0.13-cp312-none-win_amd64.whl", hash = "sha256:2fd876b82ca750bb1333236ce98488c1be96b08f4f7647cfdf4129dfad83c2d4"},
+    {file = "pywinpty-2.0.13-cp38-none-win_amd64.whl", hash = "sha256:61d420c2116c0212808d31625611b51caf621fe67f8a6377e2e8b617ea1c1f7d"},
+    {file = "pywinpty-2.0.13-cp39-none-win_amd64.whl", hash = "sha256:71cb613a9ee24174730ac7ae439fd179ca34ccb8c5349e8d7b72ab5dea2c6f4b"},
+    {file = "pywinpty-2.0.13.tar.gz", hash = "sha256:c34e32351a3313ddd0d7da23d27f835c860d32fe4ac814d372a3ea9594f41dde"},
 ]
 
 [[package]]
@@ -3256,7 +3295,6 @@ files = [
     {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"},
     {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"},
     {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"},
-    {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"},
     {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"},
     {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"},
     {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"},
@@ -3264,16 +3302,8 @@ files = [
     {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"},
     {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"},
     {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"},
-    {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"},
     {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"},
     {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"},
-    {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"},
-    {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"},
-    {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"},
-    {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"},
-    {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"},
-    {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"},
-    {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"},
     {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"},
     {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"},
     {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"},
@@ -3290,7 +3320,6 @@ files = [
     {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"},
     {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"},
     {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"},
-    {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"},
     {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"},
     {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"},
     {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"},
@@ -3298,7 +3327,6 @@ files = [
     {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"},
     {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"},
     {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"},
-    {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"},
     {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"},
     {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"},
     {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"},
@@ -3613,110 +3641,110 @@ files = [
 
 [[package]]
 name = "rpds-py"
-version = "0.17.1"
+version = "0.18.0"
 description = "Python bindings to Rust's persistent data structures (rpds)"
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "rpds_py-0.17.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:4128980a14ed805e1b91a7ed551250282a8ddf8201a4e9f8f5b7e6225f54170d"},
-    {file = "rpds_py-0.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ff1dcb8e8bc2261a088821b2595ef031c91d499a0c1b031c152d43fe0a6ecec8"},
-    {file = "rpds_py-0.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d65e6b4f1443048eb7e833c2accb4fa7ee67cc7d54f31b4f0555b474758bee55"},
-    {file = "rpds_py-0.17.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a71169d505af63bb4d20d23a8fbd4c6ce272e7bce6cc31f617152aa784436f29"},
-    {file = "rpds_py-0.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:436474f17733c7dca0fbf096d36ae65277e8645039df12a0fa52445ca494729d"},
-    {file = "rpds_py-0.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:10162fe3f5f47c37ebf6d8ff5a2368508fe22007e3077bf25b9c7d803454d921"},
-    {file = "rpds_py-0.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:720215373a280f78a1814becb1312d4e4d1077b1202a56d2b0815e95ccb99ce9"},
-    {file = "rpds_py-0.17.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:70fcc6c2906cfa5c6a552ba7ae2ce64b6c32f437d8f3f8eea49925b278a61453"},
-    {file = "rpds_py-0.17.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:91e5a8200e65aaac342a791272c564dffcf1281abd635d304d6c4e6b495f29dc"},
-    {file = "rpds_py-0.17.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:99f567dae93e10be2daaa896e07513dd4bf9c2ecf0576e0533ac36ba3b1d5394"},
-    {file = "rpds_py-0.17.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:24e4900a6643f87058a27320f81336d527ccfe503984528edde4bb660c8c8d59"},
-    {file = "rpds_py-0.17.1-cp310-none-win32.whl", hash = "sha256:0bfb09bf41fe7c51413f563373e5f537eaa653d7adc4830399d4e9bdc199959d"},
-    {file = "rpds_py-0.17.1-cp310-none-win_amd64.whl", hash = "sha256:20de7b7179e2031a04042e85dc463a93a82bc177eeba5ddd13ff746325558aa6"},
-    {file = "rpds_py-0.17.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:65dcf105c1943cba45d19207ef51b8bc46d232a381e94dd38719d52d3980015b"},
-    {file = "rpds_py-0.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:01f58a7306b64e0a4fe042047dd2b7d411ee82e54240284bab63e325762c1147"},
-    {file = "rpds_py-0.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:071bc28c589b86bc6351a339114fb7a029f5cddbaca34103aa573eba7b482382"},
-    {file = "rpds_py-0.17.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ae35e8e6801c5ab071b992cb2da958eee76340e6926ec693b5ff7d6381441745"},
-    {file = "rpds_py-0.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:149c5cd24f729e3567b56e1795f74577aa3126c14c11e457bec1b1c90d212e38"},
-    {file = "rpds_py-0.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e796051f2070f47230c745d0a77a91088fbee2cc0502e9b796b9c6471983718c"},
-    {file = "rpds_py-0.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:60e820ee1004327609b28db8307acc27f5f2e9a0b185b2064c5f23e815f248f8"},
-    {file = "rpds_py-0.17.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1957a2ab607f9added64478a6982742eb29f109d89d065fa44e01691a20fc20a"},
-    {file = "rpds_py-0.17.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8587fd64c2a91c33cdc39d0cebdaf30e79491cc029a37fcd458ba863f8815383"},
-    {file = "rpds_py-0.17.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4dc889a9d8a34758d0fcc9ac86adb97bab3fb7f0c4d29794357eb147536483fd"},
-    {file = "rpds_py-0.17.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:2953937f83820376b5979318840f3ee47477d94c17b940fe31d9458d79ae7eea"},
-    {file = "rpds_py-0.17.1-cp311-none-win32.whl", hash = "sha256:1bfcad3109c1e5ba3cbe2f421614e70439f72897515a96c462ea657261b96518"},
-    {file = "rpds_py-0.17.1-cp311-none-win_amd64.whl", hash = "sha256:99da0a4686ada4ed0f778120a0ea8d066de1a0a92ab0d13ae68492a437db78bf"},
-    {file = "rpds_py-0.17.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:1dc29db3900cb1bb40353772417800f29c3d078dbc8024fd64655a04ee3c4bdf"},
-    {file = "rpds_py-0.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:82ada4a8ed9e82e443fcef87e22a3eed3654dd3adf6e3b3a0deb70f03e86142a"},
-    {file = "rpds_py-0.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d36b2b59e8cc6e576f8f7b671e32f2ff43153f0ad6d0201250a7c07f25d570e"},
-    {file = "rpds_py-0.17.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3677fcca7fb728c86a78660c7fb1b07b69b281964673f486ae72860e13f512ad"},
-    {file = "rpds_py-0.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:516fb8c77805159e97a689e2f1c80655c7658f5af601c34ffdb916605598cda2"},
-    {file = "rpds_py-0.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df3b6f45ba4515632c5064e35ca7f31d51d13d1479673185ba8f9fefbbed58b9"},
-    {file = "rpds_py-0.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a967dd6afda7715d911c25a6ba1517975acd8d1092b2f326718725461a3d33f9"},
-    {file = "rpds_py-0.17.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:dbbb95e6fc91ea3102505d111b327004d1c4ce98d56a4a02e82cd451f9f57140"},
-    {file = "rpds_py-0.17.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:02866e060219514940342a1f84303a1ef7a1dad0ac311792fbbe19b521b489d2"},
-    {file = "rpds_py-0.17.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:2528ff96d09f12e638695f3a2e0c609c7b84c6df7c5ae9bfeb9252b6fa686253"},
-    {file = "rpds_py-0.17.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bd345a13ce06e94c753dab52f8e71e5252aec1e4f8022d24d56decd31e1b9b23"},
-    {file = "rpds_py-0.17.1-cp312-none-win32.whl", hash = "sha256:2a792b2e1d3038daa83fa474d559acfd6dc1e3650ee93b2662ddc17dbff20ad1"},
-    {file = "rpds_py-0.17.1-cp312-none-win_amd64.whl", hash = "sha256:292f7344a3301802e7c25c53792fae7d1593cb0e50964e7bcdcc5cf533d634e3"},
-    {file = "rpds_py-0.17.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:8ffe53e1d8ef2520ebcf0c9fec15bb721da59e8ef283b6ff3079613b1e30513d"},
-    {file = "rpds_py-0.17.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4341bd7579611cf50e7b20bb8c2e23512a3dc79de987a1f411cb458ab670eb90"},
-    {file = "rpds_py-0.17.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f4eb548daf4836e3b2c662033bfbfc551db58d30fd8fe660314f86bf8510b93"},
-    {file = "rpds_py-0.17.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b686f25377f9c006acbac63f61614416a6317133ab7fafe5de5f7dc8a06d42eb"},
-    {file = "rpds_py-0.17.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4e21b76075c01d65d0f0f34302b5a7457d95721d5e0667aea65e5bb3ab415c25"},
-    {file = "rpds_py-0.17.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b86b21b348f7e5485fae740d845c65a880f5d1eda1e063bc59bef92d1f7d0c55"},
-    {file = "rpds_py-0.17.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f175e95a197f6a4059b50757a3dca33b32b61691bdbd22c29e8a8d21d3914cae"},
-    {file = "rpds_py-0.17.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1701fc54460ae2e5efc1dd6350eafd7a760f516df8dbe51d4a1c79d69472fbd4"},
-    {file = "rpds_py-0.17.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:9051e3d2af8f55b42061603e29e744724cb5f65b128a491446cc029b3e2ea896"},
-    {file = "rpds_py-0.17.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:7450dbd659fed6dd41d1a7d47ed767e893ba402af8ae664c157c255ec6067fde"},
-    {file = "rpds_py-0.17.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:5a024fa96d541fd7edaa0e9d904601c6445e95a729a2900c5aec6555fe921ed6"},
-    {file = "rpds_py-0.17.1-cp38-none-win32.whl", hash = "sha256:da1ead63368c04a9bded7904757dfcae01eba0e0f9bc41d3d7f57ebf1c04015a"},
-    {file = "rpds_py-0.17.1-cp38-none-win_amd64.whl", hash = "sha256:841320e1841bb53fada91c9725e766bb25009cfd4144e92298db296fb6c894fb"},
-    {file = "rpds_py-0.17.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:f6c43b6f97209e370124baf2bf40bb1e8edc25311a158867eb1c3a5d449ebc7a"},
-    {file = "rpds_py-0.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5e7d63ec01fe7c76c2dbb7e972fece45acbb8836e72682bde138e7e039906e2c"},
-    {file = "rpds_py-0.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:81038ff87a4e04c22e1d81f947c6ac46f122e0c80460b9006e6517c4d842a6ec"},
-    {file = "rpds_py-0.17.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:810685321f4a304b2b55577c915bece4c4a06dfe38f6e62d9cc1d6ca8ee86b99"},
-    {file = "rpds_py-0.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:25f071737dae674ca8937a73d0f43f5a52e92c2d178330b4c0bb6ab05586ffa6"},
-    {file = "rpds_py-0.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aa5bfb13f1e89151ade0eb812f7b0d7a4d643406caaad65ce1cbabe0a66d695f"},
-    {file = "rpds_py-0.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dfe07308b311a8293a0d5ef4e61411c5c20f682db6b5e73de6c7c8824272c256"},
-    {file = "rpds_py-0.17.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a000133a90eea274a6f28adc3084643263b1e7c1a5a66eb0a0a7a36aa757ed74"},
-    {file = "rpds_py-0.17.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:5d0e8a6434a3fbf77d11448c9c25b2f25244226cfbec1a5159947cac5b8c5fa4"},
-    {file = "rpds_py-0.17.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:efa767c220d94aa4ac3a6dd3aeb986e9f229eaf5bce92d8b1b3018d06bed3772"},
-    {file = "rpds_py-0.17.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:dbc56680ecf585a384fbd93cd42bc82668b77cb525343170a2d86dafaed2a84b"},
-    {file = "rpds_py-0.17.1-cp39-none-win32.whl", hash = "sha256:270987bc22e7e5a962b1094953ae901395e8c1e1e83ad016c5cfcfff75a15a3f"},
-    {file = "rpds_py-0.17.1-cp39-none-win_amd64.whl", hash = "sha256:2a7b2f2f56a16a6d62e55354dd329d929560442bd92e87397b7a9586a32e3e76"},
-    {file = "rpds_py-0.17.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a3264e3e858de4fc601741498215835ff324ff2482fd4e4af61b46512dd7fc83"},
-    {file = "rpds_py-0.17.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:f2f3b28b40fddcb6c1f1f6c88c6f3769cd933fa493ceb79da45968a21dccc920"},
-    {file = "rpds_py-0.17.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9584f8f52010295a4a417221861df9bea4c72d9632562b6e59b3c7b87a1522b7"},
-    {file = "rpds_py-0.17.1-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c64602e8be701c6cfe42064b71c84ce62ce66ddc6422c15463fd8127db3d8066"},
-    {file = "rpds_py-0.17.1-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:060f412230d5f19fc8c8b75f315931b408d8ebf56aec33ef4168d1b9e54200b1"},
-    {file = "rpds_py-0.17.1-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b9412abdf0ba70faa6e2ee6c0cc62a8defb772e78860cef419865917d86c7342"},
-    {file = "rpds_py-0.17.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9737bdaa0ad33d34c0efc718741abaafce62fadae72c8b251df9b0c823c63b22"},
-    {file = "rpds_py-0.17.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9f0e4dc0f17dcea4ab9d13ac5c666b6b5337042b4d8f27e01b70fae41dd65c57"},
-    {file = "rpds_py-0.17.1-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:1db228102ab9d1ff4c64148c96320d0be7044fa28bd865a9ce628ce98da5973d"},
-    {file = "rpds_py-0.17.1-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:d8bbd8e56f3ba25a7d0cf980fc42b34028848a53a0e36c9918550e0280b9d0b6"},
-    {file = "rpds_py-0.17.1-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:be22ae34d68544df293152b7e50895ba70d2a833ad9566932d750d3625918b82"},
-    {file = "rpds_py-0.17.1-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:bf046179d011e6114daf12a534d874958b039342b347348a78b7cdf0dd9d6041"},
-    {file = "rpds_py-0.17.1-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:1a746a6d49665058a5896000e8d9d2f1a6acba8a03b389c1e4c06e11e0b7f40d"},
-    {file = "rpds_py-0.17.1-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f0b8bf5b8db49d8fd40f54772a1dcf262e8be0ad2ab0206b5a2ec109c176c0a4"},
-    {file = "rpds_py-0.17.1-pp38-pypy38_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f7f4cb1f173385e8a39c29510dd11a78bf44e360fb75610594973f5ea141028b"},
-    {file = "rpds_py-0.17.1-pp38-pypy38_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7fbd70cb8b54fe745301921b0816c08b6d917593429dfc437fd024b5ba713c58"},
-    {file = "rpds_py-0.17.1-pp38-pypy38_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9bdf1303df671179eaf2cb41e8515a07fc78d9d00f111eadbe3e14262f59c3d0"},
-    {file = "rpds_py-0.17.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fad059a4bd14c45776600d223ec194e77db6c20255578bb5bcdd7c18fd169361"},
-    {file = "rpds_py-0.17.1-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3664d126d3388a887db44c2e293f87d500c4184ec43d5d14d2d2babdb4c64cad"},
-    {file = "rpds_py-0.17.1-pp38-pypy38_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:698ea95a60c8b16b58be9d854c9f993c639f5c214cf9ba782eca53a8789d6b19"},
-    {file = "rpds_py-0.17.1-pp38-pypy38_pp73-musllinux_1_2_i686.whl", hash = "sha256:c3d2010656999b63e628a3c694f23020322b4178c450dc478558a2b6ef3cb9bb"},
-    {file = "rpds_py-0.17.1-pp38-pypy38_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:938eab7323a736533f015e6069a7d53ef2dcc841e4e533b782c2bfb9fb12d84b"},
-    {file = "rpds_py-0.17.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:1e626b365293a2142a62b9a614e1f8e331b28f3ca57b9f05ebbf4cf2a0f0bdc5"},
-    {file = "rpds_py-0.17.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:380e0df2e9d5d5d339803cfc6d183a5442ad7ab3c63c2a0982e8c824566c5ccc"},
-    {file = "rpds_py-0.17.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b760a56e080a826c2e5af09002c1a037382ed21d03134eb6294812dda268c811"},
-    {file = "rpds_py-0.17.1-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5576ee2f3a309d2bb403ec292d5958ce03953b0e57a11d224c1f134feaf8c40f"},
-    {file = "rpds_py-0.17.1-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3c3461ebb4c4f1bbc70b15d20b565759f97a5aaf13af811fcefc892e9197ba"},
-    {file = "rpds_py-0.17.1-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:637b802f3f069a64436d432117a7e58fab414b4e27a7e81049817ae94de45d8d"},
-    {file = "rpds_py-0.17.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ffee088ea9b593cc6160518ba9bd319b5475e5f3e578e4552d63818773c6f56a"},
-    {file = "rpds_py-0.17.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3ac732390d529d8469b831949c78085b034bff67f584559340008d0f6041a049"},
-    {file = "rpds_py-0.17.1-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:93432e747fb07fa567ad9cc7aaadd6e29710e515aabf939dfbed8046041346c6"},
-    {file = "rpds_py-0.17.1-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:7b7d9ca34542099b4e185b3c2a2b2eda2e318a7dbde0b0d83357a6d4421b5296"},
-    {file = "rpds_py-0.17.1-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:0387ce69ba06e43df54e43968090f3626e231e4bc9150e4c3246947567695f68"},
-    {file = "rpds_py-0.17.1.tar.gz", hash = "sha256:0210b2668f24c078307260bf88bdac9d6f1093635df5123789bfee4d8d7fc8e7"},
+    {file = "rpds_py-0.18.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:5b4e7d8d6c9b2e8ee2d55c90b59c707ca59bc30058269b3db7b1f8df5763557e"},
+    {file = "rpds_py-0.18.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c463ed05f9dfb9baebef68048aed8dcdc94411e4bf3d33a39ba97e271624f8f7"},
+    {file = "rpds_py-0.18.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:01e36a39af54a30f28b73096dd39b6802eddd04c90dbe161c1b8dbe22353189f"},
+    {file = "rpds_py-0.18.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d62dec4976954a23d7f91f2f4530852b0c7608116c257833922a896101336c51"},
+    {file = "rpds_py-0.18.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dd18772815d5f008fa03d2b9a681ae38d5ae9f0e599f7dda233c439fcaa00d40"},
+    {file = "rpds_py-0.18.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:923d39efa3cfb7279a0327e337a7958bff00cc447fd07a25cddb0a1cc9a6d2da"},
+    {file = "rpds_py-0.18.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39514da80f971362f9267c600b6d459bfbbc549cffc2cef8e47474fddc9b45b1"},
+    {file = "rpds_py-0.18.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a34d557a42aa28bd5c48a023c570219ba2593bcbbb8dc1b98d8cf5d529ab1434"},
+    {file = "rpds_py-0.18.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:93df1de2f7f7239dc9cc5a4a12408ee1598725036bd2dedadc14d94525192fc3"},
+    {file = "rpds_py-0.18.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:34b18ba135c687f4dac449aa5157d36e2cbb7c03cbea4ddbd88604e076aa836e"},
+    {file = "rpds_py-0.18.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c0b5dcf9193625afd8ecc92312d6ed78781c46ecbf39af9ad4681fc9f464af88"},
+    {file = "rpds_py-0.18.0-cp310-none-win32.whl", hash = "sha256:c4325ff0442a12113a6379af66978c3fe562f846763287ef66bdc1d57925d337"},
+    {file = "rpds_py-0.18.0-cp310-none-win_amd64.whl", hash = "sha256:7223a2a5fe0d217e60a60cdae28d6949140dde9c3bcc714063c5b463065e3d66"},
+    {file = "rpds_py-0.18.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:3a96e0c6a41dcdba3a0a581bbf6c44bb863f27c541547fb4b9711fd8cf0ffad4"},
+    {file = "rpds_py-0.18.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30f43887bbae0d49113cbaab729a112251a940e9b274536613097ab8b4899cf6"},
+    {file = "rpds_py-0.18.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fcb25daa9219b4cf3a0ab24b0eb9a5cc8949ed4dc72acb8fa16b7e1681aa3c58"},
+    {file = "rpds_py-0.18.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d68c93e381010662ab873fea609bf6c0f428b6d0bb00f2c6939782e0818d37bf"},
+    {file = "rpds_py-0.18.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b34b7aa8b261c1dbf7720b5d6f01f38243e9b9daf7e6b8bc1fd4657000062f2c"},
+    {file = "rpds_py-0.18.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2e6d75ab12b0bbab7215e5d40f1e5b738aa539598db27ef83b2ec46747df90e1"},
+    {file = "rpds_py-0.18.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b8612cd233543a3781bc659c731b9d607de65890085098986dfd573fc2befe5"},
+    {file = "rpds_py-0.18.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:aec493917dd45e3c69d00a8874e7cbed844efd935595ef78a0f25f14312e33c6"},
+    {file = "rpds_py-0.18.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:661d25cbffaf8cc42e971dd570d87cb29a665f49f4abe1f9e76be9a5182c4688"},
+    {file = "rpds_py-0.18.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1df3659d26f539ac74fb3b0c481cdf9d725386e3552c6fa2974f4d33d78e544b"},
+    {file = "rpds_py-0.18.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a1ce3ba137ed54f83e56fb983a5859a27d43a40188ba798993812fed73c70836"},
+    {file = "rpds_py-0.18.0-cp311-none-win32.whl", hash = "sha256:69e64831e22a6b377772e7fb337533c365085b31619005802a79242fee620bc1"},
+    {file = "rpds_py-0.18.0-cp311-none-win_amd64.whl", hash = "sha256:998e33ad22dc7ec7e030b3df701c43630b5bc0d8fbc2267653577e3fec279afa"},
+    {file = "rpds_py-0.18.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:7f2facbd386dd60cbbf1a794181e6aa0bd429bd78bfdf775436020172e2a23f0"},
+    {file = "rpds_py-0.18.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1d9a5be316c15ffb2b3c405c4ff14448c36b4435be062a7f578ccd8b01f0c4d8"},
+    {file = "rpds_py-0.18.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cd5bf1af8efe569654bbef5a3e0a56eca45f87cfcffab31dd8dde70da5982475"},
+    {file = "rpds_py-0.18.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5417558f6887e9b6b65b4527232553c139b57ec42c64570569b155262ac0754f"},
+    {file = "rpds_py-0.18.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:56a737287efecafc16f6d067c2ea0117abadcd078d58721f967952db329a3e5c"},
+    {file = "rpds_py-0.18.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8f03bccbd8586e9dd37219bce4d4e0d3ab492e6b3b533e973fa08a112cb2ffc9"},
+    {file = "rpds_py-0.18.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4457a94da0d5c53dc4b3e4de1158bdab077db23c53232f37a3cb7afdb053a4e3"},
+    {file = "rpds_py-0.18.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0ab39c1ba9023914297dd88ec3b3b3c3f33671baeb6acf82ad7ce883f6e8e157"},
+    {file = "rpds_py-0.18.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9d54553c1136b50fd12cc17e5b11ad07374c316df307e4cfd6441bea5fb68496"},
+    {file = "rpds_py-0.18.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0af039631b6de0397ab2ba16eaf2872e9f8fca391b44d3d8cac317860a700a3f"},
+    {file = "rpds_py-0.18.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:84ffab12db93b5f6bad84c712c92060a2d321b35c3c9960b43d08d0f639d60d7"},
+    {file = "rpds_py-0.18.0-cp312-none-win32.whl", hash = "sha256:685537e07897f173abcf67258bee3c05c374fa6fff89d4c7e42fb391b0605e98"},
+    {file = "rpds_py-0.18.0-cp312-none-win_amd64.whl", hash = "sha256:e003b002ec72c8d5a3e3da2989c7d6065b47d9eaa70cd8808b5384fbb970f4ec"},
+    {file = "rpds_py-0.18.0-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:08f9ad53c3f31dfb4baa00da22f1e862900f45908383c062c27628754af2e88e"},
+    {file = "rpds_py-0.18.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c0013fe6b46aa496a6749c77e00a3eb07952832ad6166bd481c74bda0dcb6d58"},
+    {file = "rpds_py-0.18.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e32a92116d4f2a80b629778280103d2a510a5b3f6314ceccd6e38006b5e92dcb"},
+    {file = "rpds_py-0.18.0-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e541ec6f2ec456934fd279a3120f856cd0aedd209fc3852eca563f81738f6861"},
+    {file = "rpds_py-0.18.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bed88b9a458e354014d662d47e7a5baafd7ff81c780fd91584a10d6ec842cb73"},
+    {file = "rpds_py-0.18.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2644e47de560eb7bd55c20fc59f6daa04682655c58d08185a9b95c1970fa1e07"},
+    {file = "rpds_py-0.18.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e8916ae4c720529e18afa0b879473049e95949bf97042e938530e072fde061d"},
+    {file = "rpds_py-0.18.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:465a3eb5659338cf2a9243e50ad9b2296fa15061736d6e26240e713522b6235c"},
+    {file = "rpds_py-0.18.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:ea7d4a99f3b38c37eac212dbd6ec42b7a5ec51e2c74b5d3223e43c811609e65f"},
+    {file = "rpds_py-0.18.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:67071a6171e92b6da534b8ae326505f7c18022c6f19072a81dcf40db2638767c"},
+    {file = "rpds_py-0.18.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:41ef53e7c58aa4ef281da975f62c258950f54b76ec8e45941e93a3d1d8580594"},
+    {file = "rpds_py-0.18.0-cp38-none-win32.whl", hash = "sha256:fdea4952db2793c4ad0bdccd27c1d8fdd1423a92f04598bc39425bcc2b8ee46e"},
+    {file = "rpds_py-0.18.0-cp38-none-win_amd64.whl", hash = "sha256:7cd863afe7336c62ec78d7d1349a2f34c007a3cc6c2369d667c65aeec412a5b1"},
+    {file = "rpds_py-0.18.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:5307def11a35f5ae4581a0b658b0af8178c65c530e94893345bebf41cc139d33"},
+    {file = "rpds_py-0.18.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:77f195baa60a54ef9d2de16fbbfd3ff8b04edc0c0140a761b56c267ac11aa467"},
+    {file = "rpds_py-0.18.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:39f5441553f1c2aed4de4377178ad8ff8f9d733723d6c66d983d75341de265ab"},
+    {file = "rpds_py-0.18.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9a00312dea9310d4cb7dbd7787e722d2e86a95c2db92fbd7d0155f97127bcb40"},
+    {file = "rpds_py-0.18.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8f2fc11e8fe034ee3c34d316d0ad8808f45bc3b9ce5857ff29d513f3ff2923a1"},
+    {file = "rpds_py-0.18.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:586f8204935b9ec884500498ccc91aa869fc652c40c093bd9e1471fbcc25c022"},
+    {file = "rpds_py-0.18.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ddc2f4dfd396c7bfa18e6ce371cba60e4cf9d2e5cdb71376aa2da264605b60b9"},
+    {file = "rpds_py-0.18.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5ddcba87675b6d509139d1b521e0c8250e967e63b5909a7e8f8944d0f90ff36f"},
+    {file = "rpds_py-0.18.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7bd339195d84439cbe5771546fe8a4e8a7a045417d8f9de9a368c434e42a721e"},
+    {file = "rpds_py-0.18.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:d7c36232a90d4755b720fbd76739d8891732b18cf240a9c645d75f00639a9024"},
+    {file = "rpds_py-0.18.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:6b0817e34942b2ca527b0e9298373e7cc75f429e8da2055607f4931fded23e20"},
+    {file = "rpds_py-0.18.0-cp39-none-win32.whl", hash = "sha256:99f70b740dc04d09e6b2699b675874367885217a2e9f782bdf5395632ac663b7"},
+    {file = "rpds_py-0.18.0-cp39-none-win_amd64.whl", hash = "sha256:6ef687afab047554a2d366e112dd187b62d261d49eb79b77e386f94644363294"},
+    {file = "rpds_py-0.18.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:ad36cfb355e24f1bd37cac88c112cd7730873f20fb0bdaf8ba59eedf8216079f"},
+    {file = "rpds_py-0.18.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:36b3ee798c58ace201289024b52788161e1ea133e4ac93fba7d49da5fec0ef9e"},
+    {file = "rpds_py-0.18.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8a2f084546cc59ea99fda8e070be2fd140c3092dc11524a71aa8f0f3d5a55ca"},
+    {file = "rpds_py-0.18.0-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e4461d0f003a0aa9be2bdd1b798a041f177189c1a0f7619fe8c95ad08d9a45d7"},
+    {file = "rpds_py-0.18.0-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8db715ebe3bb7d86d77ac1826f7d67ec11a70dbd2376b7cc214199360517b641"},
+    {file = "rpds_py-0.18.0-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:793968759cd0d96cac1e367afd70c235867831983f876a53389ad869b043c948"},
+    {file = "rpds_py-0.18.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66e6a3af5a75363d2c9a48b07cb27c4ea542938b1a2e93b15a503cdfa8490795"},
+    {file = "rpds_py-0.18.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6ef0befbb5d79cf32d0266f5cff01545602344eda89480e1dd88aca964260b18"},
+    {file = "rpds_py-0.18.0-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:1d4acf42190d449d5e89654d5c1ed3a4f17925eec71f05e2a41414689cda02d1"},
+    {file = "rpds_py-0.18.0-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:a5f446dd5055667aabaee78487f2b5ab72e244f9bc0b2ffebfeec79051679984"},
+    {file = "rpds_py-0.18.0-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:9dbbeb27f4e70bfd9eec1be5477517365afe05a9b2c441a0b21929ee61048124"},
+    {file = "rpds_py-0.18.0-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:22806714311a69fd0af9b35b7be97c18a0fc2826e6827dbb3a8c94eac6cf7eeb"},
+    {file = "rpds_py-0.18.0-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:b34ae4636dfc4e76a438ab826a0d1eed2589ca7d9a1b2d5bb546978ac6485461"},
+    {file = "rpds_py-0.18.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c8370641f1a7f0e0669ddccca22f1da893cef7628396431eb445d46d893e5cd"},
+    {file = "rpds_py-0.18.0-pp38-pypy38_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c8362467a0fdeccd47935f22c256bec5e6abe543bf0d66e3d3d57a8fb5731863"},
+    {file = "rpds_py-0.18.0-pp38-pypy38_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:11a8c85ef4a07a7638180bf04fe189d12757c696eb41f310d2426895356dcf05"},
+    {file = "rpds_py-0.18.0-pp38-pypy38_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b316144e85316da2723f9d8dc75bada12fa58489a527091fa1d5a612643d1a0e"},
+    {file = "rpds_py-0.18.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cf1ea2e34868f6fbf070e1af291c8180480310173de0b0c43fc38a02929fc0e3"},
+    {file = "rpds_py-0.18.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e546e768d08ad55b20b11dbb78a745151acbd938f8f00d0cfbabe8b0199b9880"},
+    {file = "rpds_py-0.18.0-pp38-pypy38_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:4901165d170a5fde6f589acb90a6b33629ad1ec976d4529e769c6f3d885e3e80"},
+    {file = "rpds_py-0.18.0-pp38-pypy38_pp73-musllinux_1_2_i686.whl", hash = "sha256:618a3d6cae6ef8ec88bb76dd80b83cfe415ad4f1d942ca2a903bf6b6ff97a2da"},
+    {file = "rpds_py-0.18.0-pp38-pypy38_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ed4eb745efbff0a8e9587d22a84be94a5eb7d2d99c02dacf7bd0911713ed14dd"},
+    {file = "rpds_py-0.18.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:6c81e5f372cd0dc5dc4809553d34f832f60a46034a5f187756d9b90586c2c307"},
+    {file = "rpds_py-0.18.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:43fbac5f22e25bee1d482c97474f930a353542855f05c1161fd804c9dc74a09d"},
+    {file = "rpds_py-0.18.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6d7faa6f14017c0b1e69f5e2c357b998731ea75a442ab3841c0dbbbfe902d2c4"},
+    {file = "rpds_py-0.18.0-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:08231ac30a842bd04daabc4d71fddd7e6d26189406d5a69535638e4dcb88fe76"},
+    {file = "rpds_py-0.18.0-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:044a3e61a7c2dafacae99d1e722cc2d4c05280790ec5a05031b3876809d89a5c"},
+    {file = "rpds_py-0.18.0-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3f26b5bd1079acdb0c7a5645e350fe54d16b17bfc5e71f371c449383d3342e17"},
+    {file = "rpds_py-0.18.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:482103aed1dfe2f3b71a58eff35ba105289b8d862551ea576bd15479aba01f66"},
+    {file = "rpds_py-0.18.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1374f4129f9bcca53a1bba0bb86bf78325a0374577cf7e9e4cd046b1e6f20e24"},
+    {file = "rpds_py-0.18.0-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:635dc434ff724b178cb192c70016cc0ad25a275228f749ee0daf0eddbc8183b1"},
+    {file = "rpds_py-0.18.0-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:bc362ee4e314870a70f4ae88772d72d877246537d9f8cb8f7eacf10884862432"},
+    {file = "rpds_py-0.18.0-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:4832d7d380477521a8c1644bbab6588dfedea5e30a7d967b5fb75977c45fd77f"},
+    {file = "rpds_py-0.18.0.tar.gz", hash = "sha256:42821446ee7a76f5d9f71f9e33a4fb2ffd724bb3e7f93386150b61a43115788d"},
 ]
 
 [[package]]
@@ -3812,13 +3840,13 @@ files = [
 
 [[package]]
 name = "schematic-db"
-version = "0.0.34"
+version = "0.0.41"
 description = ""
 optional = false
 python-versions = ">=3.9,<4.0"
 files = [
-    {file = "schematic_db-0.0.34-py3-none-any.whl", hash = "sha256:fb9194ee9085c12e4044b6f4bdb934807767fbf80e3d792c5e8a65c0a6e43e95"},
-    {file = "schematic_db-0.0.34.tar.gz", hash = "sha256:29e1fb752e330c3319670d4c562f7f54d554c23c47d9ed19910a107438c70169"},
+    {file = "schematic_db-0.0.41-py3-none-any.whl", hash = "sha256:bf8e8a73fb06113431a89a25df15f3eefbe7b40c2cfe149c4e9afa6e6b33fd5b"},
+    {file = "schematic_db-0.0.41.tar.gz", hash = "sha256:cd5ec936cdb4fca203de57aa0c771b2b251c5eec7e0af719c388cad70d8d9f6d"},
 ]
 
 [package.dependencies]
@@ -3831,14 +3859,14 @@ PyYAML = ">=6.0,<7.0"
 requests = ">=2.28.1,<3.0.0"
 SQLAlchemy = ">=2.0.19,<3.0.0"
 SQLAlchemy-Utils = ">=0.41.1,<0.42.0"
-synapseclient = {version = ">=3.0.0,<4.0.0", optional = true, markers = "extra == \"synapse\""}
+synapseclient = {version = ">=4.0.0,<5.0.0", optional = true, markers = "extra == \"synapse\""}
 tenacity = ">=8.1.0,<9.0.0"
 validators = ">=0.20.0,<0.21.0"
 
 [package.extras]
 mysql = ["mysqlclient (>=2.1.1,<3.0.0)"]
 postgres = ["psycopg2-binary (>=2.9.5,<3.0.0)"]
-synapse = ["synapseclient (>=3.0.0,<4.0.0)"]
+synapse = ["synapseclient (>=4.0.0,<5.0.0)"]
 
 [[package]]
 name = "scipy"
@@ -3882,21 +3910,6 @@ dev = ["click", "cython-lint (>=0.12.2)", "doit (>=0.36.0)", "mypy", "pycodestyl
 doc = ["jupytext", "matplotlib (>2)", "myst-nb", "numpydoc", "pooch", "pydata-sphinx-theme (==0.9.0)", "sphinx (!=4.1.0)", "sphinx-design (>=0.2.0)"]
 test = ["asv", "gmpy2", "hypothesis", "mpmath", "pooch", "pytest", "pytest-cov", "pytest-timeout", "pytest-xdist", "scikit-umfpack", "threadpoolctl"]
 
-[[package]]
-name = "secretstorage"
-version = "3.3.3"
-description = "Python bindings to FreeDesktop.org Secret Service API"
-optional = false
-python-versions = ">=3.6"
-files = [
-    {file = "SecretStorage-3.3.3-py3-none-any.whl", hash = "sha256:f356e6628222568e3af06f2eba8df495efa13b3b63081dafd4f7d9a7b7bc9f99"},
-    {file = "SecretStorage-3.3.3.tar.gz", hash = "sha256:2403533ef369eca6d2ba81718576c5e0f564d5cca1b58f73a8b23e7d4eeebd77"},
-]
-
-[package.dependencies]
-cryptography = ">=2.0"
-jeepney = ">=0.6"
-
 [[package]]
 name = "send2trash"
 version = "1.8.2"
@@ -3942,13 +3955,13 @@ files = [
 
 [[package]]
 name = "sniffio"
-version = "1.3.0"
+version = "1.3.1"
 description = "Sniff out which async library your code is running under"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "sniffio-1.3.0-py3-none-any.whl", hash = "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"},
-    {file = "sniffio-1.3.0.tar.gz", hash = "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101"},
+    {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"},
+    {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"},
 ]
 
 [[package]]
@@ -4268,19 +4281,18 @@ Jinja2 = ">=2.0"
 
 [[package]]
 name = "synapseclient"
-version = "3.2.0"
+version = "4.1.1"
 description = "A client for Synapse, a collaborative, open-source research platform that allows teams to share data, track analyses, and collaborate."
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "synapseclient-3.2.0-py3-none-any.whl", hash = "sha256:ec1bb9c3ac2db995be25f6ced08a530a170219d23224d7c9b8a381166905fe6c"},
-    {file = "synapseclient-3.2.0.tar.gz", hash = "sha256:87c91f03dbca7074efd18144325df07db24e07ea92e0b7c8691aaec46c28329a"},
+    {file = "synapseclient-4.1.1-py3-none-any.whl", hash = "sha256:5261a2751ea5757750e0cb2e481ecb2761954e2c0969e7cc5dd1dfff43c3ddcc"},
+    {file = "synapseclient-4.1.1.tar.gz", hash = "sha256:c5e0ac45faed1aa618cd66a0f50bf1a8149a2902a71d07487a0e470250955083"},
 ]
 
 [package.dependencies]
 deprecated = ">=1.2.4,<2.0"
-keyring = ">=15,<23.5"
-"keyrings.alt" = {version = "3.1", markers = "sys_platform == \"linux\""}
+nest-asyncio = ">=1.6.0,<1.7.0"
 opentelemetry-api = ">=1.21.0,<1.22.0"
 opentelemetry-exporter-otlp-proto-http = ">=1.21.0,<1.22.0"
 opentelemetry-sdk = ">=1.21.0,<1.22.0"
@@ -4289,11 +4301,11 @@ urllib3 = ">=1.26.18,<2"
 
 [package.extras]
 boto3 = ["boto3 (>=1.7.0,<2.0)"]
-dev = ["black", "flake8 (>=3.7.0,<4.0)", "func-timeout (>=4.3,<5.0)", "pre-commit", "pytest (>=6.0.0,<7.0)", "pytest-mock (>=3.0,<4.0)", "pytest-rerunfailures (>=12.0,<13.0)", "pytest-xdist[psutil] (>=2.2,<3.0.0)"]
-docs = ["sphinx (>=4.0,<5.0)", "sphinx-argparse (>=0.2,<0.3)", "sphinx-rtd-theme"]
-pandas = ["pandas (>=1.5,<2.1)"]
+dev = ["black", "flake8 (>=3.7.0,<4.0)", "func-timeout (>=4.3,<5.0)", "pre-commit", "pytest (>=6.0.0,<7.0)", "pytest-asyncio (>=0.19,<1.0)", "pytest-cov (>=4.1.0,<4.2.0)", "pytest-mock (>=3.0,<4.0)", "pytest-rerunfailures (>=12.0,<13.0)", "pytest-socket (>=0.6.0,<0.7.0)", "pytest-xdist[psutil] (>=2.2,<3.0.0)"]
+docs = ["markdown-include (>=0.8.1,<0.9.0)", "mkdocs (>=1.5.3)", "mkdocs-material (>=9.4.14)", "mkdocs-open-in-new-tab (>=1.0.3,<1.1.0)", "mkdocstrings (>=0.24.0)", "mkdocstrings-python (>=1.7.5)", "termynal (>=0.11.1)"]
+pandas = ["pandas (>=1.5,<3.0)"]
 pysftp = ["pysftp (>=0.2.8,<0.3)"]
-tests = ["flake8 (>=3.7.0,<4.0)", "func-timeout (>=4.3,<5.0)", "pytest (>=6.0.0,<7.0)", "pytest-mock (>=3.0,<4.0)", "pytest-rerunfailures (>=12.0,<13.0)", "pytest-xdist[psutil] (>=2.2,<3.0.0)"]
+tests = ["flake8 (>=3.7.0,<4.0)", "func-timeout (>=4.3,<5.0)", "pytest (>=6.0.0,<7.0)", "pytest-asyncio (>=0.19,<1.0)", "pytest-cov (>=4.1.0,<4.2.0)", "pytest-mock (>=3.0,<4.0)", "pytest-rerunfailures (>=12.0,<13.0)", "pytest-socket (>=0.6.0,<0.7.0)", "pytest-xdist[psutil] (>=2.2,<3.0.0)"]
 
 [[package]]
 name = "tabulate"
@@ -4386,13 +4398,13 @@ files = [
 
 [[package]]
 name = "tomlkit"
-version = "0.12.3"
+version = "0.12.4"
 description = "Style preserving TOML library"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "tomlkit-0.12.3-py3-none-any.whl", hash = "sha256:b0a645a9156dc7cb5d3a1f0d4bab66db287fcb8e0430bdd4664a095ea16414ba"},
-    {file = "tomlkit-0.12.3.tar.gz", hash = "sha256:75baf5012d06501f07bee5bf8e801b9f343e7aac5a92581f20f80ce632e6b5a4"},
+    {file = "tomlkit-0.12.4-py3-none-any.whl", hash = "sha256:5cd82d48a3dd89dee1f9d64420aa20ae65cfbd00668d6f094d7578a78efbb77b"},
+    {file = "tomlkit-0.12.4.tar.gz", hash = "sha256:7ca1cfc12232806517a8515047ba66a19369e71edf2439d0f5824f91032b6cc3"},
 ]
 
 [[package]]
@@ -4428,13 +4440,13 @@ files = [
 
 [[package]]
 name = "tqdm"
-version = "4.66.1"
+version = "4.66.2"
 description = "Fast, Extensible Progress Meter"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "tqdm-4.66.1-py3-none-any.whl", hash = "sha256:d302b3c5b53d47bce91fea46679d9c3c6508cf6332229aa1e7d8653723793386"},
-    {file = "tqdm-4.66.1.tar.gz", hash = "sha256:d88e651f9db8d8551a62556d3cff9e3034274ca5d66e93197cf2490e2dcb69c7"},
+    {file = "tqdm-4.66.2-py3-none-any.whl", hash = "sha256:1ee4f8a893eb9bef51c6e35730cebf234d5d0b6bd112b0271e10ed7c24a02bd9"},
+    {file = "tqdm-4.66.2.tar.gz", hash = "sha256:6cd52cdf0fef0e0f543299cfc96fec90d7b8a7e88745f411ec33eb44d5ed3531"},
 ]
 
 [package.dependencies]
@@ -4500,13 +4512,13 @@ typing-extensions = ">=3.7.4"
 
 [[package]]
 name = "tzdata"
-version = "2023.4"
+version = "2024.1"
 description = "Provider of IANA time zone data"
 optional = false
 python-versions = ">=2"
 files = [
-    {file = "tzdata-2023.4-py2.py3-none-any.whl", hash = "sha256:aa3ace4329eeacda5b7beb7ea08ece826c28d761cda36e747cfbf97996d39bf3"},
-    {file = "tzdata-2023.4.tar.gz", hash = "sha256:dd54c94f294765522c77399649b4fefd95522479a664a0cec87f41bebc6148c9"},
+    {file = "tzdata-2024.1-py2.py3-none-any.whl", hash = "sha256:9068bc196136463f5245e51efda838afa15aaeca9903f49050dfa2679db4d252"},
+    {file = "tzdata-2024.1.tar.gz", hash = "sha256:2674120f8d891909751c38abcdfd386ac0a5a1127954fbc332af6b5ceae07efd"},
 ]
 
 [[package]]
@@ -4593,6 +4605,26 @@ decorator = ">=3.4.0"
 [package.extras]
 test = ["flake8 (>=2.4.0)", "isort (>=4.2.2)", "pytest (>=2.2.3)"]
 
+[[package]]
+name = "virtualenv"
+version = "20.25.1"
+description = "Virtual Python Environment builder"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "virtualenv-20.25.1-py3-none-any.whl", hash = "sha256:961c026ac520bac5f69acb8ea063e8a4f071bcc9457b9c1f28f6b085c511583a"},
+    {file = "virtualenv-20.25.1.tar.gz", hash = "sha256:e08e13ecdca7a0bd53798f356d5831434afa5b07b93f0abdf0797b7a06ffe197"},
+]
+
+[package.dependencies]
+distlib = ">=0.3.7,<1"
+filelock = ">=3.12.2,<4"
+platformdirs = ">=3.9.1,<5"
+
+[package.extras]
+docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"]
+test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"]
+
 [[package]]
 name = "wcwidth"
 version = "0.2.13"
@@ -4648,17 +4680,20 @@ test = ["websockets"]
 
 [[package]]
 name = "werkzeug"
-version = "2.1.2"
+version = "2.3.8"
 description = "The comprehensive WSGI web application library."
 optional = true
-python-versions = ">=3.7"
+python-versions = ">=3.8"
 files = [
-    {file = "Werkzeug-2.1.2-py3-none-any.whl", hash = "sha256:72a4b735692dd3135217911cbeaa1be5fa3f62bffb8745c5215420a03dc55255"},
-    {file = "Werkzeug-2.1.2.tar.gz", hash = "sha256:1ce08e8093ed67d638d63879fd1ba3735817f7a80de3674d293f5984f25fb6e6"},
+    {file = "werkzeug-2.3.8-py3-none-any.whl", hash = "sha256:bba1f19f8ec89d4d607a3bd62f1904bd2e609472d93cd85e9d4e178f472c3748"},
+    {file = "werkzeug-2.3.8.tar.gz", hash = "sha256:554b257c74bbeb7a0d254160a4f8ffe185243f52a52035060b761ca62d977f03"},
 ]
 
+[package.dependencies]
+MarkupSafe = ">=2.1.1"
+
 [package.extras]
-watchdog = ["watchdog"]
+watchdog = ["watchdog (>=2.3)"]
 
 [[package]]
 name = "widgetsnbextension"
@@ -4766,10 +4801,10 @@ docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.link
 testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy (>=0.9.1)", "pytest-ruff"]
 
 [extras]
-api = ["Flask", "Flask-Cors", "Jinja2", "connexion"]
+api = ["Flask", "Flask-Cors", "Jinja2", "connexion", "pyopenssl"]
 aws = ["uWSGI"]
 
 [metadata]
 lock-version = "2.0"
 python-versions = ">=3.9.0,<3.11"
-content-hash = "fd6dbe19c700a80fedc739825f3c26d63476360d1a5f605f8abc2726ce9c3157"
+content-hash = "a5d0f2dd9e8e4346048dd8f2cd798a092d375714a5a3e1c7d86cfaf23d6e96fc"
diff --git a/pyproject.toml b/pyproject.toml
index 90aa08de7..7c2bb991c 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -54,30 +54,29 @@ pygsheets = "^2.0.4"
 PyYAML = "^6.0.0"
 rdflib = "^6.0.0"
 setuptools = "^66.0.0"
-synapseclient = "^3.2.0"
+synapseclient = "^4.1.0"
 tenacity = "^8.0.1"
 toml = "^0.10.2"
 great-expectations = "^0.15.0"
 sphinx-click = "^4.0.0"
-MarkupSafe = "2.1.0"
 itsdangerous = "^2.0.0"
 openpyxl = "^3.0.9"
 "backports.zoneinfo" = {markers = "python_version < \"3.9\"", version = "^0.2.1"}
 pdoc = "^12.2.0"
 dateparser = "^1.1.4"
 pandarallel = "^1.6.4"
-schematic-db = {version = "0.0.34", extras = ["synapse"]}
-pyopenssl = "^23.0.0"
+schematic-db = {version = "0.0.41", extras = ["synapse"]}
+pyopenssl = {version = "^23.0.0", optional = true}
 typing-extensions = "<4.6.0"
 dataclasses-json = "^0.6.1"
 connexion = {extras = ["swagger-ui"], version = "^2.8.0", optional = true}
-Flask = {version = "^2.0.0", optional = true}
+Flask = {version = "2.1.3", optional = true}
 Flask-Cors = {version = "^3.0.10", optional = true}
 uWSGI = {version = "^2.0.21", optional = true}
 Jinja2 = {version = ">2.11.3", optional = true}
 
 [tool.poetry.extras]
-api = ["connexion", "Flask", "Flask-Cors", "Jinja2"]
+api = ["connexion", "Flask", "Flask-Cors", "Jinja2", "pyopenssl"]
 aws = ["uWSGI"]
 
 
@@ -88,10 +87,11 @@ pytest-mock = "^3.5.1"
 pytest-rerunfailures = "^12.0"
 flake8 = "^6.0.0"
 python-dotenv = "^0.21.0"
-black = "^23.7.0"
+black = "^23.7.0" #If the version spec of black is changed here, the version specified in .pre-commit-config.yaml should be updated to match
 mypy = "^1.4.1"
 pylint = "^2.16.1"
 pytest-xdist = "^3.5.0"
+pre-commit = "^3.6.2"
 
 [tool.poetry.group.aws]
 optional = true
diff --git a/schematic/__init__.py b/schematic/__init__.py
index de46c4fe4..1b4ec14fe 100644
--- a/schematic/__init__.py
+++ b/schematic/__init__.py
@@ -1,45 +1,2 @@
-import sys
-import logging
-
-import click
-import click_log
-
 from schematic.configuration.configuration import CONFIG
 from schematic.loader import LOADER
-from schematic.utils.google_api_utils import download_creds_file
-from schematic.utils.cli_utils import query_dict
-from schematic.help import init_command
-
-logging.basicConfig(
-    format=("%(levelname)s: [%(asctime)s] %(name)s" " - %(message)s"),
-    level=logging.WARNING,
-    datefmt="%Y-%m-%d %H:%M:%S",
-)
-
-
-# Suppress INFO-level logging from some dependencies
-logging.getLogger("keyring").setLevel(logging.ERROR)
-logging.getLogger("rdflib").setLevel(logging.ERROR)
-logging.getLogger("Synapse storage").setLevel(logging.INFO)
-
-logger = logging.getLogger(__name__)
-click_log.basic_config(logger)
-
-
-@click.command("init", short_help=query_dict(init_command, ("init", "short_help")))
-@click_log.simple_verbosity_option(logger)
-@click.option(
-    "-c", "--config", required=True, help=query_dict(init_command, ("init", "config"))
-)
-def init(config):
-    """Initialize authentication for schematic."""
-    try:
-        logger.debug(f"Loading config file contents in '{config}'")
-        CONFIG.load_config(config)
-    except ValueError as exc:
-        logger.error("'--config' not provided or environment variable not set.")
-        logger.exception(exc)
-        sys.exit(1)
-
-    # download credentials file based on selected mode of authentication
-    download_creds_file()
diff --git a/schematic/__main__.py b/schematic/__main__.py
index fa0ee9a23..22216f273 100644
--- a/schematic/__main__.py
+++ b/schematic/__main__.py
@@ -1,5 +1,4 @@
 #!/usr/bin/env python
-
 import logging
 import click
 import click_log
@@ -14,7 +13,6 @@
 from schematic.visualization.commands import (
     viz as viz_cli,
 )  # viz generation commands
-from schematic import init as init_cli  # schematic initialization commands
 
 logger = logging.getLogger()
 click_log.basic_config(logger)
@@ -34,7 +32,6 @@ def main():
     logger.debug("Existing sub-commands need to be used with schematic.")
 
 
-main.add_command(init_cli)  # add init commands
 main.add_command(manifest_cli)  # add manifest commands
 main.add_command(model_cli)  # add model commands
 main.add_command(schema_cli)  # add schema commands
diff --git a/schematic/help.py b/schematic/help.py
index 60f25a2c5..7bf8c29a0 100644
--- a/schematic/help.py
+++ b/schematic/help.py
@@ -105,6 +105,16 @@
                 "Migration information for testing purposes will be logged to the INFO level."
             ),
         },
+        "download": {
+            "short_help": ("Function to download manifest from asset store (Synapse)."),
+            "dataset_id": (
+                "Specify the synID of a dataset folder on Synapse. If there is an exisiting manifest already present "
+                "in that folder, then it will be pulled with the existing annotations for further annotation/modification. "
+            ),
+            "new_manifest_name": (
+                "Specify the new name to download the manifest file as."
+            ),
+        },
     }
 }
 
@@ -161,6 +171,9 @@
                 "class_label, display_label, display_name, default, class_label. When true annotations and table columns will be uploaded with the display name formatting with blacklisted characters removed. "
                 "To use for tables, use in conjunction with the use_schema_label flag."
             ),
+            "file_annotations_upload": (
+                "This is a boolean flag. Default to True. If False, annotations will not be added to files during submission."
+            ),
             "data_model_labels": DATA_MODEL_LABELS_HELP,
         },
         "validate": {
@@ -208,17 +221,6 @@
     }
 }
 
-
-# `schematic init` command description
-init_command = {
-    "init": {
-        "short_help": ("Initialize authentication for schematic."),
-        "config": (
-            "Specify the path to the `config.yml` using this option. This is a required argument."
-        ),
-    }
-}
-
 viz_commands = {
     "visualization": {
         "config": (
diff --git a/schematic/manifest/commands.py b/schematic/manifest/commands.py
index be648b2b4..6b212239f 100644
--- a/schematic/manifest/commands.py
+++ b/schematic/manifest/commands.py
@@ -1,4 +1,6 @@
+import json
 import os
+import pandas as pd
 import logging
 from pathlib import Path
 import sys
@@ -13,6 +15,7 @@
 from schematic.utils.schema_utils import DisplayLabelType
 from schematic.utils.cli_utils import log_value_from_config, query_dict, parse_syn_ids
 from schematic.utils.google_api_utils import export_manifest_csv
+
 from schematic.help import manifest_commands
 
 from schematic.store.synapse import SynapseStorage
@@ -154,7 +157,7 @@ def get_manifest(
 
     # Generate graph
     logger.info("Generating data model graph.")
-    graph_data_model = data_model_grapher.generate_data_model_graph()
+    graph_data_model = data_model_grapher.graph
 
     def create_single_manifest(data_type, output_csv=None, output_xlsx=None):
         # create object of type ManifestGenerator
@@ -318,5 +321,47 @@ def migrate_manifests(
             synStore.move_entities_to_new_project(
                 project, archive_project, return_entities, dry_run
             )
-
     return
+
+
+@manifest.command(
+    "download",
+    short_help=query_dict(manifest_commands, ("manifest", "download", "short_help")),
+)
+@click_log.simple_verbosity_option(logger)
+# define the optional arguments
+@click.option(
+    "-d",
+    "--dataset_id",
+    help=query_dict(manifest_commands, ("manifest", "download", "dataset_id")),
+)
+@click.option(
+    "-nmn",
+    "--new_manifest_name",
+    default="",
+    help=query_dict(manifest_commands, ("manifest", "download", "new_manifest_name")),
+)
+@click.pass_obj
+def download_manifest(ctx, dataset_id, new_manifest_name):
+    master_fileview = CONFIG["synapse"]["master_fileview"]
+
+    # use Synapse Storage
+    store = SynapseStorage()
+
+    # download existing file
+    manifest_data = store.getDatasetManifest(
+        datasetId=dataset_id, downloadFile=True, newManifestName=new_manifest_name
+    )
+
+    if not manifest_data:
+        logger.error(
+            "'Dataset_id provided is not able to return a manifest, please check that the id is the parent folder containing the manifest."
+        )
+        sys.exit(1)
+
+    # return local file path
+    manifest_local_file_path = manifest_data["path"]
+    logger.info(
+        f"The manifest has been downloaded to the following location: {manifest_local_file_path}"
+    )
+    return manifest_local_file_path
diff --git a/schematic/manifest/generator.py b/schematic/manifest/generator.py
index dea6a44d6..5aced191e 100644
--- a/schematic/manifest/generator.py
+++ b/schematic/manifest/generator.py
@@ -1140,7 +1140,7 @@ def _create_requests_body(
             )
             if isinstance(validation_rules, dict):
                 validation_rules = extract_component_validation_rules(
-                    validation_rules=validation_rules, manifest_component=self.root
+                    validation_rules_dict=validation_rules, manifest_component=self.root
                 )
 
             # Add regex match validaiton rule to Google Sheets.
@@ -1677,7 +1677,7 @@ def create_manifests(
         data_model_grapher = DataModelGraph(parsed_data_model, data_model_labels)
 
         # Generate graph
-        graph_data_model = data_model_grapher.generate_data_model_graph()
+        graph_data_model = data_model_grapher.graph
 
         # Gather all returned result urls
         all_results = []
diff --git a/schematic/models/GE_Helpers.py b/schematic/models/GE_Helpers.py
index 5b356999e..d73354f31 100644
--- a/schematic/models/GE_Helpers.py
+++ b/schematic/models/GE_Helpers.py
@@ -38,11 +38,37 @@
     rule_in_rule_list,
     np_array_to_str_list,
     iterable_to_str_list,
+    required_is_only_rule,
 )
 
 logger = logging.getLogger(__name__)
 
 
+# List of modifiers that users can add to a rule, that arent rules themselves.
+# as additional modifiers are added will need to update this list
+
+RULE_MODIFIERS = ["error", "warning", "strict", "like", "set", "value"]
+VALIDATION_EXPECTATION = {
+    "int": "expect_column_values_to_be_in_type_list",
+    "float": "expect_column_values_to_be_in_type_list",
+    "str": "expect_column_values_to_be_of_type",
+    "num": "expect_column_values_to_be_in_type_list",
+    "date": "expect_column_values_to_be_dateutil_parseable",
+    "recommended": "expect_column_values_to_not_be_null",
+    "protectAges": "expect_column_values_to_be_between",
+    "unique": "expect_column_values_to_be_unique",
+    "inRange": "expect_column_values_to_be_between",
+    "IsNA": "expect_column_values_to_match_regex_list",
+    # To be implemented rules with possible expectations
+    # "list": "expect_column_values_to_not_match_regex_list",
+    # "regex": "expect_column_values_to_match_regex",
+    # "url": "expect_column_values_to_be_valid_urls",
+    # "matchAtLeastOne": "expect_foreign_keys_in_column_a_to_exist_in_column_b",
+    # "matchExactlyOne": "expect_foreign_keys_in_column_a_to_exist_in_column_b",
+    # "matchNone": "expect_compound_columns_to_be_unique",
+}
+
+
 class GreatExpectationsHelpers(object):
     """
     Great Expectations helper class
@@ -134,24 +160,6 @@ def build_expectation_suite(
             saves expectation suite and identifier to self
 
         """
-        validation_expectation = {
-            "int": "expect_column_values_to_be_in_type_list",
-            "float": "expect_column_values_to_be_in_type_list",
-            "str": "expect_column_values_to_be_of_type",
-            "num": "expect_column_values_to_be_in_type_list",
-            "date": "expect_column_values_to_be_dateutil_parseable",
-            "recommended": "expect_column_values_to_not_match_regex_list",
-            "protectAges": "expect_column_values_to_be_between",
-            "unique": "expect_column_values_to_be_unique",
-            "inRange": "expect_column_values_to_be_between",
-            "IsNA": "expect_column_values_to_match_regex_list",
-            # To be implemented rules with possible expectations
-            # "list": "expect_column_values_to_not_match_regex_list",
-            # "regex": "expect_column_values_to_match_regex",
-            # "url": "expect_column_values_to_be_valid_urls",
-            # "matchAtLeastOne": "expect_foreign_keys_in_column_a_to_exist_in_column_b",
-            # "matchExactlyOne": "expect_foreign_keys_in_column_a_to_exist_in_column_b",
-        }
 
         # create blank expectation suite
         self.expectation_suite_name = "Manifest_test_suite"
@@ -177,14 +185,21 @@ def build_expectation_suite(
                 if isinstance(validation_rules, dict):
                     validation_rules = extract_component_validation_rules(
                         manifest_component=self.manifest["Component"][0],
-                        validation_rules=validation_rules,
+                        validation_rules_dict=validation_rules,
                     )
                 # iterate through all validation rules for an attribute
                 for rule in validation_rules:
                     base_rule = rule.split(" ")[0]
 
                     # check if rule has an implemented expectation
-                    if rule_in_rule_list(rule, self.unimplemented_expectations):
+                    if rule_in_rule_list(
+                        rule, self.unimplemented_expectations
+                    ) or required_is_only_rule(
+                        rule=rule,
+                        attribute=col,
+                        rule_modifiers=RULE_MODIFIERS,
+                        validation_expectation=VALIDATION_EXPECTATION,
+                    ):
                         continue
 
                     args["column"] = col
@@ -254,7 +269,6 @@ def build_expectation_suite(
 
                     elif base_rule == ("recommended"):
                         args["mostly"] = 0.0000000001
-                        args["regex_list"] = ["^$"]
                         meta = {
                             "notes": {
                                 "format": "markdown",
@@ -328,7 +342,7 @@ def build_expectation_suite(
                         rule=rule,
                         args=args,
                         meta=meta,
-                        validation_expectation=validation_expectation,
+                        validation_expectation=VALIDATION_EXPECTATION,
                     )
 
         self.context.update_expectation_suite(
@@ -368,7 +382,7 @@ def add_expectation(
         # Create an Expectation
         expectation_configuration = ExpectationConfiguration(
             # Name of expectation type being added
-            expectation_type=validation_expectation[rule.split(" ")[0]],
+            expectation_type=VALIDATION_EXPECTATION[rule.split(" ")[0]],
             # add arguments and meta message
             kwargs={**args},
             meta={**meta},
@@ -511,7 +525,7 @@ def generate_errors(
                         val_rule=rule,
                         attribute_name=errColumn,
                         row_num=np_array_to_str_list(np.array(indices) + 2),
-                        error_val=iterable_to_str_list(values),
+                        invalid_entry=iterable_to_str_list(values),
                         dmge=self.dmge,
                     )
                     if vr_errors:
diff --git a/schematic/models/commands.py b/schematic/models/commands.py
index 81a4a3286..2119291b8 100644
--- a/schematic/models/commands.py
+++ b/schematic/models/commands.py
@@ -96,6 +96,13 @@ def model(ctx, config):  # use as `schematic model ...`
     is_flag=True,
     help=query_dict(model_commands, ("model", "validate", "restrict_rules")),
 )
+@click.option(
+    "--file_annotations_upload/--no-file_annotations_upload",
+    "-fa/-no-fa",
+    default=True,
+    is_flag=True,
+    help=query_dict(model_commands, ("model", "submit", "file_annotations_upload")),
+)
 @click.option(
     "-ps",
     "--project_scope",
@@ -147,6 +154,7 @@ def submit_manifest(
     data_model_labels,
     table_column_names,
     annotation_keys,
+    file_annotations_upload: bool,
 ):
     """
     Running CLI with manifest validation (optional) and submission options.
@@ -162,7 +170,6 @@ def submit_manifest(
     )
 
     manifest_id = metadata_model.submit_metadata_manifest(
-        path_to_json_ld=jsonld,
         manifest_path=manifest_path,
         dataset_id=dataset_id,
         validate_component=validate_component,
@@ -173,6 +180,7 @@ def submit_manifest(
         table_manipulation=table_manipulation,
         table_column_names=table_column_names,
         annotation_keys=annotation_keys,
+        file_annotations_upload=file_annotations_upload,
     )
 
     if manifest_id:
diff --git a/schematic/models/metadata.py b/schematic/models/metadata.py
index c57e9d79f..4bb00e37b 100644
--- a/schematic/models/metadata.py
+++ b/schematic/models/metadata.py
@@ -53,7 +53,9 @@ def __init__(
             f"Initializing DataModelGraphExplorer object from {inputMModelLocation} schema."
         )
 
+        # self.inputMModelLocation remains for backwards compatibility
         self.inputMModelLocation = inputMModelLocation
+        self.path_to_json_ld = inputMModelLocation
 
         data_model_parser = DataModelParser(path_to_data_model=self.inputMModelLocation)
         # Parse Model
@@ -63,7 +65,7 @@ def __init__(
         data_model_grapher = DataModelGraph(parsed_data_model, data_model_labels)
 
         # Generate graph
-        self.graph_data_model = data_model_grapher.generate_data_model_graph()
+        self.graph_data_model = data_model_grapher.graph
 
         self.dmge = DataModelGraphExplorer(self.graph_data_model)
 
@@ -233,6 +235,7 @@ def validateModelManifest(
         manifest = load_df(
             manifestPath,
             preserve_raw_input=False,
+            allow_na_values=True,
             **load_args,
         )  # read manifest csv file as is from manifest path
 
@@ -314,34 +317,51 @@ def populateModelManifest(
             manifestPath, emptyManifestURL, return_excel=return_excel, title=title
         )
 
-    def submit_metadata_manifest(
+    def submit_metadata_manifest(  # pylint: disable=too-many-arguments, too-many-locals
         self,
         manifest_path: str,
-        path_to_json_ld: str,
         dataset_id: str,
         manifest_record_type: str,
         restrict_rules: bool,
         access_token: Optional[str] = None,
         validate_component: Optional[str] = None,
+        file_annotations_upload: bool = True,
         hide_blanks: bool = False,
-        project_scope: List = None,
+        project_scope: Optional[list] = None,
         table_manipulation: str = "replace",
         table_column_names: str = "class_label",
         annotation_keys: str = "class_label",
     ) -> str:
-        """Wrap methods that are responsible for validation of manifests for a given component, and association of the
-        same manifest file with a specified dataset.
+        """
+        Wrap methods that are responsible for validation of manifests for a given component,
+          and association of the same manifest file with a specified dataset.
+
         Args:
-            manifest_path: Path to the manifest file, which contains the metadata.
-            dataset_id: Synapse ID of the dataset on Synapse containing the metadata manifest file.
-            validate_component: Component from the schema.org schema based on which the manifest template has been generated.
-        Returns:
-            Manifest ID: If both validation and association were successful.
-        Exceptions:
+            manifest_path (str): Path to the manifest file, which contains the metadata.
+            dataset_id (str): Synapse ID of the dataset on Synapse containing the
+              metadata manifest file.
+            manifest_record_type (str): How the manifest is stored in Synapse
+            restrict_rules (bool):
+              If True: bypass great expectations and restrict rule options to
+                those implemented in house
+            access_token (Optional[str], optional): Defaults to None.
+            validate_component (Optional[str], optional): Component from the schema.org
+              schema based on which the manifest template has been generated.
+            file_annotations_upload (bool, optional): Default to True. If false, do
+              not add annotations to files. Defaults to True.
+            hide_blanks (bool, optional): Defaults to False.
+            project_scope (Optional[list], optional): Defaults to None.
+            table_manipulation (str, optional): Defaults to "replace".
+            table_column_names (str, optional): Defaults to "class_label".
+            annotation_keys (str, optional): Defaults to "class_label".
+
+        Raises:
             ValueError: When validate_component is provided, but it cannot be found in the schema.
             ValidationError: If validation against data model was not successful.
-        """
 
+        Returns:
+            str: If both validation and association were successful.
+        """
         # TODO: avoid explicitly exposing Synapse store functionality
         # just instantiate a Store class and let it decide at runtime/config
         # the store type
@@ -349,25 +369,25 @@ def submit_metadata_manifest(
             access_token=access_token, project_scope=project_scope
         )
         manifest_id = None
-        censored_manifest_id = None
         restrict_maniest = False
         censored_manifest_path = manifest_path.replace(".csv", "_censored.csv")
         # check if user wants to perform validation or not
         if validate_component is not None:
             try:
-                # check if the component ("class" in schema) passed as argument is valid (present in schema) or not
+                # check if the component ("class" in schema) passed as argument is valid
+                # (present in schema) or not
                 self.dmge.is_class_in_schema(validate_component)
-            except:
-                # a KeyError exception is raised when validate_component fails in the try-block above
-                # here, we are suppressing the KeyError exception and replacing it with a more
-                # descriptive ValueError exception
+            except Exception as exc:
+                # a KeyError exception is raised when validate_component fails in the
+                # try-block above here, we are suppressing the KeyError exception and
+                # replacing it with a more descriptive ValueError exception
                 raise ValueError(
                     f"The component '{validate_component}' could not be found "
-                    f"in the schema here '{path_to_json_ld}'"
-                )
+                    f"in the schema here '{self.path_to_json_ld}'"
+                ) from exc
 
             # automatic JSON schema generation and validation with that JSON schema
-            val_errors, val_warnings = self.validateModelManifest(
+            val_errors, _ = self.validateModelManifest(
                 manifestPath=manifest_path,
                 rootNode=validate_component,
                 restrict_rules=restrict_rules,
@@ -379,7 +399,7 @@ def submit_metadata_manifest(
             if val_errors == []:
                 # upload manifest file from `manifest_path` path to entity with Syn ID `dataset_id`
                 if os.path.exists(censored_manifest_path):
-                    censored_manifest_id = syn_store.associateMetadataWithFiles(
+                    syn_store.associateMetadataWithFiles(
                         dmge=self.dmge,
                         metadataManifestPath=censored_manifest_path,
                         datasetId=dataset_id,
@@ -388,6 +408,7 @@ def submit_metadata_manifest(
                         table_manipulation=table_manipulation,
                         table_column_names=table_column_names,
                         annotation_keys=annotation_keys,
+                        file_annotations_upload=file_annotations_upload,
                     )
                     restrict_maniest = True
 
@@ -401,9 +422,10 @@ def submit_metadata_manifest(
                     table_manipulation=table_manipulation,
                     table_column_names=table_column_names,
                     annotation_keys=annotation_keys,
+                    file_annotations_upload=file_annotations_upload,
                 )
 
-                logger.info(f"No validation errors occured during validation.")
+                logger.info("No validation errors occured during validation.")
                 return manifest_id
 
             else:
@@ -414,7 +436,7 @@ def submit_metadata_manifest(
 
         # no need to perform validation, just submit/associate the metadata manifest file
         if os.path.exists(censored_manifest_path):
-            censored_manifest_id = syn_store.associateMetadataWithFiles(
+            syn_store.associateMetadataWithFiles(
                 dmge=self.dmge,
                 metadataManifestPath=censored_manifest_path,
                 datasetId=dataset_id,
@@ -423,6 +445,7 @@ def submit_metadata_manifest(
                 table_manipulation=table_manipulation,
                 table_column_names=table_column_names,
                 annotation_keys=annotation_keys,
+                file_annotations_upload=file_annotations_upload,
             )
             restrict_maniest = True
 
@@ -436,6 +459,7 @@ def submit_metadata_manifest(
             table_manipulation=table_manipulation,
             table_column_names=table_column_names,
             annotation_keys=annotation_keys,
+            file_annotations_upload=file_annotations_upload,
         )
 
         logger.debug(
diff --git a/schematic/models/validate_attribute.py b/schematic/models/validate_attribute.py
index 693309118..b34eb58a8 100644
--- a/schematic/models/validate_attribute.py
+++ b/schematic/models/validate_attribute.py
@@ -1,16 +1,13 @@
 import builtins
 import logging
 import re
-import sys
-import time
 from time import perf_counter
-from os import getenv
 
 # allows specifying explicit variable types
-from typing import Any, Dict, List, Optional, Text
+from typing import Any, Optional, Literal, Union
 from urllib import error
 from urllib.parse import urlparse
-from urllib.request import HTTPDefaultErrorHandler, OpenerDirector, Request, urlopen
+from urllib.request import Request, urlopen
 
 import numpy as np
 import pandas as pd
@@ -18,7 +15,6 @@
 
 from schematic.schemas.data_model_graph import DataModelGraphExplorer
 
-from schematic.store.base import BaseStorage
 from schematic.store.synapse import SynapseStorage
 from schematic.utils.validate_rules_utils import validation_rule_info
 from schematic.utils.validate_utils import (
@@ -33,54 +29,36 @@
 
 logger = logging.getLogger(__name__)
 
+MessageLevelType = Literal["warning", "error"]
+ScopeTypes = Literal["set", "value"]
+
 
 class GenerateError:
     def generate_schema_error(
         row_num: str,
         attribute_name: str,
-        error_msg: str,
-        invalid_entry: str,
+        error_message: str,
+        invalid_entry: Any,
         dmge: DataModelGraphExplorer,
-    ) -> List[str]:
+    ) -> tuple[list[list[str]], list[list[str]]]:
         """
         Purpose: Process error messages generated from schema
         Input:
             - row_num: the row the error occurred on.
             - attribute_name: the attribute the error occurred on.
-            - error_msg: Error message
+            - error_message: Error message
+            - invalid_entry: The value that caused the error
+            - dmge: DataModelGraphExplorer object
         """
-        error_list = []
-        warning_list = []
-
-        # Determine which, if any, message to raise
-        if attribute_name.lower() == "wrong schema":
-            raises = "error"
-        else:
-            raises = GenerateError.get_message_level(
-                val_rule="schema",
-                attribute_name=attribute_name,
-                dmge=dmge,
-            )
 
-        # if a message needs to be raised, get the approrpiate function to do so
-        if raises:
-            logLevel = getattr(logger, raises)
-        else:
-            return error_list, warning_list
-
-        error_col = attribute_name  # Attribute name
-        error_row = row_num  # index row of the manifest where the error presented.
-        error_message = error_msg
-
-        arg_error_string = (
-            f"For the attribute '{error_col}', on row {error_row}, {error_message}."
+        error_list, warning_list = GenerateError.raise_and_store_message(
+            dmge=dmge,
+            val_rule="schema",
+            error_row=row_num,
+            error_col=attribute_name,
+            error_message=error_message,
+            error_val=invalid_entry,
         )
-        logLevel(arg_error_string)
-
-        if raises == "error":
-            error_list = [error_row, error_col, error_message, invalid_entry]
-        elif raises == "warning":
-            warning_list = [error_row, error_col, error_message, invalid_entry]
 
         return error_list, warning_list
 
@@ -88,11 +66,11 @@ def generate_list_error(
         list_string: str,
         row_num: str,
         attribute_name: str,
-        list_error: str,
-        invalid_entry: str,
+        list_error: Literal["not_comma_delimited", "not_a_string"],
+        invalid_entry: Any,
         dmge: DataModelGraphExplorer,
         val_rule: str,
-    ) -> List[str]:
+    ) -> tuple[list[list[str]], list[list[str]]]:
         """
         Purpose:
             If an error is found in the string formatting, detect and record
@@ -101,46 +79,36 @@ def generate_list_error(
             - list_string: the user input list, that is represented as a string.
             - row_num: the row the error occurred on.
             - attribute_name: the attribute the error occurred on.
-        Returns:
-        logger.error or logger.warning.
-        Errors: List[str] Error details for further storage.
-        warnings: List[str] Warning details for further storage.
+            - list_error: the type of list error that occurred.
+            - invalid_entry: The value that caused the error
+            - dmge: DataModelGraphExplorer object
+            - val_rule: validation rule str, defined in the schema.
+        Returns
+        Errors: list[str] Error details for further storage.
+        warnings: list[str] Warning details for further storage.
         """
 
-        error_list = []
-        warning_list = []
-
-        # Determine which, if any, message to raise
-        raises = GenerateError.get_message_level(
-            val_rule=val_rule,
-            attribute_name=attribute_name,
-            dmge=dmge,
-        )
-
-        # if a message needs to be raised, get the approrpiate function to do so
-        if raises:
-            logLevel = getattr(logger, raises)
-        else:
-            return error_list, warning_list
-
         if list_error == "not_comma_delimited":
-            error_str = (
+            error_message = (
                 f"For attribute {attribute_name} in row {row_num} it does not "
                 f"appear as if you provided a comma delimited string. Please check "
                 f"your entry ('{list_string}'') and try again."
             )
-            logLevel(error_str)
-            error_row = row_num  # index row of the manifest where the error presented.
-            error_col = attribute_name  # Attribute name
-            error_message = error_str
-            error_val = invalid_entry
-            # return error and empty list for warnings
-
-        if raises == "error":
-            error_list = [error_row, error_col, error_message, error_val]
-        # return warning and empty list for errors
-        elif raises == "warning":
-            warning_list = [error_row, error_col, error_message, error_val]
+        elif list_error == "not_a_string":
+            error_message = (
+                f"For attribute {attribute_name} in row {row_num} it does not "
+                f"appear as if you provided a string. Please check "
+                f"your entry ('{list_string}'') and try again."
+            )
+
+        error_list, warning_list = GenerateError.raise_and_store_message(
+            dmge=dmge,
+            val_rule=val_rule,
+            error_row=row_num,
+            error_col=attribute_name,
+            error_message=error_message,
+            error_val=invalid_entry,
+        )
 
         return error_list, warning_list
 
@@ -150,9 +118,9 @@ def generate_regex_error(
         row_num: str,
         module_to_call: str,
         attribute_name: str,
-        invalid_entry: str,
+        invalid_entry: Any,
         dmge: DataModelGraphExplorer,
-    ) -> List[str]:
+    ) -> tuple[list[list[str]], list[list[str]]]:
         """
         Purpose:
             Generate an logging error as well as a stored error message, when
@@ -163,43 +131,26 @@ def generate_regex_error(
             row_num: str, row where the error was detected
             module_to_call: re module specified in the schema
             attribute_name: str, attribute being validated
+            invalid_entry: value that caused the error
+            dmge: DataModelGraphExplorer object
         Returns:
-        logger.error or logger.warning.
-        Errors: List[str] Error details for further storage.
-        warnings: List[str] Warning details for further storage.
+        Errors: list[str] Error details for further storage.
+        warnings: list[str] Warning details for further storage.
         """
-        error_list = []
-        warning_list = []
-
-        # Determine which, if any, message to raise
-        raises = GenerateError.get_message_level(
-            val_rule=val_rule,
-            attribute_name=attribute_name,
-            dmge=dmge,
-        )
-
-        # if a message needs to be raised, get the approrpiate function to do so
-        if raises:
-            logLevel = getattr(logger, raises)
-        else:
-            return error_list, warning_list
 
-        regex_error_string = (
+        error_message = (
             f"For the attribute {attribute_name}, on row {row_num}, the string is not properly formatted. "
             f'It should follow the following re.{module_to_call} pattern "{reg_expression}".'
         )
-        logLevel(regex_error_string)
-        error_row = row_num  # index row of the manifest where the error presented.
-        error_col = attribute_name  # Attribute name
-        error_message = regex_error_string
-        error_val = invalid_entry
-
-        # return error and empty list for warnings
-        if raises == "error":
-            error_list = [error_row, error_col, error_message, error_val]
-        # return warning and empty list for errors
-        elif raises == "warning":
-            warning_list = [error_row, error_col, error_message, error_val]
+
+        error_list, warning_list = GenerateError.raise_and_store_message(
+            dmge=dmge,
+            val_rule=val_rule,
+            error_row=row_num,
+            error_col=attribute_name,
+            error_message=error_message,
+            error_val=invalid_entry,
+        )
 
         return error_list, warning_list
 
@@ -207,9 +158,9 @@ def generate_type_error(
         val_rule: str,
         row_num: str,
         attribute_name: str,
-        invalid_entry: str,
+        invalid_entry: Any,
         dmge: DataModelGraphExplorer,
-    ) -> List[str]:
+    ) -> tuple[list[list[str]], list[list[str]]]:
         """
         Purpose:
             Generate an logging error as well as a stored error message, when
@@ -218,55 +169,28 @@ def generate_type_error(
             val_rule: str, defined in the schema.
             row_num: str, row where the error was detected
             attribute_name: str, attribute being validated
+            invalid_entry: str, value that caused the error
+            dmge: DataModelGraphExplorer object
         Returns:
-        logger.error or logger.warning.
-        Errors: List[str] Error details for further storage.
-        warnings: List[str] Warning details for further storage.
+        Errors: list[str] Error details for further storage.
+        warnings: list[str] Warning details for further storage.
         """
 
-        error_list = []
-        warning_list = []
-
-        # Determine which, if any, message to raise
-        raises = GenerateError.get_message_level(
-            dmge=dmge,
-            attribute_name=attribute_name,
-            val_rule=val_rule,
-        )
-
-        # if a message needs to be raised, get the approrpiate function to do so
-        if raises:
-            logLevel = getattr(logger, raises)
-        else:
-            return error_list, warning_list
+        base_rule = val_rule.split(" ")[0]
 
-        type_error_str = (
+        error_message = (
             f"On row {row_num} the attribute {attribute_name} "
-            f"does not contain the proper value type {val_rule}."
-        )
-        error_row = row_num  # index row of the manifest where the error presented.
-        error_col = attribute_name  # Attribute name
-        error_message = type_error_str
-        error_val = invalid_entry
-
-        # TODO: not sure if this i needed (to split)
-        validation_rules = dmge.get_node_validation_rules(
-            node_display_name=attribute_name
+            f"does not contain the proper value type {base_rule}."
         )
 
-        # If IsNA rule is being used to allow `Not Applicable` entries, do not log a message
-        if error_val.lower() == "not applicable" and rule_in_rule_list(
-            "IsNA", validation_rules
-        ):
-            pass
-        else:
-            logLevel(type_error_str)
-            # return error and empty list for warnings
-            if raises == "error":
-                error_list = [error_row, error_col, error_message, error_val]
-            # return warning and empty list for errors
-            elif raises == "warning":
-                warning_list = [error_row, error_col, error_message, error_val]
+        error_list, warning_list = GenerateError.raise_and_store_message(
+            dmge=dmge,
+            val_rule=val_rule,
+            error_row=row_num,
+            error_col=attribute_name,
+            error_message=error_message,
+            error_val=invalid_entry,
+        )
 
         return error_list, warning_list
 
@@ -276,10 +200,10 @@ def generate_url_error(
         row_num: str,
         attribute_name: str,
         argument: str,
-        invalid_entry: str,
+        invalid_entry: Any,
         dmge: DataModelGraphExplorer,
         val_rule: str,
-    ) -> List[str]:
+    ) -> tuple[list[list[str]], list[list[str]]]:
         """
         Purpose:
             Generate an logging error as well as a stored error message, when
@@ -297,64 +221,42 @@ def generate_url_error(
         Input:
             url: str, that was input by the user.
             url_error: str, error detected in url_validation()
+            row_num: str, row where the error was detected
             attribute_name: str, attribute being validated
             argument: str, argument being validated.
+            invalid_entry: str, value that caused the error
+            dmge: DataModelGraphExplorer object
+            val_rule: validation rule str, defined in the schema.
         Returns:
-        logger.error or logger.warning.
-        Errors: List[str] Error details for further storage.
-        warnings: List[str] Warning details for further storage.
+        Errors: list[str] Error details for further storage.
+        warnings: list[str] Warning details for further storage.
         """
 
-        error_list = []
-        warning_list = []
-
-        # Determine which, if any, message to raise
-        raises = GenerateError.get_message_level(
-            val_rule=val_rule,
-            attribute_name=attribute_name,
-            dmge=dmge,
-        )
-
-        # if a message needs to be raised, get the approrpiate function to do so
-        if raises:
-            logLevel = getattr(logger, raises)
-        else:
-            return error_list, warning_list
-
-        error_row = row_num  # index row of the manifest where the error presented.
-        error_col = attribute_name  # Attribute name
         if url_error == "invalid_url":
-            invalid_url_error_string = (
+            error_message = (
                 f"For the attribute '{attribute_name}', on row {row_num}, the URL provided ({url}) does not "
                 f"conform to the standards of a URL. Please make sure you are entering a real, working URL "
                 f"as required by the Schema."
             )
-            logLevel(invalid_url_error_string)
-            error_message = invalid_url_error_string
-            error_val = invalid_entry
         elif url_error == "arg_error":
-            arg_error_string = (
+            error_message = (
                 f"For the attribute '{attribute_name}', on row {row_num}, the URL provided ({url}) does not "
                 f"conform to the schema specifications and does not contain the required element: {argument}."
             )
-            logLevel(arg_error_string)
-            error_message = arg_error_string
-            error_val = f"URL Error: Argument Error"
         elif url_error == "random_entry":
-            random_entry_error_str = (
+            error_message = (
                 f"For the attribute '{attribute_name}', on row {row_num}, the input provided ('{url}'') does not "
                 f"look like a URL, please check input and try again."
             )
-            logLevel(random_entry_error_str)
-            error_message = random_entry_error_str
-            error_val = f"URL Error: Random Entry"
 
-        # return error and empty list for warnings
-        if raises == "error":
-            error_list = [error_row, error_col, error_message, error_val]
-        # return warning and empty list for errors
-        elif raises == "warning":
-            warning_list = [error_row, error_col, error_message, error_val]
+        error_list, warning_list = GenerateError.raise_and_store_message(
+            dmge=dmge,
+            val_rule=val_rule,
+            error_row=row_num,
+            error_col=attribute_name,
+            error_message=error_message,
+            error_val=invalid_entry,
+        )
 
         return error_list, warning_list
 
@@ -362,78 +264,65 @@ def generate_cross_warning(
         val_rule: str,
         attribute_name: str,
         dmge: DataModelGraphExplorer,
-        matching_manifests=[],
-        missing_manifest_ID=None,
-        invalid_entry=None,
-        row_num=None,
-    ) -> List[str]:
+        matching_manifests: list[str] = None,
+        manifest_id: Optional[list[str]] = None,
+        invalid_entry: Union[str, list[str]] = "No Invalid Entry Recorded",
+        row_num: Optional[list[str]] = None,
+    ) -> tuple[list[list[str]], list[list[str]]]:
         """
         Purpose:
             Generate an logging error as well as a stored error message, when
             a cross validation error is encountered.
         Input:
             val_rule: str, defined in the schema.
-            matching_manifests: list of manifests with all values in the target attribute present
-            manifest_ID: str, synID of the target manifest missing the source value
             attribute_name: str, attribute being validated
-            invalid_entry: str, value present in source manifest that is missing in the target
-            row_num: row in source manifest with value missing in target manifests
+            dmge: DataModelGraphExplorer object
+            matching_manifests: list of manifests with all values in the target attribute present
+            manifest_id: list, synID of the target manifest missing the source value
+            invalid_entry: list, value present in source manifest that is missing in the target
+            row_num: list, row in source manifest with value missing in target manifests
         Returns:
-        logger.error or logger.warning.
-        Errors: List[str] Error details for further storage.
-        warnings: List[str] Warning details for further storage.
+        Errors: list[str] Error details for further storage.
+        warnings: list[str] Warning details for further storage.
         """
-        error_list = []
-        warning_list = []
-
-        # Determine which, if any, message to raise
-        raises = GenerateError.get_message_level(
-            val_rule=val_rule,
-            attribute_name=attribute_name,
-            dmge=dmge,
-        )
-
-        # if a message needs to be raised, get the approrpiate function to do so
-        if raises:
-            logLevel = getattr(logger, raises)
-        else:
-            return error_list, warning_list
 
-        if val_rule.__contains__("matchAtLeast"):
-            cross_error_str = f"Value(s) {invalid_entry} from row(s) {row_num} of the attribute {attribute_name} in the source manifest are missing."
-            cross_error_str += (
-                f" Manifest(s) {missing_manifest_ID} are missing the value(s)."
-                if missing_manifest_ID
+        if "matchAtLeast" in val_rule:
+            error_message = f"Value(s) {invalid_entry} from row(s) {row_num} of the attribute {attribute_name} in the source manifest are missing."
+            error_message += (
+                f" Manifest(s) {manifest_id} are missing the value(s)."
+                if manifest_id
                 else ""
             )
 
-        elif val_rule.__contains__("matchExactly"):
-            if matching_manifests != []:
-                cross_error_str = f"All values from attribute {attribute_name} in the source manifest are present in {len(matching_manifests)} manifests instead of only 1."
-                cross_error_str += (
-                    f" Manifests {matching_manifests} match the values in the source attribute."
-                    if matching_manifests
-                    else ""
-                )
-
-            elif val_rule.__contains__("set"):
-                cross_error_str = f"No matches for the values from attribute {attribute_name} in the source manifest are present in any other manifests instead of being present in exactly 1. "
-            elif val_rule.__contains__("value"):
-                cross_error_str = f"Value(s) {invalid_entry} from row(s) {row_num} of the attribute {attribute_name} in the source manifest are not present in only one other manifest. "
+        elif "matchExactly" in val_rule:
+            if matching_manifests and matching_manifests != []:
+                error_message = f"All values from attribute {attribute_name} in the source manifest are present in {len(matching_manifests)} manifests instead of only 1."
+                error_message += f" Manifests {matching_manifests} match the values in the source attribute."
 
-        logLevel(cross_error_str)
-        error_row = row_num  # index row of the manifest where the error presented.
-        error_col = attribute_name  # Attribute name
-        error_message = cross_error_str
-        error_val = invalid_entry  # Value from source manifest missing from targets
+            elif "set" in val_rule:
+                error_message = f"No matches for the values from attribute {attribute_name} in the source manifest are present in any other manifests instead of being present in exactly 1. "
+            elif "value" in val_rule:
+                error_message = f"Value(s) {invalid_entry} from row(s) {row_num} of the attribute {attribute_name} in the source manifest are not present in only one other manifest. "
 
-        # return error and empty list for warnings
-        if raises == "error":
-            error_list = [error_row, error_col, error_message, error_val]
-        # return warning and empty list for errors
-        elif raises == "warning":
-            warning_list = [error_row, error_col, error_message, error_val]
+        elif "matchNone" in val_rule:
+            error_message = (
+                f"Value(s) {invalid_entry} from row(s) {row_num} for the attribute {attribute_name} "
+                f"in the source manifest are not unique."
+            )
+            error_message += (
+                f" Manifest(s) {manifest_id} contain duplicate values."
+                if manifest_id
+                else ""
+            )
 
+        error_list, warning_list = GenerateError.raise_and_store_message(
+            dmge=dmge,
+            val_rule=val_rule,
+            error_row=row_num,
+            error_col=attribute_name,
+            error_message=error_message,
+            error_val=invalid_entry,
+        )
         return error_list, warning_list
 
     def generate_content_error(
@@ -441,8 +330,8 @@ def generate_content_error(
         attribute_name: str,
         dmge: DataModelGraphExplorer,
         row_num=None,
-        error_val=None,
-    ) -> (List[str], List[str]):
+        invalid_entry=None,
+    ) -> tuple[list[str], list[str]]:
         """
         Purpose:
             Generate an logging error or warning as well as a stored error/warning message when validating the content of a manifest attribute.
@@ -456,120 +345,365 @@ def generate_content_error(
                 attribute_name: str, attribute being validated
                 dmge: DataModelGraphExplorer object
                 row_num: str, row where the error was detected
-                error_val: value duplicated
-
+                invalid_entry: erroneous value(s)
         Returns:
-            logger.error or logger.warning.
-            Errors: List[str] Error details for further storage.
-            warnings: List[str] Warning details for further storage.
+            Errors: list[str] Error details for further storage.
+            warnings: list[str] Warning details for further storage.
         """
-        error_list = []
-        warning_list = []
-        error_col = attribute_name  # Attribute name
-        if error_val:
-            error_val = iterable_to_str_list(set(error_val))
 
-        # Determine which, if any, message to raise
-        raises = GenerateError.get_message_level(
-            val_rule=val_rule,
-            attribute_name=attribute_name,
-            dmge=dmge,
-        )
-
-        # if a message needs to be raised, get the approrpiate function to do so
-        if raises:
-            logLevel = getattr(logger, raises)
-        else:
-            return error_list, warning_list
+        error_row = row_num
 
         # log warning or error message
         if val_rule.startswith("recommended"):
-            content_error_str = f"Column {attribute_name} is recommended but empty."
-            logLevel(content_error_str)
-            error_message = content_error_str
-
-            if raises == "error":
-                error_list = [error_col, error_message]
-            # return warning and empty list for errors
-            elif raises == "warning":
-                warning_list = [error_col, error_message]
-
-            return error_list, warning_list
+            error_message = f"Column {attribute_name} is recommended but empty."
+            error_row = None
+            invalid_entry = None
 
-        elif val_rule.startswith("unique"):
-            content_error_str = f"Column {attribute_name} has the duplicate value(s) {error_val} in rows: {row_num}."
+        if val_rule.startswith("unique"):
+            invalid_entry = (
+                iterable_to_str_list(set(invalid_entry)) if invalid_entry else None
+            )
+            error_message = f"Column {attribute_name} has the duplicate value(s) {invalid_entry} in rows: {row_num}."
 
         elif val_rule.startswith("protectAges"):
-            content_error_str = f"Column {attribute_name} contains ages that should be censored in rows: {row_num}."
+            error_message = f"Column {attribute_name} contains ages that should be censored in rows: {row_num}."
 
         elif val_rule.startswith("inRange"):
-            content_error_str = f"{attribute_name} values in rows {row_num} are out of the specified range."
+            error_message = f"{attribute_name} values in rows {row_num} are out of the specified range."
+
         elif val_rule.startswith("date"):
-            content_error_str = (
+            error_message = (
                 f"{attribute_name} values in rows {row_num} are not parsable as dates."
             )
+
         elif val_rule.startswith("IsNA"):
-            content_error_str = f"{attribute_name} values in rows {row_num} are not marked as 'Not Applicable'."
-
-        if val_rule != "IsNA":
-            logLevel(content_error_str)
-            error_row = row_num
-            error_message = content_error_str
-            # return error and empty list for warnings
-            if raises == "error":
-                error_list = [error_row, error_col, error_message, error_val]
-            # return warning and empty list for errors
-            elif raises == "warning":
-                warning_list = [error_row, error_col, error_message, error_val]
+            error_message = f"{attribute_name} values in rows {row_num} are not marked as 'Not Applicable'."
+
+        error_list, warning_list = GenerateError.raise_and_store_message(
+            dmge=dmge,
+            val_rule=val_rule,
+            error_row=error_row,
+            error_col=attribute_name,
+            error_message=error_message,
+            error_val=invalid_entry,
+        )
 
         return error_list, warning_list
 
+    def generate_no_cross_warning(
+        dmge: DataModelGraphExplorer, attribute_name: str, val_rule: str
+    ) -> list[list[str]]:
+        """Raise a warning if no columns were found in the specified project to validate against, inform user the
+        source manifest will be uploaded without running validation. Retain standard warning
+        Args:
+            dmge: DataModelGraphExplorer object
+            attribute_name, str: str, attribute being validated
+            val_rule: str, defined in the schema.
+        Returns:
+            warnings: list[str] Warning details for further storage.
+        """
+        error_message = (
+            "Cross Manifest Validation Warning: There are no target columns to validate "
+            f"this manifest against for attribute: {attribute_name}, "
+            f"and validation rule: {val_rule}. It is assumed this is the first manifest in a "
+            "series to be submitted, so validation will pass, for now, and will run again "
+            "when there are manifests uploaded to validate against."
+        )
+
+        _, warnings = GenerateError.raise_and_store_message(
+            dmge=dmge,
+            val_rule=val_rule,
+            error_row=None,
+            error_col=attribute_name,
+            error_message=error_message,
+            error_val=None,
+            message_level="warning",
+        )
+
+        return [warnings]
+
+    def generate_no_value_in_manifest_error(
+        dmge: DataModelGraphExplorer, attribute_name: str, val_rule: str
+    ) -> tuple[list[str], list[str]]:
+        """Raise a warning or error based on the messaging level, if target manifests have been found to
+        validate against but the manifest itself does not contain data (across the entire manifest,
+        not just the column being validated.)
+        Args:
+            dmge: DataModelGraphExplorer object
+            attribute_name, str: str, attribute being validated
+            val_rule: str, defined in the schema.
+        Returns:
+            Errors: list[str] Error details for further storage.
+            warnings: list[str] Warning details for further storage.
+        """
+        errors, warnings = [], []
+        error_message = (
+            f"Cross Manifest Validation: There were manifests found to match for attribute: {attribute_name} "
+            f"and validation rule: {val_rule}, but no data was found in this manifest to validate against."
+        )
+
+        nv_errors, nv_warnings = GenerateError.raise_and_store_message(
+            dmge=dmge,
+            val_rule=val_rule,
+            error_row=None,
+            error_col=attribute_name,
+            error_message=error_message,
+            error_val="No Invalid Entry Recorded",
+        )
+        if nv_errors:
+            errors.append(nv_errors)
+        if nv_warnings:
+            warnings.append(nv_warnings)
+        return errors, warnings
+
+    def _get_rule_attributes(
+        val_rule: str, error_col_name: str, dmge: DataModelGraphExplorer
+    ) -> tuple[list, str, MessageLevelType, bool, bool, bool]:
+        """Extract different attributes from the given rule
+        Args:
+            val_rule, str: validation_rule being passed.
+            error_col_name, str, the display name of the attribute the rule is being applied to
+            dmge, DataModelGraphExplorer Object
+        Returns:
+        """
+        rule_parts = val_rule.split(" ")
+        rule_name = rule_parts[0]
+        specified_level = rule_parts[-1].lower()
+        specified_level = (
+            specified_level if specified_level in ["warning", "error"] else None
+        )
+
+        is_schema_error = rule_name == "schema"
+        col_is_recommended = rule_name == "recommended"
+        col_is_required = dmge.get_node_required(node_display_name=error_col_name)
+        return (
+            rule_parts,
+            rule_name,
+            specified_level,
+            is_schema_error,
+            col_is_recommended,
+            col_is_required,
+        )
+
+    def get_is_na_allowed(node_display_name: str, dmge: DataModelGraphExplorer) -> bool:
+        """Determine if NAs are allowed based on the original set of rules
+        Args:
+            node_display_name, str: display name for the current attribure
+            dmge, DataModelGraphExplorer
+        Returns:
+            bool: True, if IsNA is one of the rules, else False
+        """
+        # Get -all- of the specified validation rules for the attribute,
+        validation_rule_list = dmge.get_node_validation_rules(
+            node_display_name=node_display_name
+        )
+
+        # Determine if IsNA is one of the rules, if it is return True, else return False
+        if rule_in_rule_list("IsNA", validation_rule_list):
+            return True
+        else:
+            return False
+
+    def get_error_value_is_na(
+        error_val,
+        na_allowed: bool = False,
+    ) -> bool:
+        """Determine if the erroring value is NA
+        Args:
+            error_val: erroneous value
+        Returns:
+            bool: Returns True, if the error value is evaluated to be NA, and False if not
+        ):
+
+        """
+        not_applicable_strings = [
+            "not applicable",
+        ]
+
+        # Try to figure out if the erroring value is NA
+        if isinstance(error_val, str) and na_allowed:
+            error_val_is_na = error_val.lower() in not_applicable_strings
+        elif isinstance(error_val, list):
+            error_val_is_na = False
+        elif (error_val is None) or pd.isnull(error_val) or (error_val == "<NA>"):
+            error_val_is_na = True
+        else:
+            error_val_is_na = False
+        return error_val_is_na
+
+    def _determine_messaging_level(
+        rule_name: str,
+        error_val_is_na: bool,
+        specified_level: MessageLevelType,
+        is_schema_error: bool,
+        col_is_required: bool,
+        col_is_recommended: bool,
+    ) -> Optional[MessageLevelType]:
+        """Deterimine messaging level given infromation that was gathered about the rule and the error value
+        Args:
+            rule_name, str: The name of the rule being applied to the data, stripped of additional information
+            error_val_is_na, bool: True if error is entry is non-value, False if entry has a value
+            specified_level, MessageLevelType: Messaging level specified in the rule.
+            is_schema_error, bool: True if rule_name=="schema"
+            col_is_required, bool: True if the attribute column is required in the schema.
+            col_is_recommended, bool: True if rule_name=="recommended"
+        Returns:
+            Optional[MessageLevelType]: Messaging level is returned, if applicable.
+        """
+
+        # If the erroring value is NA, do not raise message. Do not worry about if value is required or not
+        # bc this is already accounted for in JSON Validation.
+        # This allows flexibiity to where, we dont need the value to be provided,
+        # but also the case where NA is recorded as the value 'Not Applicable'
+        if error_val_is_na and col_is_recommended:
+            message_level = "warning"
+
+        elif error_val_is_na:
+            message_level = None
+
+        # If the level was specified, return that level
+        elif specified_level:
+            message_level = specified_level
+
+        # If the rule being evaluated IsNa then do not raise message
+        elif rule_name.lower() == "isna":
+            message_level = None
+
+        # If schema error return Error
+        elif is_schema_error:
+            message_level = "error"
+
+        # If the column is not required, raise a warning,
+        elif not col_is_required:
+            message_level = "warning"
+        # If is the column is required, but recommended, do not raise message
+        elif col_is_required and col_is_recommended:
+            message_level = None
+
+        # If none of the above statements catches, then return the default message level, determine for a given rule.
+        # Rules have default messaging levels.
+
+        else:
+            message_level = validation_rule_info()[rule_name]["default_message_level"]
+        return message_level
+
     def get_message_level(
         dmge: DataModelGraphExplorer,
-        attribute_name: str,
+        error_col: str,
+        error_val: Union[str, list[str]],
         val_rule: str,
-    ) -> str:
+    ) -> Optional[str]:
         """
         Purpose:
             Determine whether an error or warning message should be logged and displayed
 
-            if node is not required,
-                return warning
-            if node is recommended and requried,
-                return None
-            for other rules, parse possible, if not use default specified in validation_rule_info
-
+            Message determination hierarchy is as follows:
+                1. Schema errors are always logged as errors.
+                2. If a message level is specified in the validation rule, it is logged as such.
+                3. If the erroneous value is 'not applicable' and the rule is modified by 'IsNA', no message is logged.
+                    3a. Messages are never logged specifically for the IsNA rule.
+                4. If no level is specified and there is an erroneous value, level is determined by whether or not
+                the attribute is required and if the rule set is modified by the recommended modifier.
+                5. If none of the above conditions apply, the default message level for the rule is logged.
         Input:
-                val_rule: str, defined in the schema.
                 dmge: DataModelGraphExplorer object
-                attribute_name: str, attribute being validated
+                error_col: str, Display name of attribute being validated
+                error_val: Any, erroneous value
+                val_rule: str, current attribute rule, defined in the schema, and being evaluated in this step
+                    (single rule set)
         Returns:
             'error', 'warning' or None
-        # TODO: recommended and other rules
+        Raises:
+            Logging messagees, either error, warning, or no message
         """
 
-        level = None
-        rule_parts = val_rule.split(" ")
-        rule_info = validation_rule_info()
-
-        # set message level to default and change after
-        if rule_parts[0] != "schema":
-            level = rule_info[rule_parts[0]]["default_message_level"]
-        # Parse rule for level, set to default if not specified
-        if rule_parts[-1].lower() == "error" or rule_parts[0] == "schema":
-            level = "error"
-        elif rule_parts[-1].lower() == "warning":
-            level = "warning"
-        elif not dmge.get_node_required(node_display_name=attribute_name):
-            # If not required raise warnings to notify
-            level = "warning"
-        elif (
-            dmge.get_node_required(node_display_name=attribute_name)
-            and "recommended" in val_rule
-        ):
-            level = None
+        # Extract attributes from the current val_rule
+        (
+            rule_parts,
+            rule_name,
+            specified_level,
+            is_schema_error,
+            col_is_recommended,
+            col_is_required,
+        ) = GenerateError._get_rule_attributes(
+            val_rule=val_rule, error_col_name=error_col, dmge=dmge
+        )
+
+        # Determine if NA values are allowed.
+        na_allowed = GenerateError.get_is_na_allowed(
+            node_display_name=error_col, dmge=dmge
+        )
 
-        return level
+        # Determine if the provided value that is
+        error_val_is_na = GenerateError.get_error_value_is_na(
+            error_val,
+            na_allowed,
+        )
+        # Return Messaging Level as appropriate, determined based on logic and heirarchy
+        message_level = GenerateError._determine_messaging_level(
+            rule_name,
+            error_val_is_na,
+            specified_level,
+            is_schema_error,
+            col_is_required,
+            col_is_recommended,
+        )
+
+        return message_level
+
+    def raise_and_store_message(
+        dmge: DataModelGraphExplorer,
+        val_rule: str,
+        error_row: Optional[str],
+        error_col: Optional[str],
+        error_message: str,
+        error_val: Union[str, list[str]],
+        message_level: Optional[str] = None,
+    ) -> tuple[list[str], list[str]]:
+        """
+        Purpose:
+            Log and store error messages in a list for further storage.
+        Input:
+            - dmge: DataModelGraphExplorer object
+            - val_rule: str, single validation rule who's error is being logged
+            - error_row: str, row where the error was detected
+            - error_col: str, attribute being validated
+            - error_message: str, error message string
+            - error_val: str, erroneous value
+            - message_level: str, message level to raise, if its an unchanging level.
+        Raises:
+            logger.error or logger.warning or no message
+        Returns:
+            error_list: list of errors
+            warning_list: list of warnings
+        """
+
+        error_list = []
+        warning_list = []
+
+        if not message_level:
+            message_level = GenerateError.get_message_level(
+                dmge,
+                error_col,
+                error_val,
+                val_rule,
+            )
+
+        if message_level is None:
+            return error_list, warning_list
+
+        message_logger = getattr(logger, message_level)
+        message_logger(error_message)
+
+        if error_val == "No Invalid Entry Recorded":
+            error_val = None
+
+        if message_level == "error":
+            error_list = [error_row, error_col, error_message, error_val]
+        elif message_level == "warning":
+            warning_list = [error_row, error_col, error_message, error_val]
+
+        return error_list, warning_list
 
 
 class ValidateAttribute(object):
@@ -587,12 +721,54 @@ class ValidateAttribute(object):
         - Add string length validator
     """
 
+    def __init__(self, dmge: DataModelGraphExplorer) -> None:
+        self.dmge = dmge
+
+    def get_no_entry(self, entry: str, node_display_name: str) -> bool:
+        """Helper function to check if the entry is blank or contains a not applicable type string (and NA is permitted)
+        Args:
+            entry, str: manifest entry currently under evaluation
+            node_display_name, str: node display name of the attribute currently under evaluation
+        Returns:
+            True, if value_is_na and na allowed
+            False, if entry has a value, or if submitted not applicable string is not allowed.
+        """
+        na_allowed = GenerateError.get_is_na_allowed(
+            node_display_name=node_display_name, dmge=self.dmge
+        )
+        value_is_na = GenerateError.get_error_value_is_na(
+            entry,
+            na_allowed,
+        )
+        if value_is_na:
+            return True
+        else:
+            return False
+
+    def get_entry_has_value(
+        self,
+        entry: str,
+        node_display_name: str,
+    ) -> bool:
+        """Return the inverse of get_no_entry.
+        Args:
+            entry, str: manifest entry currently under evaluation
+            node_display_name, str: node display name of the attribute currently under evaluation
+        Returns:
+            True, if entry has a value, or if submitted not applicable string is not allowed.
+            False, if value_is_na and na allowed
+        """
+        return not self.get_no_entry(
+            entry,
+            node_display_name,
+        )
+
     def get_target_manifests(
-        target_component, project_scope: List, access_token: str = None
+        self, target_component: str, project_scope: list[str], access_token: str = None
     ):
         t_manifest_search = perf_counter()
-        target_manifest_IDs = []
-        target_dataset_IDs = []
+        target_manifest_ids = []
+        target_dataset_ids = []
 
         # login
         try:
@@ -606,6 +782,7 @@ def get_target_manifests(
 
         # Get list of all projects user has access to
         projects = synStore.getStorageProjects(project_scope=project_scope)
+
         for project in projects:
             # get all manifests associated with datasets in the projects
             target_datasets = synStore.getProjectManifests(projectId=project[0])
@@ -616,20 +793,18 @@ def get_target_manifests(
                     target_component == target_dataset[-1][0].replace(" ", "").lower()
                     and target_dataset[1][0] != ""
                 ):
-                    target_manifest_IDs.append(target_dataset[1][0])
-                    target_dataset_IDs.append(target_dataset[0][0])
-
+                    target_manifest_ids.append(target_dataset[1][0])
+                    target_dataset_ids.append(target_dataset[0][0])
         logger.debug(
             f"Cross manifest gathering elapsed time {perf_counter()-t_manifest_search}"
         )
-        return synStore, target_manifest_IDs, target_dataset_IDs
+        return synStore, target_manifest_ids, target_dataset_ids
 
     def list_validation(
         self,
         val_rule: str,
         manifest_col: pd.core.series.Series,
-        dmge: DataModelGraphExplorer,
-    ) -> (List[List[str]], List[List[str]], pd.core.series.Series):
+    ) -> tuple[list[list[str]], list[list[str]], pd.core.series.Series]:
         """
         Purpose:
             Determine if values for a particular attribute are comma separated.
@@ -639,8 +814,8 @@ def list_validation(
         Returns:
             - manifest_col: Input values in manifest arere-formatted to a list
             logger.error or logger.warning.
-            Errors: List[str] Error details for further storage.
-            warnings: List[str] Warning details for further storage.
+            Errors: list[str] Error details for further storage.
+            warnings: list[str] Warning details for further storage.
         """
 
         # For each 'list' (input as a string with a , delimiter) entered,
@@ -648,7 +823,7 @@ def list_validation(
         # white spaces removed.
         errors = []
         warnings = []
-        manifest_col = manifest_col.astype(str)
+
         csv_re = comma_separated_list_regex()
 
         rule_parts = val_rule.lower().split(" ")
@@ -658,17 +833,29 @@ def list_validation(
             list_robustness = "strict"
 
         if list_robustness == "strict":
+            manifest_col = manifest_col.astype(str)
+
             # This will capture any if an entry is not formatted properly. Only for strict lists
             for i, list_string in enumerate(manifest_col):
-                if not re.fullmatch(csv_re, list_string):
+                list_error = None
+                entry_has_value = self.get_entry_has_value(
+                    entry=list_string,
+                    node_display_name=manifest_col.name,
+                )
+
+                if not isinstance(list_string, str) and entry_has_value:
+                    list_error = "not_a_string"
+                elif not re.fullmatch(csv_re, list_string) and entry_has_value:
                     list_error = "not_comma_delimited"
+
+                if list_error:
                     vr_errors, vr_warnings = GenerateError.generate_list_error(
                         list_string,
                         row_num=str(i + 2),
                         attribute_name=manifest_col.name,
                         list_error=list_error,
                         invalid_entry=manifest_col[i],
-                        dmge=dmge,
+                        dmge=self.dmge,
                         val_rule=val_rule,
                     )
                     if vr_errors:
@@ -685,8 +872,7 @@ def regex_validation(
         self,
         val_rule: str,
         manifest_col: pd.core.series.Series,
-        dmge: DataModelGraphExplorer,
-    ) -> (List[List[str]], List[List[str]]):
+    ) -> tuple[list[list[str]], list[list[str]]]:
         """
         Purpose:
             Check if values for a given manifest attribue conform to the reguar expression,
@@ -707,8 +893,8 @@ def regex_validation(
             - This function will return errors when the user input value
             does not match schema specifications.
             logger.error or logger.warning.
-            Errors: List[str] Error details for further storage.
-            warnings: List[str] Warning details for further storage.
+            Errors: list[str] Error details for further storage.
+            warnings: list[str] Warning details for further storage.
         TODO:
             move validation to convert step.
         """
@@ -727,9 +913,10 @@ def regex_validation(
         errors = []
         warnings = []
 
-        validation_rules = dmge.get_node_validation_rules(
+        validation_rules = self.dmge.get_node_validation_rules(
             node_display_name=manifest_col.name
         )
+
         if validation_rules and "::" in validation_rules[0]:
             validation_rules = validation_rules[0].split("::")
         # Handle case where validating re's within a list.
@@ -740,30 +927,43 @@ def regex_validation(
 
             for i, row_values in enumerate(manifest_col):
                 for j, re_to_check in enumerate(row_values):
-                    re_to_check = str(re_to_check)
-                    if not bool(module_to_call(reg_expression, re_to_check)) and bool(
-                        re_to_check
-                    ):
-                        vr_errors, vr_warnings = GenerateError.generate_regex_error(
-                            val_rule=val_rule,
-                            reg_expression=reg_expression,
-                            row_num=str(i + 2),
-                            module_to_call=reg_exp_rules[1],
-                            attribute_name=manifest_col.name,
-                            invalid_entry=manifest_col[i],
-                            dmge=dmge,
-                        )
-                        if vr_errors:
-                            errors.append(vr_errors)
-                        if vr_warnings:
-                            warnings.append(vr_warnings)
+                    entry_has_value = self.get_entry_has_value(
+                        entry=re_to_check,
+                        node_display_name=manifest_col.name,
+                    )
+                    if entry_has_value:
+                        re_to_check = str(re_to_check)
+                        if not bool(
+                            module_to_call(reg_expression, re_to_check)
+                        ) and bool(re_to_check):
+                            vr_errors, vr_warnings = GenerateError.generate_regex_error(
+                                val_rule=val_rule,
+                                reg_expression=reg_expression,
+                                row_num=str(i + 2),
+                                module_to_call=reg_exp_rules[1],
+                                attribute_name=manifest_col.name,
+                                invalid_entry=manifest_col[i],
+                                dmge=self.dmge,
+                            )
+                            if vr_errors:
+                                errors.append(vr_errors)
+                            if vr_warnings:
+                                warnings.append(vr_warnings)
 
         # Validating single re's
         else:
             manifest_col = manifest_col.astype(str)
             for i, re_to_check in enumerate(manifest_col):
-                if not bool(module_to_call(reg_expression, re_to_check)) and bool(
-                    re_to_check
+                # check if <NA> in list let pass.
+                entry_has_value = self.get_entry_has_value(
+                    entry=re_to_check,
+                    node_display_name=manifest_col.name,
+                )
+
+                if (
+                    not bool(module_to_call(reg_expression, re_to_check))
+                    and bool(re_to_check)
+                    and entry_has_value
                 ):
                     vr_errors, vr_warnings = GenerateError.generate_regex_error(
                         val_rule=val_rule,
@@ -772,7 +972,7 @@ def regex_validation(
                         module_to_call=reg_exp_rules[1],
                         attribute_name=manifest_col.name,
                         invalid_entry=manifest_col[i],
-                        dmge=dmge,
+                        dmge=self.dmge,
                     )
                     if vr_errors:
                         errors.append(vr_errors)
@@ -785,8 +985,7 @@ def type_validation(
         self,
         val_rule: str,
         manifest_col: pd.core.series.Series,
-        dmge: DataModelGraphExplorer,
-    ) -> (List[List[str]], List[List[str]]):
+    ) -> tuple[list[list[str]], list[list[str]]]:
         """
         Purpose:
             Check if values for a given manifest attribue are the same type
@@ -796,13 +995,12 @@ def type_validation(
                 'float', 'int', 'num', 'str'
             - manifest_col: pd.core.series.Series, column for a given
                 attribute in the manifest
-            - dmge: DataModelGraphExplorer Object
         Returns:
             -This function will return errors when the user input value
             does not match schema specifications.
             logger.error or logger.warning.
-            Errors: List[str] Error details for further storage.
-            warnings: List[str] Warning details for further storage.
+            Errors: list[str] Error details for further storage.
+            warnings: list[str] Warning details for further storage.
         TODO:
             Convert all inputs to .lower() just to prevent any entry errors.
         """
@@ -815,16 +1013,25 @@ def type_validation(
 
         errors = []
         warnings = []
+
         # num indicates either a float or int.
         if val_rule == "num":
             for i, value in enumerate(manifest_col):
-                if bool(value) and not isinstance(value, specified_type[val_rule]):
+                entry_has_value = self.get_entry_has_value(
+                    entry=value,
+                    node_display_name=manifest_col.name,
+                )
+                if (
+                    bool(value)
+                    and not isinstance(value, specified_type[val_rule])
+                    and entry_has_value
+                ):
                     vr_errors, vr_warnings = GenerateError.generate_type_error(
                         val_rule=val_rule,
                         row_num=str(i + 2),
                         attribute_name=manifest_col.name,
                         invalid_entry=str(manifest_col[i]),
-                        dmge=dmge,
+                        dmge=self.dmge,
                     )
                     if vr_errors:
                         errors.append(vr_errors)
@@ -832,13 +1039,21 @@ def type_validation(
                         warnings.append(vr_warnings)
         elif val_rule in ["int", "float", "str"]:
             for i, value in enumerate(manifest_col):
-                if bool(value) and not isinstance(value, specified_type[val_rule]):
+                entry_has_value = self.get_entry_has_value(
+                    entry=value,
+                    node_display_name=manifest_col.name,
+                )
+                if (
+                    bool(value)
+                    and not isinstance(value, specified_type[val_rule])
+                    and entry_has_value
+                ):
                     vr_errors, vr_warnings = GenerateError.generate_type_error(
                         val_rule=val_rule,
                         row_num=str(i + 2),
                         attribute_name=manifest_col.name,
                         invalid_entry=str(manifest_col[i]),
-                        dmge=dmge,
+                        dmge=self.dmge,
                     )
                     if vr_errors:
                         errors.append(vr_errors)
@@ -847,8 +1062,10 @@ def type_validation(
         return errors, warnings
 
     def url_validation(
-        self, val_rule: str, manifest_col: str, dmge: DataModelGraphExplorer
-    ) -> (List[List[str]], List[List[str]]):
+        self,
+        val_rule: str,
+        manifest_col: str,
+    ) -> tuple[list[list[str]], list[list[str]]]:
         """
         Purpose:
             Validate URL's submitted for a particular attribute in a manifest.
@@ -858,7 +1075,6 @@ def url_validation(
             - val_rule: str, Validation rule
             - manifest_col: pd.core.series.Series, column for a given
                 attribute in the manifest
-            - dmge: DataModelGraphExplorer Object
         Output:
             This function will return errors when the user input value
             does not match schema specifications.
@@ -869,46 +1085,23 @@ def url_validation(
         warnings = []
 
         for i, url in enumerate(manifest_col):
-            # Check if a random phrase, string or number was added and
-            # log the appropriate error.
-            if not isinstance(url, str) or not (
-                urlparse(url).scheme
-                + urlparse(url).netloc
-                + urlparse(url).params
-                + urlparse(url).query
-                + urlparse(url).fragment
-            ):
-                #
-                url_error = "random_entry"
-                valid_url = False
-                vr_errors, vr_warnings = GenerateError.generate_url_error(
-                    url,
-                    url_error=url_error,
-                    row_num=str(i + 2),
-                    attribute_name=manifest_col.name,
-                    argument=url_args,
-                    invalid_entry=manifest_col[i],
-                    dmge=dmge,
-                    val_rule=val_rule,
-                )
-                if vr_errors:
-                    errors.append(vr_errors)
-                if vr_warnings:
-                    warnings.append(vr_warnings)
-            else:
-                # add scheme to the URL if not currently added.
-                if not urlparse(url).scheme:
-                    url = "http://" + url
-                try:
-                    # Check that the URL points to a working webpage
-                    # if not log the appropriate error.
-                    request = Request(url)
-                    response = urlopen(request)
-                    valid_url = True
-                    response_code = response.getcode()
-                except:
+            entry_has_value = self.get_entry_has_value(
+                entry=url,
+                node_display_name=manifest_col.name,
+            )
+            if entry_has_value:
+                # Check if a random phrase, string or number was added and
+                # log the appropriate error. Specifically, Raise an error if the value added is not a string or no part
+                # of the string can be parsed as a part of a URL.
+                if not isinstance(url, str) or not (
+                    urlparse(url).scheme
+                    + urlparse(url).netloc
+                    + urlparse(url).params
+                    + urlparse(url).query
+                    + urlparse(url).fragment
+                ):
+                    url_error = "random_entry"
                     valid_url = False
-                    url_error = "invalid_url"
                     vr_errors, vr_warnings = GenerateError.generate_url_error(
                         url,
                         url_error=url_error,
@@ -916,215 +1109,837 @@ def url_validation(
                         attribute_name=manifest_col.name,
                         argument=url_args,
                         invalid_entry=manifest_col[i],
-                        dmge=dmge,
+                        dmge=self.dmge,
                         val_rule=val_rule,
                     )
                     if vr_errors:
                         errors.append(vr_errors)
                     if vr_warnings:
                         warnings.append(vr_warnings)
-                if valid_url == True:
-                    # If the URL works, check to see if it contains the proper arguments
-                    # as specified in the schema.
-                    for arg in url_args:
-                        if arg not in url:
-                            url_error = "arg_error"
-                            vr_errors, vr_warnings = GenerateError.generate_url_error(
-                                url,
-                                url_error=url_error,
-                                row_num=str(i + 2),
-                                attribute_name=manifest_col.name,
-                                argument=arg,
-                                invalid_entry=manifest_col[i],
-                                dmge=dmge,
-                                val_rule=val_rule,
-                            )
-                            if vr_errors:
-                                errors.append(vr_errors)
-                            if vr_warnings:
-                                warnings.append(vr_warnings)
+                else:
+                    # add scheme to the URL if not currently added.
+                    if not urlparse(url).scheme:
+                        url = "http://" + url
+                    try:
+                        # Check that the URL points to a working webpage
+                        # if not log the appropriate error.
+                        request = Request(url)
+                        response = urlopen(request)
+                        valid_url = True
+                        response_code = response.getcode()
+                    except:
+                        valid_url = False
+                        url_error = "invalid_url"
+                        vr_errors, vr_warnings = GenerateError.generate_url_error(
+                            url,
+                            url_error=url_error,
+                            row_num=str(i + 2),
+                            attribute_name=manifest_col.name,
+                            argument=url_args,
+                            invalid_entry=manifest_col[i],
+                            dmge=self.dmge,
+                            val_rule=val_rule,
+                        )
+                        if vr_errors:
+                            errors.append(vr_errors)
+                        if vr_warnings:
+                            warnings.append(vr_warnings)
+                    if valid_url == True:
+                        # If the URL works, check to see if it contains the proper arguments
+                        # as specified in the schema.
+                        for arg in url_args:
+                            if arg not in url:
+                                url_error = "arg_error"
+                                (
+                                    vr_errors,
+                                    vr_warnings,
+                                ) = GenerateError.generate_url_error(
+                                    url,
+                                    url_error=url_error,
+                                    row_num=str(i + 2),
+                                    attribute_name=manifest_col.name,
+                                    argument=arg,
+                                    invalid_entry=manifest_col[i],
+                                    dmge=self.dmge,
+                                    val_rule=val_rule,
+                                )
+                                if vr_errors:
+                                    errors.append(vr_errors)
+                                if vr_warnings:
+                                    warnings.append(vr_warnings)
         return errors, warnings
 
-    def cross_validation(
+    def _parse_validation_log(
+        self, validation_log: dict[str, pd.core.series.Series]
+    ) -> tuple[[list[str], list[str], list[str]]]:
+        """Parse validation log, so values can be used to raise warnings/errors
+        Args:
+            validation_log, dict[str, pd.core.series.Series]:
+        Returns:
+            invalid_rows, list: invalid rows recorded in the validation log
+            invalid_enties, list: invalid values recorded in the validation log
+            manifest_ids, list:
+        """
+        # Initialize parameters
+        validation_rows, validation_values = [], []
+
+        manifest_entries = list(validation_log.values())
+        manifest_ids = list(validation_log.keys())
+        for entry in manifest_entries:
+            # Increment pandas row values by 2 so they will match manifest sheet row values.
+            validation_rows.append(entry.index[0] + 2)
+            validation_values.append(entry.values[0])
+
+        # Parse invalid_rows and invalid_values so they can be used to raise warnings/errors
+        invalid_rows = iterable_to_str_list(set(validation_rows))
+        invalid_entries = iterable_to_str_list(set(validation_values))
+
+        return invalid_rows, invalid_entries, manifest_ids
+
+    def _merge_format_invalid_rows_values(
+        self, series_1: pd.core.series.Series, series_2: pd.core.series.Series
+    ) -> tuple[[list[str], list[str]]]:
+        """Merge two series to identify gather all invalid values, and parse out invalid rows and entries
+        Args:
+            series_1, pd.core.series.Series: first set of invalid values to extract
+            series_2, pd.core.series.Series: second set of invalid values to extract
+        Returns:
+            invalid_rows, list: invalid rows taken from both series
+            invalid_entry, list: invalid values taken from both series
+        """
+        # Merge series to get invalid values and rows
+        invalid_values = pd.merge(series_1, series_2, how="outer")
+        # Increment pandas row values by 2 so they will match manifest sheet row values.
+        invalid_rows = (
+            pd.merge(
+                series_1,
+                series_2,
+                how="outer",
+                left_index=True,
+                right_index=True,
+            ).index.to_numpy()
+            + 2
+        )
+        # Parse invalid_rows and invalid_values so they can be used to raise warnings/errors
+        invalid_rows = np_array_to_str_list(invalid_rows)
+        invalid_entry = iterable_to_str_list(invalid_values.squeeze())
+        return invalid_rows, invalid_entry
+
+    def _format_invalid_row_values(
+        self, invalid_values: dict[str, pd.core.series.Series]
+    ) -> tuple[[list[str], list[str]]]:
+        """Parse invalid_values dictionary, to extract invalid_rows and invalid_entry to be used later
+        to raise warnings or errors.
+        Args:
+            invalid_values, dict[str, pd.core.series.Series]:
+        Returns:
+            invalid_rows, list: invalid rows recorded in invalid_values
+            invalid_entry, list: invalid values recorded in invalid_values
+        """
+        # Increment pandas row values by 2 so they will match manifest sheet row values.
+        invalid_rows = invalid_values.index.to_numpy() + 2
+
+        # Parse invalid_rows and invalid_values so they can be used to raise warnings/errors
+        invalid_rows = np_array_to_str_list(invalid_rows)
+        invalid_entry = iterable_to_str_list(invalid_values)
+        return invalid_rows, invalid_entry
+
+    def _gather_set_warnings_errors(
+        self,
+        val_rule: str,
+        source_attribute: str,
+        set_validation_store: tuple[
+            dict[str, pd.core.series.Series],
+            list[str],
+            dict[str, pd.core.series.Series],
+        ],
+    ) -> tuple[[list[str], list[str]]]:
+        """Based on the cross manifest validation rule, and in set rule scope, pass variables to
+        _get_cross_errors_warnings
+            to log appropriate error or warning.
+        Args:
+            val_rule, str: Validation Rule
+            source_attribute, str: Source manifest column name
+            set_validation_store, tuple[dict[str, pd.core.series.Series], list[string],
+            dict[str, pd.core.series.Series]]:
+                contains the missing_manifest_log, present_manifest_log, and repeat_manifest_log
+            dmge: DataModelGraphExplorer Object.
+
+        Returns:
+            errors, list[str]: list of errors to raise, as appropriate, if values in current manifest do
+            not pass relevant cross mannifest validation across the target manifest(s)
+            warnings, list[str]: list of warnings to raise, as appropriate, if values in current manifest do
+            not pass relevant cross mannifest validation across the target manifest(s)
+        """
+        errors, warnings = [], []
+
+        (
+            missing_manifest_log,
+            present_manifest_log,
+            repeat_manifest_log,
+        ) = set_validation_store
+
+        # Process error handling by rule
+        if "matchAtLeastOne" in val_rule and len(present_manifest_log) < 1:
+            (
+                invalid_rows,
+                invalid_entries,
+                manifest_ids,
+            ) = self._parse_validation_log(validation_log=missing_manifest_log)
+            errors, warnings = self._get_cross_errors_warnings(
+                val_rule=val_rule,
+                row_num=invalid_rows,
+                attribute_name=source_attribute,
+                invalid_entry=invalid_entries,
+                manifest_id=manifest_ids,
+            )
+
+        elif "matchExactlyOne" in val_rule and len(present_manifest_log) != 1:
+            errors, warnings = self._get_cross_errors_warnings(
+                val_rule=val_rule,
+                attribute_name=source_attribute,
+                matching_manifests=present_manifest_log,
+            )
+
+        elif "matchNone" in val_rule and repeat_manifest_log:
+            (
+                invalid_rows,
+                invalid_entries,
+                manifest_ids,
+            ) = self._parse_validation_log(validation_log=repeat_manifest_log)
+            errors, warnings = self._get_cross_errors_warnings(
+                val_rule=val_rule,
+                row_num=invalid_rows,
+                attribute_name=source_attribute,
+                invalid_entry=invalid_entries,
+                manifest_id=manifest_ids,
+            )
+
+        return errors, warnings
+
+    def _remove_non_entry_from_invalid_entry_list(
+        self,
+        invalid_entry: Optional[list[str]],
+        row_num: Optional[list[str]],
+        attribute_name: str,
+    ) -> tuple[list[str], list[str]]:
+        """Helper to remove NAs from a list of invalid entries (if applicable, and allowed), remove the row
+        too from row_num. This will make sure errors are not rasied for NA entries unless the value is required.
+        Args:
+            invalid_entry, list[str]: default=None, list of entries in the source manifest where
+                invalid values were located.
+            row_num, list[str[: default=None, list of rows in the source manifest where invalid values were located
+            attribute_name, str: source attribute name
+        Returns:
+            invalid_entry and row_num returned with any NA and corresponding row index value removed, if applicable.
+        """
+        idx_to_remove = []
+        # Check if the current attribute column is required, via the data model
+        if invalid_entry and row_num:
+            # Check each invalid entry and determine if it has a value and/or is required.
+            # If there is no entry and its not required, remove the NA value so an error is not raised.
+            for idx, entry in enumerate(invalid_entry):
+                entry_has_value = self.get_entry_has_value(entry, attribute_name)
+                # If there is no value, and is not required, recored the index
+                if not entry_has_value:
+                    idx_to_remove.append(idx)
+
+            # If indices are recorded for NA values to remove, remove them and the corresponding row
+            if idx_to_remove:
+                for idx in sorted(idx_to_remove, reverse=True):
+                    del invalid_entry[idx]
+                    del row_num[idx]
+                # Perform check to make sure length of invalid_entry and row_num is the same. If not that would suggest
+                # there was an issue recording or removing values.
+                if len(invalid_entry) != len(row_num):
+                    logger.error(
+                        f"There was an error handling and validating a non-entry."
+                        f"Please try again or contact Schematic administrators."
+                    )
+        return invalid_entry, row_num
+
+    def _get_cross_errors_warnings(
+        self,
+        val_rule: str,
+        attribute_name: str,
+        row_num: Optional[list[str]] = None,
+        matching_manifests: Optional[list[str]] = None,
+        manifest_id: Optional[list[str]] = None,
+        invalid_entry: Optional[list[str]] = None,
+    ) -> tuple[[list[str], list[str]]]:
+        """Helper to call GenerateError.generate_cross_warning in a consistent way, gather warnings and errors.
+        Args:
+            val_rule, str: Validation Rule
+            attribute_name, str: source attribute name
+            row_num, list: default=None, list of rows in the source manifest where invalid values were located
+            matching_manifests, list: default=None, ist of manifests with all values in the target attribute present
+            manifest_id, list: default=None, list of manifests where invalid values were located.
+            invalid_entry, list: default=None, list of entries in the source manifest where invalid values were located.
+        Returns:
+            errors, list[str]: list of errors to raise, as appropriate, if values in current manifest do
+            not pass relevant cross mannifest validation across the target manifest(s)
+            warnings, list[str]: list of warnings to raise, as appropriate, if values in current manifest do
+            not pass relevant cross mannifest validation across the target manifest(s)
+        """
+        invalid_entry, row_num = self._remove_non_entry_from_invalid_entry_list(
+            invalid_entry, row_num, attribute_name
+        )
+        errors, warnings = [], []
+
+        # Want to make sure we only generate errors when appropriate. Dont call, if we have removed all Nans,
+        # Also rules either require an invalid entry OR matching manifests. So let pass if either of those
+        # thresholds are met.
+        if invalid_entry or matching_manifests:
+            if not invalid_entry:
+                invalid_entry = "No Invalid Entry Recorded"
+            vr_errors, vr_warnings = GenerateError.generate_cross_warning(
+                val_rule=val_rule,
+                attribute_name=attribute_name,
+                dmge=self.dmge,
+                matching_manifests=matching_manifests,
+                manifest_id=manifest_id,
+                invalid_entry=invalid_entry,
+                row_num=row_num,
+            )
+            if vr_errors:
+                errors.append(vr_errors)
+            if vr_warnings:
+                warnings.append(vr_warnings)
+        return errors, warnings
+
+    def _gather_value_warnings_errors(
+        self,
+        val_rule: str,
+        source_attribute: str,
+        value_validation_store: tuple[
+            dict[str, pd.core.series.Series],
+            dict[str, pd.core.series.Series],
+            dict[str, pd.core.series.Series],
+        ],
+    ) -> tuple[[list[str], list[str]]]:
+        """For value rule scope, find invalid rows and entries, and generate appropriate errors and warnings
+        Args:
+            val_rule, str: Validation rule
+            source_attribute, str: source manifest column name
+            value_validation_store, tuple(dict[str, pd.core.series.Series], dict[str, pd.core.series.Series],
+                dict[str, pd.core.series.Series]):
+                contains missing_values, duplicated_values, and repeat values
+        Returns:
+            errors, list[str]: list of errors to raise, as appropriate, if values in current manifest do
+            not pass relevant cross mannifest validation across the target manifest(s)
+            warnings, list[str]: list of warnings to raise, as appropriate, if values in current manifest do
+            not pass relevant cross mannifest validation across the target manifest(s)
+        """
+        # Initialize with empty lists
+        errors, warnings = [], []
+        invalid_rows, invalid_entry = [], []
+
+        # Unpack the value_validation_store
+        (missing_values, duplicated_values, repeat_values) = value_validation_store
+
+        # Determine invalid rows and entries based on the rule type
+        if "matchAtLeastOne" in val_rule and not missing_values.empty:
+            invalid_rows, invalid_entry = self._format_invalid_row_values(
+                missing_values
+            )
+
+        elif "matchExactlyOne" in val_rule and (
+            duplicated_values.any() or missing_values.any()
+        ):
+            (
+                invalid_rows,
+                invalid_entry,
+            ) = self._merge_format_invalid_rows_values(
+                duplicated_values, missing_values
+            )
+
+        elif "matchNone" in val_rule and repeat_values.any():
+            invalid_rows, invalid_entry = self._format_invalid_row_values(repeat_values)
+
+        # If invalid rows/entries found, raise warning/error
+        if invalid_rows and invalid_entry:
+            errors, warnings = self._get_cross_errors_warnings(
+                val_rule=val_rule,
+                row_num=invalid_rows,
+                attribute_name=source_attribute,
+                invalid_entry=invalid_entry,
+            )
+        return errors, warnings
+
+    def _check_if_target_manifest_is_empty(
+        self,
+        target_manifest: pd.core.series.Series,
+        target_manifest_empty: list[bool],
+        column_names: dict[str, str],
+    ) -> list[bool]:
+        """If a target manifest is found with the attribute column of interest check to see if the manifest is empty.
+        Args:
+            target_manifest, pd.core.series.Series: Current target manifest
+            target_manifest_empty, list[bool]: a list of booleans recording if the target manifest are emtpy or not.
+            column_names, dict[str, str]: {stripped_col_name:original_column_name}
+        Returns:
+            target_manifest_empty, list[bool]: a list of booleans recording if the target manifest are emtpy or not.
+        """
+        # Make a copy of the target manifest with only user uploaded columns
+        target_manifest_dupe = target_manifest.drop(
+            [column_names["component"], column_names["id"], column_names["entityid"]],
+            axis=1,
+        )
+
+        # Check if the entire target_manifest is empty
+        if sum(target_manifest_dupe.isnull().values.all(axis=0)) == len(
+            target_manifest_dupe.columns
+        ):
+            target_manifest_empty.append(True)
+        else:
+            target_manifest_empty.append(False)
+        return target_manifest_empty
+
+    def _run_validation_across_targets_set(
         self,
         val_rule: str,
+        column_names: dict[str, str],
         manifest_col: pd.core.series.Series,
-        project_scope: List,
-        dmge: DataModelGraphExplorer,
-        access_token: str,
-    ) -> List[List[str]]:
+        target_attribute: str,
+        target_manifest: pd.core.series.Series,
+        target_manifest_id: str,
+        missing_manifest_log: dict[str, pd.core.series.Series],
+        present_manifest_log: list[str],
+        repeat_manifest_log: dict[str, pd.core.series.Series],
+        target_attribute_in_manifest_list: list[bool],
+        target_manifest_empty: list[bool],
+    ) -> tuple[
+        tuple[
+            dict[str, pd.core.series.Series],
+            list[str],
+            dict[str, pd.core.series.Series],
+        ],
+        list[bool],
+        list[bool],
+    ]:
+        """For set rule scope, go through the given target column and look
+        Args:
+            val_rule, str: Validation rule
+            column_names, dict[str,str]: {stripped_col_name:original_column_name}
+            target_column, pd.core.series.Series: Empty target_column to fill out in this function
+            manifest_col, pd.core.series.Series: Source manifest column
+            target_attribute, str: current target attribute
+            target_column, pd.core.series.Series: Current target column
+            target_manifest, pd.core.series.Series: Current target manifest
+            target_manifest_id, str: Current target manifest Synapse ID
+            missing_manifest_log, dict[str, pd.core.series.Series]:
+                Log of manifests with missing values, {synapse_id: index,missing value}, updated.
+            present_manifest_log, list[str]
+                Log of present manifests, [synapse_id present manifest], updated.
+            repeat_manifest_log, dict[str, pd.core.series.Series]
+                Log of manifests with repeat values, {synapse_id: index,repeat value}, updated.
+
+        Returns:
+            tuple(
+            missing_manifest_log, dict[str, pd.core.series.Series]:
+                Log of manifests with missing values, {synapse_id: index,missing value}, updated.
+            present_manifest_log, list[str]
+                Log of present manifests, [synapse_id present manifest], updated.
+            repeat_manifest_log, dict[str, pd.core.series.Series]
+                Log of manifests with repeat values, {synapse_id: index,repeat value}, updated.)
+            target_attribute_in_manifest, bool: True if the target attribute is in the current manifest.
         """
-        Purpose:
-            Do cross validation between the current manifest and all other manifests a user has access to on Synapse.
-            Check if values in this manifest are present fully in others.
-        Input:
-            - val_rule: str, Validation rule
-            - manifest_col: pd.core.series.Series, column for a given
-                attribute in the manifest
-            - dmge: DataModelGraphExplorer Object
-        Output:
-            This function will return errors when values in the current manifest's attribute
-            are not fully present in the correct amount of other manifests.
+        target_attribute_in_manifest = False
+        # If the manifest has the target attribute for the component do the cross validation
+        if target_attribute in column_names:
+            target_attribute_in_manifest = True
+            target_column = target_manifest[column_names[target_attribute]]
+
+            target_manifest_empty = self._check_if_target_manifest_is_empty(
+                target_manifest=target_manifest,
+                target_manifest_empty=target_manifest_empty,
+                column_names=column_names,
+            )
+
+            # Do the validation on both columns
+            if "matchNone" in val_rule:
+                # Look for repeats between the source manifest and target_column, if there are
+                # repeats log the repeat value and manifest
+                repeat_values = manifest_col[manifest_col.isin(target_column)]
+
+                if repeat_values.any():
+                    repeat_manifest_log[target_manifest_id] = repeat_values
+            else:
+                # Determine elements in manifest column that are missing from the target column
+                missing_values = manifest_col[~manifest_col.isin(target_column)]
+
+                if missing_values.empty:
+                    # If there are no missing values in the target column, log this manifest as
+                    # one where all items are present
+                    present_manifest_log.append(target_manifest_id)
+                else:
+                    # If there are missing values in the target manifest, log the manifest
+                    # and the missing values.
+                    missing_manifest_log[target_manifest_id] = missing_values
+        target_attribute_in_manifest_list.append(target_attribute_in_manifest)
+        return (
+            (
+                missing_manifest_log,
+                present_manifest_log,
+                repeat_manifest_log,
+            ),
+            target_attribute_in_manifest_list,
+            target_manifest_empty,
+        )
+
+    def _gather_target_columns_value(
+        self,
+        column_names: dict[str, str],
+        target_attribute: str,
+        concatenated_target_column: pd.core.series.Series,
+        target_manifest: pd.core.series.Series,
+        target_attribute_in_manifest_list: list[bool],
+        target_manifest_empty: list[bool],
+    ) -> tuple[pd.core.series.Series, list[bool], list[bool],]:
+        """A helper function for creating a concatenating all target attribute columns across all target manifest.
+            This function checks if the target attribute is in the current target manifest. If it is, and is the
+            first manifest with this column, start recording it, if it has already been recorded from
+            another manifest concatenate the new column to the concatenated_target_column series.
+        Args:
+            column_names, dict: {stripped_col_name:original_column_name}
+            target_attribute, str: current target attribute
+            concatenated_target_column, pd.core.series.Series: target column in the process of being built, possibly
+                passed through this function multiple times based on the number of manifests
+            target_manifest, pd.core.series.Series: current target manifest
+        Returns:
+            concatenated_target_column, pd.core.series.Series: All target columns concatenated into a single column
         """
-        errors = []
-        warnings = []
+        # Check if the target_attribute is in the current target manifest.
+        target_attribute_in_manifest = False
+        if target_attribute in column_names:
+            target_attribute_in_manifest = True
+
+            # If it is, make sure the column names match the original column names
+            target_manifest.rename(
+                columns={column_names[target_attribute]: target_attribute},
+                inplace=True,
+            )
+            # If matches with other columns have already been found, concatenate current target attribute column to
+            # the series
+            if concatenated_target_column.any():
+                concatenated_target_column = pd.concat(
+                    objs=[
+                        concatenated_target_column,
+                        target_manifest[target_attribute],
+                    ],
+                    join="outer",
+                    ignore_index=True,
+                )
+            else:
+                # Otherwise, start recording the target_attribute column
+                concatenated_target_column = target_manifest[target_attribute]
+
+            concatenated_target_column = concatenated_target_column.astype("object")
+
+        target_manifest_empty = self._check_if_target_manifest_is_empty(
+            target_manifest, target_manifest_empty, column_names
+        )
+        target_attribute_in_manifest_list.append(target_attribute_in_manifest)
+        return (
+            concatenated_target_column,
+            target_attribute_in_manifest_list,
+            target_manifest_empty,
+        )
+
+    def _run_validation_across_targets_value(
+        self,
+        manifest_col: pd.core.series.Series,
+        concatenated_target_column: pd.core.series.Series,
+    ) -> tuple[[pd.core.series.Series, pd.core.series.Series, pd.core.series.Series]]:
+        """Get missing values, duplicated values and repeat values assesed comapring the source manifest to all
+            the values in all target columns.
+        Args:
+            manifest_col, pd.core.series.Series: Current source manifest column
+            concatenated_target_column, pd.core.series.Series: All target columns concatenated into a single column
+        Returns:
+            missing_values, pd.core.series.Series: values that are present in the source manifest, but not present
+                in the target manifest
+            duplicated_values, pd.core.series.Series: values that duplicated in the concatenated target column, and
+                also present in the source manifest column
+            repeat_values, pd.core.series.Series: values that are repeated between the manifest column and
+                concatenated target column
+        """
+        # Find values that are present in the source manifest, but not present in the target manifest
+        missing_values = manifest_col[~manifest_col.isin(concatenated_target_column)]
+
+        # Find values that duplicated in the concatenated target column, and also present in the source manifest column
+        duplicated_values = manifest_col[
+            manifest_col.isin(
+                concatenated_target_column[concatenated_target_column.duplicated()]
+            )
+        ]
+
+        # Find values that are repeated between the manifest column and concatenated target column
+        repeat_values = manifest_col[manifest_col.isin(concatenated_target_column)]
+
+        return missing_values, duplicated_values, repeat_values
+
+    def _get_column_names(
+        self, target_manifest: pd.core.series.Series
+    ) -> dict[str, str]:
+        """Convert manifest column names into validation rule input format
+        Args:
+            target_manifest, pd.core.series.Series: Current target manifest
+        Returns:
+            column_names, dict[str,str]: {stripped_col_name:original_column_name}
+        """
+        column_names = {}
+        for original_column_name in target_manifest.columns:
+            stripped_col_name = original_column_name.replace(" ", "").lower()
+            column_names[stripped_col_name] = original_column_name
+        return column_names
+
+    def _get_rule_scope(self, val_rule: str) -> ScopeTypes:
+        """Parse scope from validation rule
+        Args:
+            val_rule, str: Validation Rule
+        Returns:
+            scope, ScopeTypes: The scope of the rule, taken from validation rule
+        """
+        scope = val_rule.lower().split(" ")[2]
+        return scope
+
+    def _run_validation_across_target_manifests(
+        self,
+        project_scope: Optional[list[str]],
+        rule_scope: ScopeTypes,
+        access_token: str,
+        val_rule: str,
+        manifest_col: pd.core.series.Series,
+        target_column: pd.core.series.Series,
+    ) -> tuple[
+        float,
+        Union[
+            Union[
+                tuple[
+                    dict[str, pd.core.series.Series],
+                    list[str],
+                    dict[str, pd.core.series.Series],
+                ],
+                tuple[
+                    dict[str, pd.core.series.Series],
+                    dict[str, pd.core.series.Series],
+                    dict[str, pd.core.series.Series],
+                ],
+            ],
+            bool,
+            str,
+        ],
+    ]:
+        """Run cross manifest validation from a source manifest, across all relevant target manifests,
+            based on scope. Output start time and validation outputs..
+        Args:
+            project_scope, Optional[list]: Projects to limit the scope of cross manifest validation to.
+            rule_scope, ScopeTypes: The scope of the rule, taken from validation rule
+            access_token, str: Asset Store access token
+            val_rule, str: Validation rule.
+            manifest_col, pd.core.series.Series: Source manifest column for a given source component
+            target_column, pd.core.series.Series: Empty target_column to fill out in this function
+        Returns:
+            start_time, float: start time in fractional seconds
+            valdiation_output:
+                Union:
+                    target_attribute_in_manifest, bool: will return a false boolean if no target manfiest are found.
+                    "values not recorded in targets stored", str, will return a string if targets were found, but there
+                        was no data in the target.
+                    Union[
+                        tuple[dict[str, pd.core.series.Series], list[str], dict[str, pd.core.series.Series]],
+                        tuple[dict[str, pd.core.series.Series], dict[str, pd.core.series.Series],
+                            dict[str, pd.core.series.Series]]:
+                            validation outputs, exact types depend on scope,
+        """
+        # Initialize variables
+        present_manifest_log = []
+        duplicated_values = {}
         missing_values = {}
+        repeat_values = {}
         missing_manifest_log = {}
-        present_manifest_log = []
-        target_column = pd.Series(dtype=object)
-        # parse sources and targets
-        source_attribute = manifest_col.name
+        repeat_manifest_log = {}
+        target_attribute_in_manifest_list = []
+        target_manifest_empty = []
+
+        target_attribute_in_manifest = False
+
+        # Set relevant parameters
         [target_component, target_attribute] = val_rule.lower().split(" ")[1].split(".")
-        scope = val_rule.lower().split(" ")[2]
         target_column.name = target_attribute
 
         # Get IDs of manifests with target component
         (
             synStore,
-            target_manifest_IDs,
-            target_dataset_IDs,
-        ) = ValidateAttribute.get_target_manifests(
-            target_component, project_scope, access_token
-        )
+            target_manifest_ids,
+            target_dataset_ids,
+        ) = self.get_target_manifests(target_component, project_scope, access_token)
 
-        t_cross_manifest = perf_counter()
-        # Read each manifest
-        for target_manifest_ID, target_dataset_ID in zip(
-            target_manifest_IDs, target_dataset_IDs
+        # Start timer
+        start_time = perf_counter()
+
+        # For each target manifest, gather target manifest column and compare to the source manifest column
+        # Save relevant data as appropriate for the given scope
+        for target_manifest_id, target_dataset_id in zip(
+            target_manifest_ids, target_dataset_ids
         ):
+            # Pull manifest from Synapse
             entity = synStore.getDatasetManifest(
-                datasetId=target_dataset_ID, downloadFile=True
+                datasetId=target_dataset_id, downloadFile=True
             )
+            # Load manifest
             target_manifest = pd.read_csv(entity.path)
 
-            # convert manifest column names into validation rule input format -
-            column_names = {}
-            for name in target_manifest.columns:
-                column_names[name.replace(" ", "").lower()] = name
-
-            if scope.__contains__("set"):
-                # If the manifest has the target attribute for the component do the cross validation
-                if target_attribute in column_names:
-                    target_column = target_manifest[column_names[target_attribute]]
-
-                    # Do the validation on both columns
-                    missing_values = manifest_col[~manifest_col.isin(target_column)]
-
-                    if missing_values.empty:
-                        present_manifest_log.append(target_manifest_ID)
-                    else:
-                        missing_manifest_log[target_manifest_ID] = missing_values
-
-            elif scope.__contains__("value"):
-                if target_attribute in column_names:
-                    target_manifest.rename(
-                        columns={column_names[target_attribute]: target_attribute},
-                        inplace=True,
-                    )
-
-                    target_column = pd.concat(
-                        objs=[target_column, target_manifest[target_attribute]],
-                        join="outer",
-                        ignore_index=True,
-                    )
-                    target_column = target_column.astype("object")
-                    # print(target_column)
-
-        missing_rows = []
-        missing_values = []
-
-        if scope.__contains__("value"):
-            missing_values = manifest_col[~manifest_col.isin(target_column)]
-            duplicated_values = manifest_col[
-                manifest_col.isin(target_column[target_column.duplicated()])
-            ]
-
-            if val_rule.__contains__("matchAtLeastOne") and not missing_values.empty:
-                missing_rows = missing_values.index.to_numpy() + 2
-                missing_rows = np_array_to_str_list(missing_rows)
-                vr_errors, vr_warnings = GenerateError.generate_cross_warning(
+            # Get manifest column names
+            column_names = self._get_column_names(target_manifest=target_manifest)
+
+            # Read each target manifest and run validation of current manifest column (set) against each
+            # manifest individually, gather results
+            if "set" in rule_scope:
+                (
+                    (
+                        missing_manifest_log,
+                        present_manifest_log,
+                        repeat_manifest_log,
+                    ),
+                    target_attribute_in_manifest_list,
+                    target_manifest_empty,
+                ) = self._run_validation_across_targets_set(
                     val_rule=val_rule,
-                    row_num=missing_rows,
-                    attribute_name=source_attribute,
-                    invalid_entry=iterable_to_str_list(missing_values),
-                    dmge=dmge,
+                    column_names=column_names,
+                    manifest_col=manifest_col,
+                    target_attribute=target_attribute,
+                    target_manifest=target_manifest,
+                    target_manifest_id=target_manifest_id,
+                    missing_manifest_log=missing_manifest_log,
+                    present_manifest_log=present_manifest_log,
+                    repeat_manifest_log=repeat_manifest_log,
+                    target_attribute_in_manifest_list=target_attribute_in_manifest_list,
+                    target_manifest_empty=target_manifest_empty,
                 )
-                if vr_errors:
-                    errors.append(vr_errors)
-                if vr_warnings:
-                    warnings.append(vr_warnings)
-            elif val_rule.__contains__("matchExactlyOne") and (
-                duplicated_values.any() or missing_values.any()
-            ):
-                invalid_values = pd.merge(
-                    duplicated_values, missing_values, how="outer"
+
+            # Concatenate target manifest columns, in a subsequent step will run cross manifest validation from
+            # the current manifest
+            # column values against the concatenated target column
+            if "value" in rule_scope:
+                (
+                    target_column,
+                    target_attribute_in_manifest_list,
+                    target_manifest_empty,
+                ) = self._gather_target_columns_value(
+                    column_names=column_names,
+                    target_attribute=target_attribute,
+                    concatenated_target_column=target_column,
+                    target_manifest=target_manifest,
+                    target_attribute_in_manifest_list=target_attribute_in_manifest_list,
+                    target_manifest_empty=target_manifest_empty,
                 )
-                invalid_rows = (
-                    pd.merge(
-                        duplicated_values,
-                        missing_values,
-                        how="outer",
-                        left_index=True,
-                        right_index=True,
-                    ).index.to_numpy()
-                    + 2
+
+        if len(target_attribute_in_manifest_list) > 0:
+            if sum(target_attribute_in_manifest_list) == 0:
+                target_attribute_in_manifest = False
+            else:
+                target_attribute_in_manifest = True
+
+        # Check if a target manifest has been found, if so do not export validation_store
+        if not target_attribute_in_manifest:
+            return (start_time, target_attribute_in_manifest)
+        elif sum(target_manifest_empty) > 0:
+            return (start_time, "values not recorded in targets stored")
+        else:
+            # Store outputs according to the scope for which they are used.
+            if "set" in rule_scope:
+                validation_store = (
+                    missing_manifest_log,
+                    present_manifest_log,
+                    repeat_manifest_log,
                 )
-                invalid_rows = np_array_to_str_list(invalid_rows)
-                vr_errors, vr_warnings = GenerateError.generate_cross_warning(
-                    val_rule=val_rule,
-                    row_num=invalid_rows,
-                    attribute_name=source_attribute,
-                    invalid_entry=iterable_to_str_list(invalid_values.squeeze()),
-                    dmge=dmge,
+
+            elif "value" in rule_scope:
+                # From the concatenated target column, for value scope, run validation
+                (
+                    missing_values,
+                    duplicated_values,
+                    repeat_values,
+                ) = self._run_validation_across_targets_value(
+                    manifest_col=manifest_col,
+                    concatenated_target_column=target_column,
                 )
-                if vr_errors:
-                    errors.append(vr_errors)
-                if vr_warnings:
-                    warnings.append(vr_warnings)
-
-        # generate warnings if necessary
-        elif scope.__contains__("set"):
-            if (
-                val_rule.__contains__("matchAtLeastOne")
-                and len(present_manifest_log) < 1
-            ):
-                missing_entries = list(missing_manifest_log.values())
-                missing_manifest_IDs = list(missing_manifest_log.keys())
-                for missing_entry in missing_entries:
-                    missing_rows.append(missing_entry.index[0] + 2)
-                    missing_values.append(missing_entry.values[0])
-
-                missing_rows = iterable_to_str_list(set(missing_rows))
-                missing_values = iterable_to_str_list(set(missing_values))
-
-                vr_errors, vr_warnings = GenerateError.generate_cross_warning(
+                validation_store = (missing_values, duplicated_values, repeat_values)
+            return (start_time, validation_store)
+
+    def cross_validation(
+        self,
+        val_rule: str,
+        manifest_col: pd.core.series.Series,
+        project_scope: Optional[list[str]],
+        access_token: str,
+    ) -> list[list[str]]:
+        """
+        Purpose:
+            Do cross validation between the current manifest and all other manifests in a given asset view (limited
+                by project scope, if provided).
+        Args:
+            val_rule, str: Validation rule
+            manifest_col, pd.core.series.Series: column for a given
+                attribute in the manifest
+            project_scope, Optional[list] = None: Projects to limit the scope of cross manifest validation to.
+            dmge: DataModelGraphExplorer Object
+            access_token, str: Asset Store access token
+        Returns:
+            errors, warnings, list[list[str]]: raise warnings and errors as appropriate if values in current manifest do
+            no pass relevant cross mannifest validation across the target manifest(s)
+        """
+        # Initialize target_column
+        target_column = pd.Series(dtype=object)
+
+        # Get the rule_scope from the validation rule
+        rule_scope = self._get_rule_scope(val_rule)
+
+        # Run validation from source to target manifest(s), gather outputs
+        (
+            start_time,
+            validation_output,
+        ) = self._run_validation_across_target_manifests(
+            project_scope=project_scope,
+            rule_scope=rule_scope,
+            access_token=access_token,
+            val_rule=val_rule,
+            manifest_col=manifest_col,
+            target_column=target_column,
+        )
+
+        # If there are no target_columns to validate against, assume this is the first manifest being submitted and
+        # allow users to just submit.
+        if isinstance(validation_output, bool) and not validation_output:
+            errors = []
+            warnings = GenerateError.generate_no_cross_warning(
+                dmge=self.dmge, attribute_name=manifest_col.name, val_rule=val_rule
+            )
+        elif (
+            isinstance(validation_output, str)
+            and validation_output == "values not recorded in targets stored"
+        ):
+            errors, warnings = GenerateError.generate_no_value_in_manifest_error(
+                dmge=self.dmge, attribute_name=manifest_col.name, val_rule=val_rule
+            )
+
+        elif isinstance(validation_output, tuple):
+            # Raise warnings/errors based on validation output and rule_scope.
+            if "set" in rule_scope:
+                errors, warnings = self._gather_set_warnings_errors(
                     val_rule=val_rule,
-                    row_num=missing_rows,
-                    attribute_name=source_attribute,
-                    invalid_entry=missing_values,
-                    missing_manifest_ID=missing_manifest_IDs,
-                    dmge=dmge,
+                    source_attribute=manifest_col.name,
+                    set_validation_store=validation_output,
                 )
-                if vr_errors:
-                    errors.append(vr_errors)
-                if vr_warnings:
-                    warnings.append(vr_warnings)
-            elif (
-                val_rule.__contains__("matchExactlyOne")
-                and len(present_manifest_log) != 1
-            ):
-                vr_errors, vr_warnings = GenerateError.generate_cross_warning(
+            elif "value" in rule_scope:
+                errors, warnings = self._gather_value_warnings_errors(
                     val_rule=val_rule,
-                    attribute_name=source_attribute,
-                    matching_manifests=present_manifest_log,
-                    dmge=dmge,
+                    source_attribute=manifest_col.name,
+                    value_validation_store=validation_output,
                 )
-                if vr_errors:
-                    errors.append(vr_errors)
-                if vr_warnings:
-                    warnings.append(vr_warnings)
 
-        logger.debug(
-            f"cross manifest validation time {perf_counter()-t_cross_manifest}"
-        )
+        logger.debug(f"cross manifest validation time {perf_counter()-start_time}")
+
         return errors, warnings
diff --git a/schematic/models/validate_manifest.py b/schematic/models/validate_manifest.py
index b4e5f12f7..118d0d6cd 100644
--- a/schematic/models/validate_manifest.py
+++ b/schematic/models/validate_manifest.py
@@ -65,6 +65,39 @@ def get_multiple_types_error(
             error_val = f"Multiple Rules: list not first"
         return ["NA", error_col, error_message, error_val]
 
+    def check_max_rule_num(
+        self,
+        validation_rules: list[str],
+        col: pd.core.series.Series,
+        errors: list[list[str]],
+    ) -> list[list[str]]:
+        """Check that user isnt applying more rule combinations than allowed. Do not consider certain rules as a part of this rule limit.
+        Args:
+            validation_rules, list: Validation rules for current manifest column/attribute being evaluated
+            col, pd.core.series.Series: the current manifest column being evaluated
+            errors, list[list[str]]: list of errors being compiled.
+        Returns:
+            errors, list[list[str]]: list of errors being compiled, with additional error list being appended if appropriate
+        """
+        # Check that attribute rules conform to limits:
+        # IsNa and required is operate differently than most rules, do not consider it as a rule for evaluating
+        # if the number of rule pairs has been exceeded.
+        if "IsNa" in validation_rules:
+            validation_rules.remove("IsNa")
+
+        if "required" in validation_rules:
+            validation_rules.remove("required")
+
+        # no more than two rules for an attribute.
+        # As more combinations get added, may want to bring out into its own function / or use validate_rules_utils?
+        if len(validation_rules) > 2:
+            errors.append(
+                self.get_multiple_types_error(
+                    validation_rules, col, error_type="too_many_rules"
+                )
+            )
+        return errors
+
     def validate_manifest_rules(
         self,
         manifest: pd.core.frame.DataFrame,
@@ -116,6 +149,7 @@ def validate_manifest_rules(
             "regex.*",
             "matchAtLeastOne.*",
             "matchExactlyOne.*",
+            "matchNone.*",
         ]
 
         in_house_rules = [
@@ -128,6 +162,7 @@ def validate_manifest_rules(
             "list",
             "matchAtLeastOne.*",
             "matchExactlyOne.*",
+            "matchNone.*",
         ]
 
         # initialize error and warning handling lists.
@@ -180,6 +215,10 @@ def validate_manifest_rules(
 
         t_err = perf_counter()
         regex_re = re.compile("regex.*")
+
+        # Instantiate Validate Attribute
+        validate_attribute = ValidateAttribute(dmge=dmge)
+
         for col in manifest.columns:
             # remove trailing/leading whitespaces from manifest
             manifest.map(lambda x: x.strip() if isinstance(x, str) else x)
@@ -189,18 +228,13 @@ def validate_manifest_rules(
             if validation_rules and isinstance(validation_rules, dict):
                 validation_rules = extract_component_validation_rules(
                     manifest_component=manifest["Component"][0],
-                    validation_rules=validation_rules,
+                    validation_rules_dict=validation_rules,
                 )
 
-            # Check that attribute rules conform to limits:
-            # no more than two rules for an attribute.
-            # As more combinations get added, may want to bring out into its own function / or use validate_rules_utils?
-            if len(validation_rules) > 2:
-                errors.append(
-                    self.get_multiple_types_error(
-                        validation_rules, col, error_type="too_many_rules"
-                    )
-                )
+            # Check for max rule allowance
+            errors = self.check_max_rule_num(
+                validation_rules=validation_rules, col=col, errors=errors
+            )
 
             # Given a validation rule, run validation. Skip validations already performed by GE
             for rule in validation_rules:
@@ -208,6 +242,9 @@ def validate_manifest_rules(
                 if rule_in_rule_list(rule, unimplemented_expectations) or (
                     rule_in_rule_list(rule, in_house_rules) and restrict_rules
                 ):
+                    # Note rules not listed in unimplemented_expectations or inhouse rules will not be run through
+                    # the validation steps. IsNA is not a true rule, so it will not have any validation run,
+                    # it is handled in validate_attribute
                     if not rule_in_rule_list(rule, in_house_rules):
                         logger.warning(
                             f"Validation rule {rule.split(' ')[0]} has not been implemented in house and cannnot be validated without Great Expectations."
@@ -217,27 +254,23 @@ def validate_manifest_rules(
                     t_indiv_rule = perf_counter()
                     # Validate for each individual validation rule.
                     validation_method = getattr(
-                        ValidateAttribute, validation_types[validation_type]["type"]
+                        validate_attribute, validation_types[validation_type]["type"]
                     )
 
                     if validation_type == "list":
                         vr_errors, vr_warnings, manifest_col = validation_method(
-                            self,
                             rule,
                             manifest[col],
-                            dmge,
                         )
                         manifest[col] = manifest_col
                     elif validation_type.lower().startswith("match"):
                         vr_errors, vr_warnings = validation_method(
-                            self, rule, manifest[col], project_scope, dmge, access_token
+                            rule, manifest[col], project_scope, access_token
                         )
                     else:
                         vr_errors, vr_warnings = validation_method(
-                            self,
                             rule,
                             manifest[col],
-                            dmge,
                         )
                     # Check for validation rule errors and add them to other errors.
                     if vr_errors:
@@ -262,6 +295,9 @@ def validate_manifest_values(
         warnings = []
         col_attr = {}  # save the mapping between column index and attribute name
 
+        # Replace nans with empty strings so jsonschema
+        manifest = manifest.replace({np.nan: ""})
+
         # numerical values need to be type string for the jsonValidator
         for col in manifest.select_dtypes(
             include=[int, np.int64, float, np.float64]
@@ -284,7 +320,7 @@ def validate_manifest_values(
                 val_errors, val_warnings = GenerateError.generate_schema_error(
                     row_num=errorRow,
                     attribute_name=errorColName,
-                    error_msg=errorMsg,
+                    error_message=errorMsg,
                     invalid_entry=errorVal,
                     dmge=dmge,
                 )
diff --git a/schematic/schemas/__init__.py b/schematic/schemas/__init__.py
index 7943ef50e..9fce15edf 100644
--- a/schematic/schemas/__init__.py
+++ b/schematic/schemas/__init__.py
@@ -1,3 +1,5 @@
+"Schema init file"
+
 from schematic.schemas.data_model_edges import DataModelEdges
 from schematic.schemas.data_model_nodes import DataModelNodes
 from schematic.schemas.data_model_graph import DataModelGraph, DataModelGraphExplorer
diff --git a/schematic/schemas/commands.py b/schematic/schemas/commands.py
index 9836c792f..17f622a6c 100644
--- a/schematic/schemas/commands.py
+++ b/schematic/schemas/commands.py
@@ -1,17 +1,19 @@
-#!/usr/bin/env python3
+"""Schema Commands"""
 
-import click
-import click_log
 import logging
-import sys
 import time
 import re
 from typing import get_args
 
+import click
+import click_log  # type: ignore
+
+# pylint: disable=logging-fstring-interpolation
+
 from schematic.schemas.data_model_parser import DataModelParser
-from schematic.schemas.data_model_graph import DataModelGraph, DataModelGraphExplorer
+from schematic.schemas.data_model_graph import DataModelGraph
 from schematic.schemas.data_model_validator import DataModelValidator
-from schematic.schemas.data_model_jsonld import DataModelJsonLD, convert_graph_to_jsonld
+from schematic.schemas.data_model_jsonld import convert_graph_to_jsonld
 
 from schematic.utils.schema_utils import DisplayLabelType
 from schematic.utils.cli_utils import query_dict
@@ -21,16 +23,17 @@
 logger = logging.getLogger("schematic")
 click_log.basic_config(logger)
 
-CONTEXT_SETTINGS = dict(help_option_names=["--help", "-h"])  # help options
+CONTEXT_SETTINGS = {"help_option_names": ["--help", "-h"]}  # help options
 
 
-# invoke_without_command=True -> forces the application not to show aids before losing them with a --h
+# invoke_without_command=True -> forces the application not to show aids before
+# losing them with a --h
 @click.group(context_settings=CONTEXT_SETTINGS, invoke_without_command=True)
 def schema():  # use as `schematic model ...`
     """
     Sub-commands for Schema related utilities/methods.
     """
-    pass
+    pass  # pylint: disable=unnecessary-pass
 
 
 # prototype based on submit_metadata_manifest()
@@ -61,11 +64,15 @@ def convert(schema, data_model_labels, output_jsonld):
     Running CLI to convert data model specification in CSV format to
     data model in JSON-LD format.
 
-    Note: Currently, not configured to build off of base model, so removing --base_schema argument for now
+    Note: Currently, not configured to build off of base model, so removing --base_schema
+      argument for now
     """
+    # pylint: disable=too-many-locals
+    # pylint: disable=redefined-outer-name
+    # pylint: disable=too-many-branches
 
     # get the start time
-    st = time.time()
+    start_time = time.time()
 
     # Instantiate Parser
     data_model_parser = DataModelParser(schema)
@@ -78,9 +85,9 @@ def convert(schema, data_model_labels, output_jsonld):
     # Instantiate DataModelGraph
     data_model_grapher = DataModelGraph(parsed_data_model, data_model_labels)
 
-    # Generate graph
+    # Generate graphschema
     logger.info("Generating data model graph.")
-    graph_data_model = data_model_grapher.generate_data_model_graph()
+    graph_data_model = data_model_grapher.graph
 
     # Validate generated data model.
     logger.info("Validating the data model internally.")
@@ -93,8 +100,8 @@ def convert(schema, data_model_labels, output_jsonld):
             if isinstance(err, str):
                 logger.error(err)
             elif isinstance(err, list):
-                for e in err:
-                    logger.error(e)
+                for error in err:
+                    logger.error(error)
 
     # If there are warnings log them.
     if data_model_warnings:
@@ -102,11 +109,11 @@ def convert(schema, data_model_labels, output_jsonld):
             if isinstance(war, str):
                 logger.warning(war)
             elif isinstance(war, list):
-                for w in war:
-                    logger.warning(w)
+                for warning in war:
+                    logger.warning(warning)
 
     logger.info("Converting data model to JSON-LD")
-    jsonld_data_model = convert_graph_to_jsonld(Graph=graph_data_model)
+    jsonld_data_model = convert_graph_to_jsonld(graph=graph_data_model)
 
     # output JSON-LD file alongside CSV file by default, get path.
     if output_jsonld is None:
@@ -128,14 +135,17 @@ def convert(schema, data_model_labels, output_jsonld):
         click.echo(
             f"The Data Model was created and saved to '{output_jsonld}' location."
         )
-    except:
+    except:  # pylint: disable=bare-except
         click.echo(
-            f"The Data Model could not be created by using '{output_jsonld}' location. Please check your file path again"
+            (
+                f"The Data Model could not be created by using '{output_jsonld}' location. "
+                "Please check your file path again"
+            )
         )
 
     # get the end time
-    et = time.time()
+    end_time = time.time()
 
     # get the execution time
-    elapsed_time = time.strftime("%M:%S", time.gmtime(et - st))
+    elapsed_time = time.strftime("%M:%S", time.gmtime(end_time - start_time))
     click.echo(f"Execution time: {elapsed_time} (M:S)")
diff --git a/schematic/schemas/curie.py b/schematic/schemas/curie.py
deleted file mode 100644
index b30a758ad..000000000
--- a/schematic/schemas/curie.py
+++ /dev/null
@@ -1,163 +0,0 @@
-"""CURIE - A Module for handling Compact URIs (CURIEs)
-
-Features:
-- Support for RDFLib datatypes (BNode, URIRef), but doesn't require it
-- MIT licensed
-
-CURIE Syntax specification:
-    <http://www.w3.org/TR/2007/WD-curie-20070307/>
-Blank node handling notes:
-    <http://milicicvuk.com/blog/2011/08/18/extended-curie-prefixlocalnamekey/>
-
-Copyright (C) 2011-2012 Jaakko Salonen
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in
-all copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-THE SOFTWARE.
-
-"""
-
-__author__ = "Jaakko Salonen"
-__copyright__ = "Copyright 2011-2012, Jaakko Salonen"
-__version__ = "0.5.0"
-__license__ = "MIT"
-__status__ = "Prototype"
-
-from urllib.parse import unquote
-from copy import copy
-from rdflib import BNode, URIRef
-
-import sys
-
-if sys.version_info.major == 3:
-    unicode = str
-
-
-try:
-    from rdflib import BNode, URIRef
-except:
-    # Fallback if rdflib is not present
-    class BNode(object):
-        def __init__(self, val):
-            self.val = val
-
-        def n3(self):
-            return unicode("_:" + self.val)
-
-    class URIRef(unicode):
-        pass
-
-
-class Curie(object):
-    """Curie Datatype Class
-
-    Examples:
-
-    >>> nss = dict(dc='http://purl.org/dc/elements/1.1/')
-
-    >>> dc_title = Curie('http://purl.org/dc/elements/1.1/title', nss)
-
-    >>> dc_title.curie
-    u'dc:title'
-
-    >>> dc_title.uri
-    u'http://purl.org/dc/elements/1.1/title'
-
-    >>> dc_title.curie
-    u'dc:title'
-
-    >>> nss['example'] = 'http://www.example.org/'
-
-    >>> iri_test = Curie('http://www.example.org/D%C3%BCrst', nss)
-
-    >>> iri_test.uri
-    u'http://www.example.org/D\\xfcrst'
-
-    >>> iri_test.curie
-    u'example:D%C3%BCrst'
-
-    """
-
-    def __init__(self, uri, namespaces=dict()):
-        self.namespaces = namespaces
-        self.uri = unicode(unquote(uri), "utf-8")
-        self.curie = copy(self.uri)
-        for ns in self.namespaces:
-            self.curie = uri.replace("" + self.namespaces["%s" % ns], "%s:" % ns)
-
-    def __str__(self):
-        return self.__unicode__()
-
-    def __unicode__(self):
-        return self.curie
-
-
-def uri2curie(uri, namespaces):
-    """Convert URI to CURIE
-
-    Define namespaces we want to use:
-    >>> nss = dict(dc='http://purl.org/dc/elements/1.1/')
-
-    Converting a string URI to CURIE
-    >>> uri2curie('http://purl.org/dc/elements/1.1/title', nss)
-    u'dc:title'
-
-    RDFLib data type conversions:
-
-    URIRef to CURIE
-    >>> uri2curie(URIRef('http://purl.org/dc/elements/1.1/title'), nss)
-    u'dc:title'
-
-    Blank node to CURIE
-    >>> uri2curie(BNode('blanknode1'), nss)
-    u'_:blanknode1'
-
-    """
-
-    # Use n3() method if BNode
-    if isinstance(uri, BNode):
-        result = uri.n3()
-    else:
-        result = uri
-
-    #  result = unicode(uri)
-    for ns in namespaces:
-        ns_raw = "%s" % namespaces["%s" % ns]
-        if ns_raw == "http://www.w3.org/2002/07/owl#uri":
-            ns_raw = "http://www.w3.org/2002/07/owl#"
-        result = result.replace(ns_raw, "%s:" % ns)
-
-    result = result.replace("http://www.w3.org/2002/07/owl#", "owl:")
-    return result
-
-
-def curie2uri(curie, namespaces):
-    """Convert CURIE to URI
-
-    TODO: testing
-
-    """
-    result = unicode(curie)
-    for ns in namespaces:
-        result = result.replace("%s:" % ns, "" + namespaces["%s" % ns])
-    return URIRef(result)
-
-
-if __name__ == "__main__":
-    import doctest
-
-    doctest.testmod()
diff --git a/schematic/schemas/data_model_edges.py b/schematic/schemas/data_model_edges.py
index 225039866..02df4e16e 100644
--- a/schematic/schemas/data_model_edges.py
+++ b/schematic/schemas/data_model_edges.py
@@ -1,39 +1,52 @@
-import networkx as nx
-
+"""Data Model Edges"""
+from typing import Union, Any
 from schematic.schemas.data_model_relationships import DataModelRelationships
 
 
-class DataModelEdges:
-    def __init__(self):
+class DataModelEdges:  # pylint: disable=too-few-public-methods
+    """Data Model Edges"""
+
+    def __init__(self) -> None:
         self.dmr = DataModelRelationships()
         self.data_model_relationships = self.dmr.relationships_dictionary
 
-    def generate_edge(
+    def generate_edge(  # pylint: disable=too-many-arguments
         self,
         node: str,
-        all_node_dict: dict,
-        attr_rel_dict: dict,
-        edge_relationships: dict,
-        edge_list: list,
-    ) -> list[tuple[str, str, dict[str:str, str:int]]]:
-        """Generate an edge between a target node and relevant other nodes the data model. In short, does this current node belong to a recorded relationship in the attribute, relationshps dictionary. Go through each attribute and relationship to find where the node may be.
+        all_node_dict: dict[str, dict[str, Any]],
+        attr_rel_dict: dict[str, dict[str, Any]],
+        edge_relationships: dict[str, str],
+        edge_list: list[tuple[str, str, dict[str, Union[str, int]]]],
+    ) -> list[tuple[str, str, dict[str, Union[str, int]]]]:
+        """
+
+        Generate an edge between a target node and relevant other nodes the data model.
+          In short, does this current node belong to a recorded relationship in the attribute,
+          relationshps dictionary.
+          Go through each attribute and relationship to find where the node may be.
+
         Args:
-            G, nx.MultiDiGraph: networkx graph representation of the data model, that is in the process of being fully built. At this point, all the nodes would have been added, and edges are being added per target node.
-            node, str: target node to look for connecting edges
-            all_node_dict, dict: a dictionary containing information about all nodes in the model
+            node (str): target node to look for connecting edges
+            all_node_dict (dict): a dictionary containing information about all nodes in the model
                 key: node display name
                 value: node attribute dict, containing attributes to attach to each node.
-            attr_rel_dict, dict:
-                {Attribute Display Name: {
-                        Relationships: {
-                                    CSV Header: Value}}}
-            edge_relationships: dict, rel_key: csv_header if the key represents a value relationship.
-            edge_list: list(tuple), list of tuples describing the edges and the edge attributes, organized as (node_1, node_2, {key:edge_relationship_key, weight:int})
-                At this point, the edge list will be in the process of being built. Adding edges from list so they will be added properly to the graph without being overwritten in the loop, and passing the Graph around more.
-        Returns:
-            edge_list: list(tuple), list of tuples describing the edges and the edge attributes, organized as (node_1, node_2, {key:edge_relationship_key, weight:int})
-                At this point, the edge list will have additional edges added related to the current node.
+            attr_rel_dict (dict):
+                {Attribute Display Name: {--disallow-untyped-defs
+                    Relationships: {
+                        CSV Header: Value}
+                    }
+                }
+            edge_relationships (dict): dict, rel_key: csv_header if the key represents a value
+              relationship.
+            edge_list (list): list of tuples describing the edges and the edge attributes,
+              organized as (node_1, node_2, {key:edge_relationship_key, weight:int})
+              At this point, the edge list will be in the process of being built.
+              Adding edges from list so they will be added properly to the graph without being
+              overwritten in the loop, and passing the Graph around more.
+
+
         """
+
         # For each attribute in the model.
         for attribute_display_name, relationship in attr_rel_dict.items():
             # Get the relationships associated with the current attribute
@@ -49,15 +62,18 @@ def generate_edge(
                         and node != attribute_display_name
                     ):
                         # Generate weights based on relationship type.
-                        # Weights will allow us to preserve the order of entries order in the data model in later steps.
+                        # Weights will allow us to preserve the order of entries order in the
+                        # data model in later steps.
                         if rel_key == "domainIncludes":
-                            # For 'domainIncludes'/properties relationship, users do not explicitly provide a list order (like for valid values, or dependsOn)
+                            # For 'domainIncludes'/properties relationship, users do not explicitly
+                            # provide a list order (like for valid values, or dependsOn)
                             # so we pull the order/weight from the order of the attributes.
                             weight = list(attr_rel_dict.keys()).index(
                                 attribute_display_name
                             )
-                        elif type(relationships[csv_header]) == list:
-                            # For other relationships that pull in lists of values, we can explicilty pull the weight by their order in the provided list
+                        elif isinstance(relationships[csv_header], list):
+                            # For other relationships that pull in lists of values, we can
+                            # explicilty pull the weight by their order in the provided list
                             weight = relationships[csv_header].index(node)
                         else:
                             # For single (non list) entries, add weight of 0
@@ -65,7 +81,7 @@ def generate_edge(
                         # Get the edge_key for the edge relationship we are adding at this step
                         edge_key = self.data_model_relationships[rel_key]["edge_key"]
                         # Add edges, in a manner that preserves directionality
-                        # TODO: rewrite to use edge_dir
+                        # TODO: rewrite to use edge_dir pylint: disable=fixme
                         if rel_key in ["subClassOf", "domainIncludes"]:
                             edge_list.append(
                                 (
@@ -85,7 +101,8 @@ def generate_edge(
                                     {"key": edge_key, "weight": weight},
                                 )
                             )
-                        # Add add rangeIncludes/valid value relationships in reverse as well, making the attribute the parent of the valid value.
+                        # Add add rangeIncludes/valid value relationships in reverse as well,
+                        # making the attribute the parent of the valid value.
                         if rel_key == "rangeIncludes":
                             edge_list.append(
                                 (
diff --git a/schematic/schemas/data_model_graph.py b/schematic/schemas/data_model_graph.py
index 72b1ebf56..515421491 100644
--- a/schematic/schemas/data_model_graph.py
+++ b/schematic/schemas/data_model_graph.py
@@ -1,9 +1,10 @@
-from copy import deepcopy
-import graphviz
+"""DataModel Graph"""
+
 import logging
-from typing import Any, Dict, Optional, Text
-import networkx as nx
-from rdflib import Namespace
+from typing import Optional, Union, Any
+
+import networkx as nx  # type: ignore
+import graphviz  # type: ignore
 
 from schematic.schemas.data_model_edges import DataModelEdges
 from schematic.schemas.data_model_nodes import DataModelNodes
@@ -13,28 +14,33 @@
     get_property_label_from_display_name,
     get_class_label_from_display_name,
     DisplayLabelType,
+    extract_component_validation_rules,
 )
 from schematic.utils.general import unlist
 from schematic.utils.viz_utils import visualize
+from schematic.utils.validate_utils import rule_in_rule_list
+
 
 logger = logging.getLogger(__name__)
 
 
-class DataModelGraphMeta(object):
-    _instances = {}
+class DataModelGraphMeta:  # pylint: disable=too-few-public-methods
+    """DataModelGraphMeta"""
 
-    def __call__(cls, *args, **kwargs):
+    _instances: dict = {}
+
+    def __call__(cls, *args: Any, **kwargs: Any):  # pylint: disable=no-self-argument
         """
         Possible changes to the value of the `__init__` argument do not affect
         the returned instance.
         """
         if cls not in cls._instances:
-            instance = super().__call__(*args, **kwargs)
+            instance = super().__call__(*args, **kwargs)  # pylint: disable=no-member
             cls._instances[cls] = instance
         return cls._instances[cls]
 
 
-class DataModelGraph:
+class DataModelGraph:  # pylint: disable=too-few-public-methods
     """
     Generate graph network (networkx) from the attributes and relationships returned
     from the data model parser.
@@ -56,8 +62,10 @@ def __init__(
                         Relationships: {
                                     CSV Header: Value}}}
             data_model_labels: str, display_label or class_label.
-                display_label, use the display name as a label, if it is valid (contains no blacklisted characters) otherwise will default to schema_label.
-                class_label, default, use standard class or property label.         Raises:
+                display_label, use the display name as a label, if it is valid
+                (contains no blacklisted characters) otherwise will default to schema_label.
+                class_label, default, use standard class or property label.
+        Raises:
             ValueError, attribute_relationship_dict not loaded.
         """
         self.attribute_relationships_dict = attribute_relationships_dict
@@ -68,12 +76,18 @@ def __init__(
 
         if not self.attribute_relationships_dict:
             raise ValueError(
-                "Something has gone wrong, a data model was not loaded into the DataModelGraph Class. Please check that your paths are correct"
+                (
+                    "Something has gone wrong, a data model was not loaded into the DataModelGraph "
+                    "Class. Please check that your paths are correct"
+                )
             )
         self.graph = self.generate_data_model_graph()
 
     def generate_data_model_graph(self) -> nx.MultiDiGraph:
-        """Generate NetworkX Graph from the Relationships/attributes dictionary, the graph is built by first adding all nodes to the graph, then connecting nodes by the relationships defined in the attributes_relationship dictionary.
+        """
+        Generate NetworkX Graph from the Relationships/attributes dictionary, the graph is built
+          by first adding all nodes to the graph, then connecting nodes by the relationships defined
+          in the attributes_relationship dictionary.
         Returns:
             G: nx.MultiDiGraph, networkx graph representation of the data model
         """
@@ -86,7 +100,7 @@ def generate_data_model_graph(self) -> nx.MultiDiGraph:
         )
 
         # Instantiate NetworkX MultiDigraph
-        G = nx.MultiDiGraph()
+        graph = nx.MultiDiGraph()
 
         all_node_dict = {}
 
@@ -103,9 +117,9 @@ def generate_data_model_graph(self) -> nx.MultiDiGraph:
             all_node_dict[node] = node_dict
 
             # Generate node and attach information (attributes) to each node
-            G = self.dmn.generate_node(G, node_dict)
+            graph = self.dmn.generate_node(graph, node_dict)
 
-        edge_list = []
+        edge_list: list[tuple[str, str, dict[str, Union[str, int]]]] = []
         ## Connect nodes via edges
         for node in all_nodes:
             # Generate edges
@@ -120,39 +134,50 @@ def generate_data_model_graph(self) -> nx.MultiDiGraph:
 
         # Add edges to the Graph
         for node_1, node_2, edge_dict in edge_list:
-            G.add_edge(node_1, node_2, key=edge_dict["key"], weight=edge_dict["weight"])
-        return G
+            graph.add_edge(
+                node_1, node_2, key=edge_dict["key"], weight=edge_dict["weight"]
+            )
+        return graph
+
 
+class DataModelGraphExplorer:  # pylint: disable=too-many-public-methods
+    """DataModelGraphExplorer"""
 
-class DataModelGraphExplorer:
     def __init__(
         self,
-        G,
+        graph: nx.MultiDiGraph,
     ):
         """Load data model graph as a singleton.
         Args:
             G: nx.MultiDiGraph, networkx graph representation of the data model
         """
-        self.graph = G  # At this point the graph is expected to be fully formed.
+        self.graph = graph  # At this point the graph is expected to be fully formed.
         self.dmr = DataModelRelationships()
         self.rel_dict = self.dmr.relationships_dictionary
 
     def find_properties(self) -> set[str]:
-        """Identify all properties, as defined by the first node in a pair, connected with 'domainIncludes' edge type
+        """
+        Identify all properties, as defined by the first node in a pair, connected with
+        'domainIncludes' edge type
+
         Returns:
-            properties, set: All properties defined in the data model, each property name is defined by its label.
+            properties, set: All properties defined in the data model, each property name
+              is defined by its label.
         """
-        properties = []
-        for node_1, node_2, rel in self.graph.edges:
+        properties_list: list[str] = []
+        for node_1, _, rel in self.graph.edges:
             if rel == self.rel_dict["domainIncludes"]["edge_key"]:
-                properties.append(node_1)
-        properties = set(properties)
-        return properties
+                properties_list.append(node_1)
+        properties_set = set(properties_list)
+        return properties_set
 
     def find_classes(self) -> set[str]:
-        """Identify all classes, as defined but all nodes, minus all properties (which are explicitly defined)
+        """
+        Identify all classes, as defined but all nodes, minus all properties
+        (which are explicitly defined)
         Returns:
-            classes, set:  All classes defined in the data model, each class name is defined by its label.
+            classes, set:  All classes defined in the data model, each class
+              name is defined by its label.
         """
         nodes = self.graph.nodes
         properties = self.find_properties()
@@ -165,11 +190,13 @@ def find_node_range(
         """Get valid values for the given node (attribute)
         Args:
             node_label, str, Optional[str]: label of the node for which to retrieve valid values
-            node_display_name, str, Optional[str]: Display Name of the node for which to retrieve valid values
+            node_display_name, str, Optional[str]: Display Name of the node for which to
+              retrieve valid values
         Returns:
             valid_values, list: List of valid values associated with the provided node.
         """
         if not node_label:
+            assert node_display_name is not None
             node_label = self.get_node_label(node_display_name)
 
         valid_values = []
@@ -196,25 +223,116 @@ def get_adjacent_nodes_by_relationship(
         #checked
         """
         nodes = set()
-        for node_1, node_2, key, _ in self.graph.out_edges(
-            node_label, data=True, keys=True
-        ):
+        for _, node_2, key, _ in self.graph.out_edges(node_label, data=True, keys=True):
             if key == relationship:
                 nodes.add(node_2)
 
         return list(nodes)
 
+    def get_component_node_required(
+        self,
+        manifest_component: str,
+        node_validation_rules: Optional[list[str]] = None,
+        node_label: Optional[str] = None,
+        node_display_name: Optional[str] = None,
+    ) -> bool:
+        """Check if a node is required taking into account the manifest component it is defined in
+        (requirements can be set in validaiton rule as well as required column)
+        Args:
+            manifest_component: str, manifest component display name that the node belongs to.
+            node_validation_rules: list[str], valdation rules for a given node and component.
+            node_label: str, Label of the node you would want to get the comment for.
+            node_display_name: str, node display name for the node being queried.
+        Returns:
+            True, if node is required, False if not
+        """
+        node_required = False
+
+        if not node_validation_rules:
+            # Get node validation rules for a given component
+            node_validation_rules = self.get_component_node_validation_rules(
+                manifest_component=manifest_component,
+                node_label=node_label,
+                node_display_name=node_display_name,
+            )
+
+        # Check if the valdation rule specifies that the node is required for this particular
+        # component.
+        if rule_in_rule_list("required", node_validation_rules):
+            node_required = True
+            # To prevent any unintended errors, ensure the Required field for this node is False
+            if self.get_node_required(
+                node_label=node_label, node_display_name=node_display_name
+            ):
+                if not node_display_name:
+                    assert node_label is not None
+                    node_display_name = self.graph.nodes[node_label][
+                        self.rel_dict["displayName"]["node_label"]
+                    ]
+                error_str = " ".join(
+                    [
+                        f"For component: {manifest_component} and attribute: {node_display_name}",
+                        "requirements are being specified in both the Required field and in the",
+                        "Validation Rules. If you desire to use validation rules to set component",
+                        "specific requirements for this attribute",
+                        "then the Required field needs to be set to False, or the validation may",
+                        "not work as intended, for other components where the attribute",
+                        "that should not be required.",
+                    ]
+                )
+
+                logger.error(error_str)
+        else:
+            # If requirements are not being set in the validaiton rule, then just pull the
+            # standard node requirements from the model
+            node_required = self.get_node_required(
+                node_label=node_label, node_display_name=node_display_name
+            )
+        return node_required
+
+    def get_component_node_validation_rules(
+        self,
+        manifest_component: str,
+        node_label: Optional[str] = None,
+        node_display_name: Optional[str] = None,
+    ) -> list[str]:
+        """Get valdation rules for a given node and component.
+        Args:
+            manifest_component: str, manifest component display name that the node belongs to.
+            node_label: str, Label of the node you would want to get the comment for.
+            node_display_name: str, node display name for the node being queried.
+        Returns:
+            validation_rules: list[str], validation rules list for a given node and component.
+        """
+        # get any additional validation rules associated with this node (e.g. can this node
+        # be mapped to a list of other nodes)
+        node_validation_rules = self.get_node_validation_rules(
+            node_label=node_label, node_display_name=node_display_name
+        )
+
+        # Parse the validation rules per component if applicable
+        if node_validation_rules and isinstance(node_validation_rules, dict):
+            node_validation_rules = extract_component_validation_rules(
+                manifest_component=manifest_component,
+                validation_rules_dict=node_validation_rules,
+            )
+        return node_validation_rules
+
     def get_component_requirements(
         self,
         source_component: str,
     ) -> list[str]:
-        """Get all components that are associated with a given source component and are required by it.
+        """
+        Get all components that are associated with a given source component and are
+          required by it.
 
         Args:
-            source_component: source component for which we need to find all required downstream components.
+            source_component: source component for which we need to find all required downstream
+              components.
 
         Returns:
-            List of nodes that are descendants from the source component are are related to the source through a specific component relationship.
+            List of nodes that are descendants from the source component are are related to the
+              source through a specific component relationship.
         """
 
         req_components = list(
@@ -233,13 +351,17 @@ def get_component_requirements_graph(
         self,
         source_component: str,
     ) -> nx.DiGraph:
-        """Get all components that are associated with a given source component and are required by it; return the components as a dependency graph (i.e. a DAG).
+        """
+        Get all components that are associated with a given source component and are required by it;
+          return the components as a dependency graph (i.e. a DAG).
 
         Args:
-            source_component, str: source component for which we need to find all required downstream components.
+            source_component, str: source component for which we need to find all required
+              downstream components.
 
         Returns:
-            A subgraph of the schema graph induced on nodes that are descendants from the source component and are related to the source through a specific component relationship.
+            A subgraph of the schema graph induced on nodes that are descendants from the source
+              component and are related to the source through a specific component relationship.
         """
 
         # get a list of required component nodes
@@ -259,16 +381,22 @@ def get_descendants_by_edge_type(
         connected: bool = True,
         ordered: bool = False,
     ) -> list[str]:
-        """Get all nodes that are descendants of a given source node, based on a specific type of edge / relationship type.
+        """
+        Get all nodes that are descendants of a given source node, based on a specific
+          type of edge / relationship type.
 
         Args:
             source_node: The node whose descendants need to be retreived.
             relationship: Edge / link relationship type with possible values same as in above docs.
-            connected: If True, we need to ensure that all descendant nodes are reachable from the source node, i.e., they are part of the same connected component.
-                       If False, the descendants could be in multiple connected components.
-                       Default value is True.
-            ordered: If True, the list of descendants will be topologically ordered.
-                     If False, the list has no particular order (depends on the order in which the descendats were traversed in the subgraph).
+            connected:
+              If True, we need to ensure that all descendant nodes are reachable from the source
+                node, i.e., they are part of the same connected component.
+              If False, the descendants could be in multiple connected components.
+              Default value is True.
+            ordered:
+              If True, the list of descendants will be topologically ordered.
+              If False, the list has no particular order (depends on the order in which the
+                descendats were traversed in the subgraph).
 
         Returns:
             List of nodes that are descendants from a particular node (sorted / unsorted)
@@ -280,7 +408,8 @@ def get_descendants_by_edge_type(
         subgraph_nodes.append(source_node)
         descendants_subgraph = self.graph.subgraph(subgraph_nodes)
 
-        # prune the descendants subgraph so as to include only those edges that match the relationship type
+        # prune the descendants subgraph so as to include only those edges that match
+        # the relationship type
         rel_edges = []
         for node_1, node_2, key, _ in descendants_subgraph.edges(data=True, keys=True):
             if key == relationship:
@@ -292,7 +421,8 @@ def get_descendants_by_edge_type(
         descendants = relationship_subgraph.nodes()
 
         if not descendants:
-            # return empty list if there are no nodes that are reachable from the source node based on this relationship type
+            # return empty list if there are no nodes that are reachable from the
+            # source node based on this relationship type
             return []
 
         if connected and ordered:
@@ -300,7 +430,8 @@ def get_descendants_by_edge_type(
             descendants = nx.descendants(relationship_subgraph, source_node)
             descendants.add(source_node)
 
-            # normally, the descendants from a node are unordered (peculiarity of nx descendants call)
+            # normally, the descendants from a node are unordered (peculiarity
+            # of nx descendants call)
             # form the subgraph on descendants and order it topologically
             # this assumes an acyclic subgraph
             descendants = nx.topological_sort(
@@ -308,7 +439,9 @@ def get_descendants_by_edge_type(
             )
         elif connected:
             # get the nodes that are reachable from a given source node
-            # after the pruning process above some nodes in the root_descendants subgraph might have become disconnected and will be omitted
+            # after the pruning process above some nodes in the
+            # root_descendants subgraph might have become disconnected and
+            # will be omitted
             descendants = nx.descendants(relationship_subgraph, source_node)
             descendants.add(source_node)
         elif ordered:
@@ -322,7 +455,8 @@ def get_digraph_by_edge_type(self, edge_type: str) -> nx.DiGraph:
         """Get a networkx digraph of the nodes connected via a given edge_type.
         Args:
             edge_type:
-                Edge type to search for, possible types are defined by 'edge_key' in relationship class
+                Edge type to search for, possible types are defined by 'edge_key'
+                  in relationship class
         Returns:
         """
         digraph = nx.DiGraph()
@@ -335,10 +469,11 @@ def get_edges_by_relationship(
         self,
         node: str,
         relationship: str,
-    ) -> list[str]:
+    ) -> list[tuple[str, str]]:
         """Get a list of out-edges of a node where the edges match a specifc type of relationship.
 
-        i.e., the edges connecting a node to its neighbors are of relationship type -- "parentOf" (set of edges to children / sub-class nodes).
+        i.e., the edges connecting a node to its neighbors are of relationship type -- "parentOf"
+          (set of edges to children / sub-class nodes).
 
         Args:
             node: the node whose edges we need to look at.
@@ -347,7 +482,7 @@ def get_edges_by_relationship(
         Returns:
             List of edges that are connected to the node.
         """
-        edges = []
+        edges: list[tuple[str, str]] = []
 
         for node_1, node_2, key, _ in self.graph.out_edges(node, data=True, keys=True):
             if key == relationship:
@@ -356,16 +491,24 @@ def get_edges_by_relationship(
         return edges
 
     def get_ordered_entry(self, key: str, source_node_label: str) -> list[str]:
-        """Order the values associated with a particular node and edge_key to match original ordering in schema.
+        """
+        Order the values associated with a particular node and edge_key to
+          match original ordering in schema.
+
         Args:
-            key: a key representing and edge relationship in DataModelRelationships.relationships_dictionary
-            source_node_label, str: node to look for edges of and order
-        Returns:
-            sorted_nodes, list: list of sorted nodes, that share the specified relationship with the source node
-            Example:
-                For the example data model, for key='rangeIncludes', source_node_label='CancerType' the return would be ['Breast, 'Colorectal', 'Lung', 'Prostate', 'Skin'] in that exact order.
+            key (str): a key representing and edge relationship in
+              DataModelRelationships.relationships_dictionary
+            source_node_label (str): node to look for edges of and order
+
         Raises:
-            KeyError, cannot find source node in graph
+            KeyError: cannot find source node in graph
+
+        Returns:
+            list[str]:
+              list of sorted nodes, that share the specified relationship with the source node
+              For the example data model, for key='rangeIncludes', source_node_label='CancerType'
+                the return would be ['Breast, 'Colorectal', 'Lung', 'Prostate', 'Skin'] in that
+                exact order.
         """
         # Check if node is in the graph, if not throw an error.
         if not self.is_class_in_schema(node_label=source_node_label):
@@ -410,18 +553,20 @@ def get_ordered_entry(self, key: str, source_node_label: str) -> list[str]:
     # Get values associated with a node
     def get_nodes_ancestors(self, subgraph: nx.DiGraph, node_label: str) -> list[str]:
         """Get a list of nodes reachable from source component in graph
+
         Args:
-            subgraph: networkx graph object
-            node_label, str: label of node to find ancestors for
+            subgraph (nx.DiGraph): networkx graph object
+            node_label (str): label of node to find ancestors for
+
         Returns:
-            all_ancestors, list: nodes reachable from source in graph
+            list[str]: nodes reachable from source in graph
         """
         all_ancestors = list(nx.ancestors(subgraph, node_label))
 
         return all_ancestors
 
     def get_node_comment(
-        self, node_display_name: str = None, node_label: str = None
+        self, node_display_name: Optional[str] = None, node_label: Optional[str] = None
     ) -> str:
         """Get the node definition, i.e., the "comment" associated with a given node display name.
 
@@ -432,6 +577,7 @@ def get_node_comment(
             Comment associated with node, as a string.
         """
         if not node_label:
+            assert node_display_name is not None
             node_label = self.get_node_label(node_display_name)
 
         if not node_label:
@@ -454,8 +600,10 @@ def get_node_dependencies(
             source_node: The node whose dependencies we need to compute.
             display_names: if True, return list of display names of each of the dependencies.
                            if False, return list of node labels of each of the dependencies.
-            schema_ordered: if True, return the dependencies of the node following the order of the schema (slower).
-                            if False, return dependencies from graph without guaranteeing schema order (faster)
+            schema_ordered:
+              if True, return the dependencies of the node following the order of the schema
+                (slower).
+              if False, return dependencies from graph without guaranteeing schema order (faster)
 
         Returns:
             List of nodes that are dependent on the source node.
@@ -548,29 +696,37 @@ def get_node_range(
         node_display_name: Optional[str] = None,
         display_names: bool = False,
     ) -> list[str]:
-        """Get the range, i.e., all the valid values that are associated with a node label.
+        """
+        Get the range, i.e., all the valid values that are associated with a node label.
+
 
         Args:
-            node_label: Node for which you need to retrieve the range.
-            display_names, bool: True
-        Returns:
-            required_range: Returned if display_names=False, list of valid values (labels) associated with a given node.
-            dependencies_display_name: Returned if display_names=True,
-                List of valid values (display names) associated with a given node
+            node_label (Optional[str], optional): Node for which you need to retrieve the range.
+              Defaults to None.
+            node_display_name (Optional[str], optional): _description_. Defaults to None.
+            display_names (bool, optional): _description_. Defaults to False.
+
         Raises:
             ValueError: If the node cannot be found in the graph.
+
+        Returns:
+            list[str]:
+              If display_names=False, a list of valid values (labels) associated with a given node.
+              If display_names=True, a list of valid values (display names) associated
+                with a given node
         """
         if not node_label:
+            assert node_display_name is not None
             node_label = self.get_node_label(node_display_name)
 
         try:
             # get node range in the order defined in schema for given node
             required_range = self.find_node_range(node_label=node_label)
-        except KeyError:
+        except KeyError as exc:
             raise ValueError(
                 f"The source node {node_label} does not exist in the graph. "
                 "Please use a different node."
-            )
+            ) from exc
 
         if display_names:
             # get the display name(s) of all dependencies
@@ -598,6 +754,7 @@ def get_node_required(
             False: If the given node is not a "required" (i.e., an "optional") node.
         """
         if not node_label:
+            assert node_display_name is not None
             node_label = self.get_node_label(node_display_name)
 
         rel_node_label = self.rel_dict["required"]["node_label"]
@@ -606,7 +763,7 @@ def get_node_required(
 
     def get_node_validation_rules(
         self, node_label: Optional[str] = None, node_display_name: Optional[str] = None
-    ) -> str:
+    ) -> Union[list, dict[str, str]]:
         """Get validation rules associated with a node,
 
         Args:
@@ -616,6 +773,7 @@ def get_node_validation_rules(
             A set of validation rules associated with node, as a list.
         """
         if not node_label:
+            assert node_display_name is not None
             node_label = self.get_node_label(node_display_name)
 
         if not node_label:
@@ -635,7 +793,8 @@ def get_subgraph_by_edge_type(self, relationship: str) -> nx.DiGraph:
             Directed graph on edges of a particular type (aka relationship)
         """
 
-        # prune the metadata model graph so as to include only those edges that match the relationship type
+        # prune the metadata model graph so as to include only those edges that
+        # match the relationship type
         rel_edges = []
         for node_1, node_2, key, _ in self.graph.out_edges(data=True, keys=True):
             if key == relationship:
@@ -657,6 +816,7 @@ def find_adjacent_child_classes(
             List of nodes that are adjacent to the given node, by SubclassOf relationship.
         """
         if not node_label:
+            assert node_display_name is not None
             node_label = self.get_node_label(node_display_name)
 
         return self.get_adjacent_nodes_by_relationship(
@@ -684,13 +844,19 @@ def find_class_specific_properties(self, schema_class: str) -> list[str]:
 
         if not self.is_class_in_schema(schema_class):
             raise KeyError(
-                f"Schema_class provided: {schema_class} is not in the data model, please check that you are providing the proper class/node label"
+                (
+                    f"Schema_class provided: {schema_class} is not in the data model, please check "
+                    "that you are providing the proper class/node label"
+                )
             )
 
         properties = []
-        for n1, n2 in self.graph.edges():
-            if n2 == schema_class and "domainValue" in self.graph[n1][schema_class]:
-                properties.append(n1)
+        for node1, node2 in self.graph.edges():
+            if (
+                node2 == schema_class
+                and "domainValue" in self.graph[node1][schema_class]
+            ):
+                properties.append(node1)
         return properties
 
     def find_parent_classes(self, node_label: str) -> list[list[str]]:
@@ -714,7 +880,8 @@ def find_parent_classes(self, node_label: str) -> list[list[str]]:
     def full_schema_graph(self, size: Optional[int] = None) -> graphviz.Digraph:
         """Create a graph of the data model.
         Args:
-            size, float: max height and width of the graph, if one value provided it is used for both.
+            size, float: max height and width of the graph, if one value provided
+               it is used for both.
         Returns:
             schema graph viz
         """
@@ -729,26 +896,24 @@ def is_class_in_schema(self, node_label: str) -> bool:
             True, if node is in the graph schema
             False, if node is not in graph schema
         """
-        if node_label in self.graph.nodes():
-            return True
-        else:
-            return False
+        return node_label in self.graph.nodes()
 
     def sub_schema_graph(
-        self, source: str, direction: str, size=None
+        self, source: str, direction: str, size: Optional[float] = None
     ) -> Optional[graphviz.Digraph]:
         """Create a sub-schema graph
         Args:
             source, str: source node label to start graph
             direction, str: direction to create the vizualization, choose from "up", "down", "both"
-            size, float: max height and width of the graph, if one value provided it is used for both.
+            size, float: max height and width of the graph, if one value provided it is used for
+              both.
         Returns:
             Sub-schema graph viz
         """
         if direction == "down":
             edges = list(nx.edge_bfs(self.graph, [source]))
             return visualize(edges, size=size)
-        elif direction == "up":
+        if direction == "up":
             paths = self.find_parent_classes(source)
             edges = []
             for _path in paths:
@@ -756,7 +921,7 @@ def sub_schema_graph(
                 for i in range(0, len(_path) - 1):
                     edges.append((_path[i], _path[i + 1]))
             return visualize(edges, size=size)
-        elif direction == "both":
+        if direction == "both":
             paths = self.find_parent_classes(source)
             edges = list(nx.edge_bfs(self.graph, [source]))
             for _path in paths:
@@ -764,3 +929,4 @@ def sub_schema_graph(
                 for i in range(0, len(_path) - 1):
                     edges.append((_path[i], _path[i + 1]))
             return visualize(edges, size=size)
+        return None
diff --git a/schematic/schemas/data_model_json_schema.py b/schematic/schemas/data_model_json_schema.py
index 844f5a4ec..d21ca2744 100644
--- a/schematic/schemas/data_model_json_schema.py
+++ b/schematic/schemas/data_model_json_schema.py
@@ -1,45 +1,52 @@
+"Data Model Json Schema"
+
 import logging
-import networkx as nx
 import os
-from typing import Any, Dict, Optional, Text, List
+from typing import Any, Optional
+
+import networkx as nx  # type: ignore
 
 from schematic.schemas.data_model_graph import DataModelGraphExplorer
 from schematic.schemas.data_model_relationships import DataModelRelationships
-
 from schematic.utils.validate_utils import rule_in_rule_list
 
 logger = logging.getLogger(__name__)
 
 
 class DataModelJSONSchema:
+    "Data Model Json Schema"
+
     def __init__(
         self,
         jsonld_path: str,
         graph: nx.MultiDiGraph,
     ):
+        # pylint:disable=fixme
         # TODO: Change jsonld_path to data_model_path (can work with CSV too)
         self.jsonld_path = jsonld_path
+        self.jsonld_path_root: Optional[str] = None
         self.graph = graph  # Graph would be fully made at this point.
         self.dmge = DataModelGraphExplorer(self.graph)
         self.dmr = DataModelRelationships()
         self.rel_dict = self.dmr.relationships_dictionary
 
     def get_array_schema(
-        self, node_range: List[str], node_name: str, blank=False
-    ) -> Dict[str, Dict[str, List[str]]]:
-        """Add a list of nodes to the "enum" key in a given JSON schema object.
-           Allow a node to be mapped to any subset of the list
+        self, node_range: list[str], node_name: str, blank: bool = False
+    ) -> dict[str, dict[str, Any]]:
+        """
+        Add a list of nodes to the "enum" key in a given JSON schema object.
+        Allow a node to be mapped to any subset of the list
 
         Args:
-                node_name: Name of the "main" / "head" key in the JSON schema / object.
-                node_range: List of nodes to be added to the JSON object.
-                blank: If True, add empty node to end of node list.
-                           If False, do not add empty node to end of node list.
+            node_range (list[str]): List of nodes to be added to the JSON object.
+            node_name (str): Name of the "main" / "head" key in the JSON schema / object.
+            blank (bool, optional): Defaults to False.
+              If True, add empty node to end of node list.
+              If False, do not add empty node to end of node list.
 
         Returns:
-                JSON object with array validation rule.
+            dict[str, dict[str, Any]]: JSON object with array validation rule.
         """
-
         schema_node_range_array = {
             node_name: {
                 "type": "array",
@@ -52,7 +59,7 @@ def get_array_schema(
 
     def get_non_blank_schema(
         self, node_name: str
-    ) -> Dict[str, dict[str, Any]]:  # can't define heterogenous Dict generic types
+    ) -> dict[str, dict[str, Any]]:  # can't define heterogenous Dict generic types
         """Get a schema rule that does not allow null or empty values.
 
         Args:
@@ -66,18 +73,20 @@ def get_non_blank_schema(
         return non_blank_schema
 
     def get_range_schema(
-        self, node_range: List[str], node_name: str, blank=False
-    ) -> Dict[str, Dict[str, List[str]]]:
-        """Add a list of nodes to the "enum" key in a given JSON schema object.
+        self, node_range: list[str], node_name: str, blank=False
+    ) -> dict[str, dict[str, list[str]]]:
+        """
+        Add a list of nodes to the "enum" key in a given JSON schema object.
 
         Args:
-                node_name: Name of the "main" / "head" key in the JSON schema / object.
-                node_range: List of nodes to be added to the JSON object.
-                blank: If True, add empty node to end of node list.
-                           If False, do not add empty node to end of node list.
+            node_range (list[str]): List of nodes to be added to the JSON object.
+            node_name (str): Name of the "main" / "head" key in the JSON schema / object.
+            blank (bool, optional): Defaults to False.
+              If True, add empty node to end of node list.
+              If False, do not add empty node to end of node list.
 
         Returns:
-                JSON object with nodes.
+            dict[str, dict[str, list[str]]]: JSON object with nodes.
         """
         if blank:
             schema_node_range = {node_name: {"enum": node_range + [""]}}
@@ -88,21 +97,33 @@ def get_range_schema(
 
     def get_json_validation_schema(
         self, source_node: str, schema_name: str
-    ) -> Dict[str, dict[str, Any]]:
+    ) -> dict[str, dict[str, Any]]:
         """
-        Consolidated method that aims to gather dependencies and value constraints across terms / nodes in a schema.org schema and store them in a jsonschema /JSON Schema schema.
+        Consolidated method that aims to gather dependencies and value constraints across terms
+        / nodes in a schema.org schema and store them in a jsonschema /JSON Schema schema.
 
-        It does so for any given node in the schema.org schema (recursively) using the given node as starting point in the following manner:
-        1) Find all the nodes / terms this node depends on (which are required as "additional metadata" given this node is "required").
-        2) Find all the allowable metadata values / nodes that can be assigned to a particular node (if such a constraint is specified on the schema).
+        It does so for any given node in the schema.org schema (recursively) using the given
+          node as starting point in the following manner:
+        1) Find all the nodes / terms this node depends on (which are required as
+          "additional metadata" given this node is "required").
+        2) Find all the allowable metadata values / nodes that can be assigned to a particular
+          node (if such a constraint is specified on the schema).
 
         Args:
-                source_node: Node from which we can start recursive dependancy traversal (as mentioned above).
-                schema_name: Name assigned to JSON-LD schema (to uniquely identify it via URI when it is hosted on the Internet).
+                source_node: Node from which we can start recursive dependancy traversal
+                  (as mentioned above).
+                schema_name: Name assigned to JSON-LD schema (to uniquely identify it via URI
+                  when it is hosted on the Internet).
 
         Returns:
                 JSON Schema as a dictionary.
         """
+        # pylint:disable=too-many-locals
+        # pylint:disable=too-many-branches
+        # pylint:disable=too-many-statements
+        # pylint:disable=too-many-nested-blocks
+        # pylint:disable=fixme
+
         json_schema = {
             "$schema": "http://json-schema.org/draft-07/schema#",
             "$id": "http://example.com/" + schema_name,
@@ -113,20 +134,16 @@ def get_json_validation_schema(
             "allOf": [],
         }
 
-        nodes_to_process = (
-            []
-        )  # list of nodes to be checked for dependencies, starting with the source node
-        processed_nodes = (
-            []
-        )  # keep of track of nodes whose dependencies have been processed
-        reverse_dependencies = (
-            {}
-        )  # maintain a map between conditional nodes and their dependencies (reversed) -- {dependency : conditional_node}
-        range_domain_map = (
-            {}
-        )  # maintain a map between range nodes and their domain nodes {range_value : domain_value}
+        # list of nodes to be checked for dependencies, starting with the source node
+        nodes_to_process = []
+        # keep of track of nodes whose dependencies have been processed
+        processed_nodes = []
+        # maintain a map between conditional nodes and their dependencies
+        # (reversed) -- {dependency : conditional_node}
+        reverse_dependencies: dict[str, Any] = {}
+        # maintain a map between range nodes and their domain nodes {range_value : domain_value}
         # the domain node is very likely the parentof ("parentOf" relationship) of the range node
-
+        range_domain_map: dict[str, Any] = {}
         root_dependencies = self.dmge.get_adjacent_nodes_by_relationship(
             node_label=source_node,
             relationship=self.rel_dict["requiresDependency"]["edge_key"],
@@ -165,18 +182,21 @@ def get_json_validation_schema(
                 ]
 
                 # updating map between node and node's valid values
-                for n in node_range_d:
-                    if not n in range_domain_map:
-                        range_domain_map[n] = []
-                    range_domain_map[n].append(node_display_name)
-
-                # can this node be map to the empty set (if required no; if not required yes)
-                # TODO: change "required" to different term, required may be a bit misleading (i.e. is the node required in the schema)
-                node_required = self.dmge.get_node_required(node_label=process_node)
-
-                # get any additional validation rules associated with this node (e.g. can this node be mapped to a list of other nodes)
-                node_validation_rules = self.dmge.get_node_validation_rules(
-                    node_display_name=node_display_name
+                for node in node_range_d:
+                    if not node in range_domain_map:
+                        range_domain_map[node] = []
+                    range_domain_map[node].append(node_display_name)
+
+                # Get node validation rules for the current node, and the given component
+                node_validation_rules = self.dmge.get_component_node_validation_rules(
+                    manifest_component=source_node, node_display_name=node_display_name
+                )
+
+                # Get if the node is required for the given component
+                node_required = self.dmge.get_component_node_required(
+                    manifest_component=source_node,
+                    node_validation_rules=node_validation_rules,
+                    node_display_name=node_display_name,
                 )
 
                 if node_display_name in reverse_dependencies:
@@ -192,7 +212,8 @@ def get_json_validation_schema(
 
                         if node_validation_rules:
                             # if this node has extra validation rules process them
-                            # TODO: abstract this into its own validation rule constructor/generator module/class
+                            # TODO: abstract this into its own validation rule constructor/generator
+                            # module/class
                             if rule_in_rule_list("list", node_validation_rules):
                                 # if this node can be mapped to a list of nodes
                                 # set its schema accordingly
@@ -211,7 +232,6 @@ def get_json_validation_schema(
                     # set schema conditional dependencies
                     for node in reverse_dependencies[node_display_name]:
                         # set all of the conditional nodes that require this process node
-
                         # get node domain if any
                         # ow this node is a conditional requirement
                         if node in range_domain_map:
@@ -227,7 +247,9 @@ def get_json_validation_schema(
                                     }
                                 )
 
-                                # given node conditional are satisfied, this process node (which is dependent on these conditionals) has to be set or not depending on whether it is required
+                                # given node conditional are satisfied, this process node
+                                # (which is dependent on these conditionals) has to be set
+                                # or not depending on whether it is required
                                 if node_range:
                                     dependency_properties = self.get_range_schema(
                                         node_range=node_range_d,
@@ -239,7 +261,8 @@ def get_json_validation_schema(
                                         if rule_in_rule_list(
                                             "list", node_validation_rules
                                         ):
-                                            # TODO: get_range_schema and get_range_schema have similar behavior - combine in one module
+                                            # TODO: get_range_schema and get_range_schema have
+                                            # similar behavior - combine in one module
                                             dependency_properties = (
                                                 self.get_array_schema(
                                                     node_range=node_range_d,
@@ -299,7 +322,8 @@ def get_json_validation_schema(
                         json_schema["required"] += [node_display_name]
 
                     elif process_node in root_dependencies:
-                        # node doesn't have conditionals and is not required; it belongs in the schema only if it is in root's dependencies
+                        # node doesn't have conditionals and is not required; it belongs in the
+                        # schema only if it is in root's dependencies
 
                         if node_range:
                             schema_valid_vals = self.get_range_schema(
@@ -322,9 +346,10 @@ def get_json_validation_schema(
                         json_schema["properties"].update(schema_valid_vals)
 
                     else:
-                        # node doesn't have conditionals and it is not required and it is not a root dependency
-                        # the node doesn't belong in the schema
-                        # do not add to processed nodes since its conditional may be traversed at a later iteration (though unlikely for most schemas we consider)
+                        # node doesn't have conditionals and it is not required and it
+                        # is not a root dependency the node doesn't belong in the schema
+                        # do not add to processed nodes since its conditional may be traversed
+                        # at a later iteration (though unlikely for most schemas we consider)
                         node_is_processed = False
 
                 # add process node as a conditional to its dependencies
@@ -358,26 +383,17 @@ def get_json_validation_schema(
 
         logger.info("JSON schema successfully generated from schema.org schema!")
 
-        # if no conditional dependencies were added we can't have an empty 'AllOf' block in the schema, so remove it
+        # if no conditional dependencies were added we can't have an empty 'AllOf'
+        # block in the schema, so remove it
         if not json_schema["allOf"]:
             del json_schema["allOf"]
 
         # If no config value and SchemaGenerator was initialized with
         # a JSON-LD path, construct
         if self.jsonld_path is not None:
-            self.jsonld_path_root, jsonld_ext = os.path.splitext(self.jsonld_path)
+            self.jsonld_path_root, _ = os.path.splitext(self.jsonld_path)
             prefix = self.jsonld_path_root
             prefix_root, prefix_ext = os.path.splitext(prefix)
             if prefix_ext == ".model":
                 prefix = prefix_root
-            json_schema_log_file = f"{prefix}.{source_node}.schema.json"
-        """
-		# Commenting out loggins since the JSON Schema file is not currently saved.
-		logger.info(
-			"The JSON schema file can be inspected by setting the following "
-			"nested key in the configuration: (model > location)."
-		)
-
-		logger.info(f"JSON schema file log stored as {json_schema_log_file}")
-		"""
         return json_schema
diff --git a/schematic/schemas/data_model_jsonld.py b/schematic/schemas/data_model_jsonld.py
index 6d4c2b48c..58f86cfe6 100644
--- a/schematic/schemas/data_model_jsonld.py
+++ b/schematic/schemas/data_model_jsonld.py
@@ -1,10 +1,11 @@
-import copy
-from dataclasses import dataclass, field, asdict
-from dataclasses_json import config, dataclass_json
+"""Data Model Jsonld"""
+
 import json
 import logging
+import copy
+from dataclasses import dataclass, field
+from dataclasses_json import config, dataclass_json
 
-from typing import Any, Dict, Optional, Text, List
 import networkx as nx
 
 from schematic.schemas.data_model_graph import DataModelGraphExplorer
@@ -12,17 +13,22 @@
 from schematic.utils.schema_utils import (
     get_label_from_display_name,
     convert_bool_to_str,
-    strip_context,
 )
 
 logging.basicConfig()
 logger = logging.getLogger(__name__)
 
+# pylint:disable=fixme
+# pylint:disable=invalid-name
+# pylint:disable=too-many-instance-attributes
+
 
 @dataclass_json
 @dataclass
 class BaseTemplate:
-    magic_context: str = field(
+    """Base Template"""
+
+    magic_context: dict[str, str] = field(
         default_factory=lambda: {
             "bts": "http://schema.biothings.io/",
             "rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
@@ -32,7 +38,9 @@ class BaseTemplate:
         },
         metadata=config(field_name="@context"),
     )
-    magic_graph: str = field(default_factory=list, metadata=config(field_name="@graph"))
+    magic_graph: list = field(
+        default_factory=list, metadata=config(field_name="@graph")
+    )
     magic_id: str = field(
         default="http://schema.biothings.io/#0.1", metadata=config(field_name="@id")
     )
@@ -41,6 +49,8 @@ class BaseTemplate:
 @dataclass_json
 @dataclass
 class PropertyTemplate:
+    """Property Template"""
+
     magic_id: str = field(default="", metadata=config(field_name="@id"))
     magic_type: str = field(default="rdf:Property", metadata=config(field_name="@type"))
     magic_comment: str = field(default="", metadata=config(field_name="rdfs:comment"))
@@ -68,6 +78,7 @@ class PropertyTemplate:
 @dataclass_json
 @dataclass
 class ClassTemplate:
+    "Class Template"
     magic_id: str = field(default="", metadata=config(field_name="@id"))
     magic_type: str = field(default="rdfs:Class", metadata=config(field_name="@type"))
     magic_comment: str = field(default="", metadata=config(field_name="rdfs:comment"))
@@ -98,14 +109,14 @@ class ClassTemplate:
     )
 
 
-class DataModelJsonLD(object):
+class DataModelJsonLD:
     """
     #Interface to JSONLD_object
     """
 
-    def __init__(self, Graph: nx.MultiDiGraph, output_path: str = ""):
+    def __init__(self, graph: nx.MultiDiGraph, output_path: str = ""):
         # Setup
-        self.graph = Graph  # Graph would be fully made at this point.
+        self.graph = graph  # Graph would be fully made at this point.
         self.dmr = DataModelRelationships()
         self.rel_dict = self.dmr.relationships_dictionary
         self.dmge = DataModelGraphExplorer(self.graph)
@@ -113,22 +124,30 @@ def __init__(self, Graph: nx.MultiDiGraph, output_path: str = ""):
 
         # Gather the templates
         base_template = BaseTemplate()
-        self.base_jsonld_template = json.loads(base_template.to_json())
+        self.base_jsonld_template = json.loads(
+            base_template.to_json()  # pylint:disable=no-member
+        )
 
         property_template = PropertyTemplate()
-        self.property_template = json.loads(property_template.to_json())
+        self.property_template = json.loads(
+            property_template.to_json()  # pylint:disable=no-member
+        )
 
         class_template = ClassTemplate()
-        self.class_template = json.loads(class_template.to_json())
+        self.class_template = json.loads(
+            class_template.to_json()  # pylint:disable=no-member
+        )
 
     def get_edges_associated_with_node(
         self, node: str
-    ) -> List[tuple[str, str, dict[str, int]]]:
+    ) -> list[tuple[str, str, dict[str, int]]]:
         """Retrieve all edges traveling in and out of a node.
         Args:
             node, str: Label of node in the graph to look for assiciated edges
         Returns:
-            node_edges, list: List of Tuples of edges associated with the given node, tuple contains the two nodes, plus the weight dict associated with the edge connection.
+            node_edges, list: List of Tuples of edges associated with the given node,
+              tuple contains the two nodes, plus the weight dict associated with
+              the edge connection.
         """
         node_edges = list(self.graph.in_edges(node, data=True))
         node_edges.extend(list(self.graph.out_edges(node, data=True)))
@@ -136,15 +155,17 @@ def get_edges_associated_with_node(
 
     def get_edges_associated_with_property_nodes(
         self, node: str
-    ) -> List[tuple[str, str, dict[str, int]]]:
+    ) -> list[tuple[str, str, dict[str, int]]]:
         """Get edges associated with property nodes to make sure we add that relationship.
         Args:
             node, str: Label of node property in the graph to look for assiciated edges
         Returns:
-            node_edges, list: List of Tuples of edges associated with the given node, tuple contains the two nodes, plus the weight dict associated with the edge connection.
+            node_edges, list: List of Tuples of edges associated with the given node,
+              tuple contains the two nodes, plus the weight dict associated with the
+              edge connection.
         """
         # Get edge keys for domainIncludes and subclassOf
-        domainIncludes_edge_key = self.rel_dict["domainIncludes"]["edge_key"]
+        domain_includes_edge_key = self.rel_dict["domainIncludes"]["edge_key"]
         node_edges = []
         # Get dict of edges for the current property node
         node_edges_dict = self.graph[node]
@@ -152,15 +173,19 @@ def get_edges_associated_with_property_nodes(
             # Look through relationships in the edge dictionary
             for edge_key in edge_dict:
                 # If the edge is a property or subclass then add the edges to the list
-                if edge_key in [domainIncludes_edge_key]:
+                if edge_key in [domain_includes_edge_key]:
                     node_edges.append((node, node_2, edge_dict[edge_key]))
         return node_edges
 
-    def add_edge_rels_to_template(self, template: dict, rel_vals: dict, node: str):
+    def add_edge_rels_to_template(
+        self, template: dict, rel_vals: dict, node: str
+    ):  # pylint:disable=too-many-branches
         """
         Args:
-            template, dict: single class or property JSONLD template that is in the process of being filled.
-            rel_vals, dict: sub relationship dict for a given relationship (contains informtion like, 'edge_rel', 'jsonld_key' etc..)
+            template, dict: single class or property JSONLD template that is in the process of being
+             filled.
+            rel_vals, dict: sub relationship dict for a given relationship (contains informtion
+              like 'edge_rel', 'jsonld_key' etc..)
             node, str: node whose edge information is presently being added to the JSONLD
         Returns:
         """
@@ -175,7 +200,7 @@ def add_edge_rels_to_template(self, template: dict, rel_vals: dict, node: str):
             node_edges.extend(property_node_edges)
 
         # Get node pairs and weights for each edge
-        for node_1, node_2, weight in node_edges:
+        for node_1, node_2, _ in node_edges:  # pylint:disable=too-many-nested-blocks
             # Retrieve the relationship(s) and related info between the two nodes
             node_edge_relationships = self.graph[node_1][node_2]
 
@@ -185,17 +210,18 @@ def add_edge_rels_to_template(self, template: dict, rel_vals: dict, node: str):
             # Check if edge_key is even one of the relationships for this node pair.
             if edge_key in node_edge_relationships:
                 # for each relationship between the given nodes
-                for relationship, weight_dict in node_edge_relationships.items():
+                for relationship in node_edge_relationships.keys():
                     # If the relationship defined and edge_key
                     if relationship == edge_key:
                         # TODO: rewrite to use edge_dir
-                        domainIncludes_edge_key = self.rel_dict["domainIncludes"][
+                        domain_includes_edge_key = self.rel_dict["domainIncludes"][
                             "edge_key"
                         ]
-                        subclassOf_edge_key = self.rel_dict["subClassOf"]["edge_key"]
-                        if edge_key in [subclassOf_edge_key]:
+                        subclass_of_edge_key = self.rel_dict["subClassOf"]["edge_key"]
+                        if edge_key in [subclass_of_edge_key]:
                             if node_2 == node:
-                                # Make sure the key is in the template (differs between properties and classes)
+                                # Make sure the key is in the template
+                                # (differs between properties and classes)
                                 if rel_vals["jsonld_key"] in template.keys():
                                     node_1_id = {"@id": "bts:" + node_1}
                                     # TODO Move this to a helper function to clear up.
@@ -209,11 +235,10 @@ def add_edge_rels_to_template(self, template: dict, rel_vals: dict, node: str):
                                         template[rel_vals["jsonld_key"]].append(
                                             node_1_id
                                         )
-                                    else:
-                                        template[rel_vals["jsonld_key"]] == node_1
-                        elif edge_key in [domainIncludes_edge_key]:
+                        elif edge_key in [domain_includes_edge_key]:
                             if node_1 == node:
-                                # Make sure the key is in the template (differs between properties and classes)
+                                # Make sure the key is in the template
+                                # (differs between properties and classes)
                                 if rel_vals["jsonld_key"] in template.keys():
                                     node_2_id = {"@id": "bts:" + node_2}
                                     # TODO Move this to a helper function to clear up.
@@ -227,11 +252,10 @@ def add_edge_rels_to_template(self, template: dict, rel_vals: dict, node: str):
                                         template[rel_vals["jsonld_key"]].append(
                                             node_2_id
                                         )
-                                    else:
-                                        template[rel_vals["jsonld_key"]] == node_2
                         else:
                             if node_1 == node:
-                                # Make sure the key is in the template (differs between properties and classes)
+                                # Make sure the key is in the template
+                                # (differs between properties and classes)
                                 if rel_vals["jsonld_key"] in template.keys():
                                     node_2_id = {"@id": "bts:" + node_2}
                                     # TODO Move this to a helper function to clear up.
@@ -245,18 +269,19 @@ def add_edge_rels_to_template(self, template: dict, rel_vals: dict, node: str):
                                         template[rel_vals["jsonld_key"]].append(
                                             node_2_id
                                         )
-                                    else:
-                                        template[rel_vals["jsonld_key"]] == node_2
         return template
 
     def add_node_info_to_template(self, template, rel_vals, node):
         """For a given node and relationship, add relevant value to template
         Args:
-            template, dict: single class or property JSONLD template that is in the process of being filled.
-            rel_vals, dict: sub relationship dict for a given relationship (contains informtion like, 'edge_rel', 'jsonld_key' etc..)
+            template, dict: single class or property JSONLD template that is in the process
+              of being filled.
+            rel_vals, dict: sub relationship dict for a given relationship
+              (contains informtion like, 'edge_rel', 'jsonld_key' etc..)
             node, str: node whose information is presently being added to the JSONLD
         Returns:
-            template, dict: single class or property JSONLD template that is in the process of being filled, and now has had additional node information added.
+            template, dict: single class or property JSONLD template that is in the
+              process of being filled, and now has had additional node information added.
         """
         # Get label for relationship used in the graph
         node_label = rel_vals["node_label"]
@@ -269,20 +294,25 @@ def add_node_info_to_template(self, template, rel_vals, node):
         return template
 
     def fill_entry_template(self, template: dict, node: str) -> dict:
-        """Fill in a blank JSONLD template with information for each node. All relationships are filled from the graph, based on the type of information (node or edge)
+        """
+        Fill in a blank JSONLD template with information for each node.
+        All relationships are filled from the graph, based on the type of information
+          (node or edge)
+
         Args:
-            template, dict: empty class or property template to be filled with information for the given node.
+            template, dict: empty class or property template to be filled with
+              information for the given node.
             node, str: target node to fill the template out for.
         Returns:
-            template, dict: filled class or property template, that has been processed and cleaned up.
+            template, dict: filled class or property template, that has been
+              processed and cleaned up.
         """
         data_model_relationships = self.dmr.relationships_dictionary
 
         # For each field in template fill out with information from the graph
-        for rel, rel_vals in data_model_relationships.items():
-            key_context, key_rel = strip_context(context_value=rel_vals["jsonld_key"])
-
-            # Fill in the JSONLD template for this node, with data from the graph by looking up the nodes edge relationships, and the value information attached to the node.
+        for rel_vals in data_model_relationships.values():
+            # Fill in the JSONLD template for this node, with data from the graph by looking
+            # up the nodes edge relationships, and the value information attached to the node.
 
             # Fill edge information (done per edge type)
             if rel_vals["edge_rel"]:
@@ -317,12 +347,14 @@ def fill_entry_template(self, template: dict, node: str) -> dict:
     def add_contexts_to_entries(self, template: dict) -> dict:
         """
         Args:
-            template, dict: JSONLD template that has been filled up to the current node, with information
+            template, dict: JSONLD template that has been filled up to
+              the current node, with information
         Returns:
             template, dict: JSONLD template where contexts have been added back to certain values.
         Note: This will likely need to be modified when Contexts are truly added to the model
         """
-        for jsonld_key, entry in template.items():
+        # pylint:disable=comparison-with-callable
+        for jsonld_key in template.keys():
             # Retrieve the relationships key using the jsonld_key
             rel_key = []
 
@@ -337,11 +369,12 @@ def add_contexts_to_entries(self, template: dict) -> dict:
                     try:
                         # if possible pull standard function to get node information
                         rel_func = self.rel_dict[rel_key]["node_attr_dict"]["standard"]
-                    except:
+                    except:  # pylint:disable=bare-except
                         # if not pull default function to get node information
                         rel_func = self.rel_dict[rel_key]["node_attr_dict"]["default"]
 
-                    # Add appropritae contexts that have been removed in previous steps (for JSONLD) or did not exist to begin with (csv)
+                    # Add appropritae contexts that have been removed in previous steps
+                    # (for JSONLD) or did not exist to begin with (csv)
                     if (
                         rel_key == "id"
                         and rel_func == get_label_from_display_name
@@ -360,34 +393,47 @@ def add_contexts_to_entries(self, template: dict) -> dict:
         return template
 
     def clean_template(self, template: dict, data_model_relationships: dict) -> dict:
-        """Get rid of empty k:v pairs. Fill with a default if specified in the relationships dictionary.
+        """
+        Get rid of empty k:v pairs. Fill with a default if specified in the
+          relationships dictionary.
+
         Args:
-            template, dict: JSONLD template for a single entry, keys specified in property and class templates.
-            data_model_relationships, dict: dictionary containing information for each relationship type supported.
+            template, dict: JSONLD template for a single entry, keys specified in property
+              and class templates.
+            data_model_relationships, dict: dictionary containing information for each
+              relationship type supported.
         Returns:
-            template: JSONLD template where unfilled entries have been removed, or filled with default depending on specifications in the relationships dictionary.
+            template: JSONLD template where unfilled entries have been removed,
+              or filled with default depending on specifications in the relationships dictionary.
         """
         for rels in data_model_relationships.values():
             # Get the current relationships, jsonld key
             relationship_jsonld_key = rels["jsonld_key"]
-            # Check if the relationship_relationship_key is part of the template, and if it is, look to see if it has an entry
+            # Check if the relationship_relationship_key is part of the template,
+            # and if it is, look to see if it has an entry
             if (
                 relationship_jsonld_key in template.keys()
                 and not template[rels["jsonld_key"]]
             ):
-                # If there is no value recorded, fill out the template with the default relationship value (if recorded.)
+                # If there is no value recorded, fill out the template with the
+                # default relationship value (if recorded.)
                 if "jsonld_default" in rels.keys():
                     template[relationship_jsonld_key] = rels["jsonld_default"]
                 else:
-                    # If there is no default specified in the relationships dictionary, delete the empty value from the template.
+                    # If there is no default specified in the relationships dictionary,
+                    # delete the empty value from the template.
                     del template[relationship_jsonld_key]
         return template
 
     def reorder_template_entries(self, template: dict) -> dict:
-        """In JSONLD some classes or property keys have list values. We want to make sure these lists are ordered according to the order supplied by the user.
+        """
+        In JSONLD some classes or property keys have list values.
+        We want to make sure these lists are ordered according to the order supplied by the user.
         This will look specically in lists and reorder those.
+
         Args:
-            template, dict: JSONLD template for a single entry, keys specified in property and class templates.
+            template, dict: JSONLD template for a single entry, keys specified in
+              property and class templates.
         Returns:
             template, dict: list entries re-ordered to match user supplied order.
         Note:
@@ -399,15 +445,16 @@ def reorder_template_entries(self, template: dict) -> dict:
             # Make sure dealing with an edge relationship:
             is_edge = [
                 "True"
-                for rel_key, rel_vals in self.rel_dict.items()
+                for rel_vals in self.rel_dict.values()
                 if rel_vals["jsonld_key"] == jsonld_key
-                if rel_vals["edge_rel"] == True
+                if rel_vals["edge_rel"]
             ]
 
-            # if the entry is of type list and theres more than one value in the list attempt to reorder
+            # if the entry is of type list and theres more than one value in the
+            # list attempt to reorder
             if is_edge and isinstance(entry, list) and len(entry) > 1:
                 # Get edge key from data_model_relationships using the jsonld_key:
-                key, edge_key = [
+                key, _ = [
                     (rel_key, rel_vals["edge_key"])
                     for rel_key, rel_vals in self.rel_dict.items()
                     if jsonld_key == rel_vals["jsonld_key"]
@@ -419,7 +466,10 @@ def reorder_template_entries(self, template: dict) -> dict:
                 )
                 if not len(entry) == len(sorted_edges):
                     logger.error(
-                        "There is an error with sorting values in the JSONLD, please issue a bug report."
+                        (
+                            "There is an error with sorting values in the JSONLD, "
+                            "please issue a bug report."
+                        )
                     )
 
                 edge_weights_dict = {edge: i for i, edge in enumerate(sorted_edges)}
@@ -430,7 +480,10 @@ def reorder_template_entries(self, template: dict) -> dict:
                 # Throw an error if ordered_edges does not get fully filled as expected.
                 if 0 in ordered_edges:
                     logger.error(
-                        "There was an issue getting values to match order specified in the data model, please submit a help request."
+                        (
+                            "There was an issue getting values to match order specified in "
+                            "the data model, please submit a help request."
+                        )
                     )
                 template[jsonld_key] = ordered_edges
         return template
@@ -460,8 +513,9 @@ def generate_jsonld_object(self):
         return json_ld_template
 
 
-def convert_graph_to_jsonld(Graph):
+def convert_graph_to_jsonld(graph):
+    """convert graph to jsonld"""
     # Make the JSONLD object
-    data_model_jsonld_converter = DataModelJsonLD(Graph=Graph)
+    data_model_jsonld_converter = DataModelJsonLD(graph=graph)
     jsonld_dm = data_model_jsonld_converter.generate_jsonld_object()
     return jsonld_dm
diff --git a/schematic/schemas/data_model_nodes.py b/schematic/schemas/data_model_nodes.py
index 2535e3f20..24de9cf37 100644
--- a/schematic/schemas/data_model_nodes.py
+++ b/schematic/schemas/data_model_nodes.py
@@ -1,9 +1,11 @@
+"""Data model Nodes"""
+
+from typing import Optional, Callable
+
 from inspect import isfunction
-import networkx as nx
+import networkx as nx  # type: ignore
 from rdflib import Namespace
-from typing import Any, Dict, Optional, Text, List, Literal, Callable
 
-from schematic.schemas.data_model_parser import DataModelJSONLDParser
 from schematic.schemas.data_model_relationships import DataModelRelationships
 
 from schematic.utils.schema_utils import (
@@ -13,15 +15,15 @@
     parse_validation_rules,
     DisplayLabelType,
 )
-from schematic.utils.validate_rules_utils import validate_schema_rules
-from schematic.schemas.curie import uri2curie, curie2uri
 
 
 class DataModelNodes:
+    """Data model Nodes"""
+
     def __init__(self, attribute_relationships_dict):
-        self.namespaces = dict(
-            rdf=Namespace("http://www.w3.org/1999/02/22-rdf-syntax-ns#")
-        )
+        self.namespaces = {
+            "rdf": Namespace("http://www.w3.org/1999/02/22-rdf-syntax-ns#")
+        }
         self.data_model_relationships = DataModelRelationships()
         self.value_relationships = (
             self.data_model_relationships.retreive_rel_headers_dict(edge=False)
@@ -36,9 +38,14 @@ def __init__(self, attribute_relationships_dict):
         self.node_relationships = list(self.edge_relationships_dictionary.values())
 
     def gather_nodes(self, attr_info: tuple) -> list:
-        """Take in a tuple containing attriute name and relationship dictionary, and find all nodes defined in attribute information.
+        """
+        Take in a tuple containing attriute name and relationship dictionary,
+          and find all nodes defined in attribute information.
+
         Args:
-            attr_info, tuple: (Display Name, Relationships Dictionary portion of attribute_relationships dictionary)
+            attr_info, tuple: (Display Name, Relationships Dictionary portion of
+              attribute_relationships dictionary)
+
         Returns:
             nodes, list: nodes related to the given node (specified in attr_info).
         Note:
@@ -65,7 +72,8 @@ def gather_all_nodes_in_model(self, attr_rel_dict: dict) -> list:
                         Relationships: {
                                     CSV Header: Value}}}
         Returns:
-            all_nodes, list: List of all node display names in the data model preserving order entered.
+            all_nodes, list: List of all node display names in the data model
+              preserving order entered.
         Note:
             Gathering nodes in this fashion ensures order is preserved.
         """
@@ -83,15 +91,20 @@ def get_rel_node_dict_info(self, relationship: str) -> Optional[tuple[str, dict]
             relationship, str: relationship key to match.
         Returns:
             rel_key, str: relationship node label
-            rel_node_dict, dict: node_attr_dict, from relationships dictionary for a given relationship
+            rel_node_dict, dict: node_attr_dict, from relationships dictionary for a
+              given relationship
         TODO: Move to data_model_relationships.
         """
-        for k, v in self.data_model_relationships.relationships_dictionary.items():
-            if k == relationship:
-                if "node_attr_dict" in v.keys():
-                    rel_key = v["node_label"]
-                    rel_node_dict = v["node_attr_dict"]
+        for (
+            key,
+            value,
+        ) in self.data_model_relationships.relationships_dictionary.items():
+            if key == relationship:
+                if "node_attr_dict" in value:
+                    rel_key = value["node_label"]
+                    rel_node_dict = value["node_attr_dict"]
                     return rel_key, rel_node_dict
+        return None
 
     def get_data_model_properties(self, attr_rel_dict: dict) -> list:
         """Identify all properties defined in the data model.
@@ -104,7 +117,7 @@ def get_data_model_properties(self, attr_rel_dict: dict) -> list:
             properties,list: properties defined in the data model
         """
         properties = []
-        for attribute, relationships in attr_rel_dict.items():
+        for relationships in attr_rel_dict.values():
             if "Properties" in relationships["Relationships"].keys():
                 properties.extend(relationships["Relationships"]["Properties"])
         properties = list(set(properties))
@@ -125,15 +138,18 @@ def get_entry_type(self, node_display_name: str) -> str:
 
     def run_rel_functions(
         self,
-        rel_func: callable,
+        rel_func: Callable,
         node_display_name: str = "",
         key: str = "",
-        attr_relationships={},
+        attr_relationships=None,
         csv_header="",
         entry_type="",
         data_model_labels: DisplayLabelType = "class_label",
     ):
-        """This function exists to centralzie handling of functions for filling out node information, makes sure all the proper parameters are passed to each function.
+        """
+        This function exists to centralzie handling of functions for filling out node information,
+          makes sure all the proper parameters are passed to each function.
+
         Args:
             rel_func, callable: Function to call to get information to attach to the node
             node_display_name, str: node display name
@@ -147,38 +163,53 @@ def run_rel_functions(
 
         For legacy:
         elif key == 'id' and rel_func == get_label_from_display_name:
-            func_output = get_label_from_display_name(display_name =node_display_name, entry_type=entry_type)
+            func_output = get_label_from_display_name(
+                display_name =node_display_name, entry_type=entry_type
+            )
         """
+        # pylint: disable=too-many-arguments
+        # pylint: disable=too-many-return-statements
+        # pylint: disable=comparison-with-callable
+        if attr_relationships is None:
+            attr_relationships = {}
+
         if rel_func == get_attribute_display_name_from_label:
             return get_attribute_display_name_from_label(
                 node_display_name, attr_relationships
             )
 
-        elif rel_func == parse_validation_rules:
-            return parse_validation_rules(attr_relationships[csv_header])
+        if rel_func == parse_validation_rules:
+            rules = attr_relationships[csv_header]
+            if isinstance(rules, (dict, list)):
+                return parse_validation_rules(rules)
 
-        elif rel_func == get_label_from_display_name:
+        if rel_func == get_label_from_display_name:
             return get_label_from_display_name(
                 display_name=node_display_name,
                 entry_type=entry_type,
                 data_model_labels=data_model_labels,
             )
 
-        elif rel_func == convert_bool_to_str:
-            if type(attr_relationships[csv_header]) == str:
+        if rel_func == convert_bool_to_str:
+            if isinstance(attr_relationships[csv_header], str):
                 if attr_relationships[csv_header].lower() == "true":
                     return True
-                elif attr_relationships[csv_header].lower() == "false":
+                if attr_relationships[csv_header].lower() == "false":
                     return False
+                return None
 
-            elif type(attr_relationships[csv_header]) == bool:
+            if isinstance(attr_relationships[csv_header], bool):
                 return attr_relationships[csv_header]
 
-        else:
-            # Raise Error if the rel_func provided is not captured.
-            raise ValueError(
-                f"The function provided ({rel_func}) to define the relationship {key} is not captured in the function run_rel_functions, please update."
+            return None
+
+        # Raise Error if the rel_func provided is not captured.
+        raise ValueError(
+            (
+                f"The function provided ({rel_func}) to define the relationship {key} "
+                "is not captured in the function run_rel_functions, please update."
             )
+        )
 
     def generate_node_dict(
         self,
@@ -187,22 +218,29 @@ def generate_node_dict(
         data_model_labels: DisplayLabelType = "class_label",
     ) -> dict:
         """Gather information to be attached to each node.
+
+        Note:
+            If the default calls function, call that function for the default or alternate
+              implementation.
+            May need to update this logic for varying function calls. (for example the current
+              function takes in the node display name would need to update if new function took
+              in something else.)
+
         Args:
-            node_display_name, str: display name for current node
-            attr_rel_dict, dict: generated in data_model_parser
-                {Attribute Display Name: {
+            node_display_name (str): display name for current node
+            attr_rel_dict (dict): generated in data_model_parser
+              {Attribute Display Name: {
                         Relationships: {
                                     CSV Header: Value}}}
-            data_model_labels: str, display_label or class_label.
-                display_label, use the display name as a label, if it is valid (contains no blacklisted characters) otherwise will default to schema_label.
+            data_model_labels (DisplayLabelType, optional):str, display_label or class_label.
+                display_label, use the display name as a label, if it is valid (contains no
+                  blacklisted characters) otherwise will default to schema_label.
                 class_label, default, use standard class or property label.
+
         Returns:
-            node_dict, dict: dictionary of relationship information about the current node
-                {'displayName': '', 'label': '', 'comment': 'TBD', 'required': None, 'validationRules': [], 'isPartOf': '', 'uri': ''}
-        Note:
-            If the default calls function, call that function for the default or alternate implementation.
-            May need to update this logic for varying function calls. (for example the current function takes in the node display name
-            would need to update if new function took in something else.)
+            dict: dictionary of relationship information about the current node
+                {'displayName': '', 'label': '', 'comment': 'TBD', 'required': None,
+                 'validationRules': [], 'isPartOf': '', 'uri': ''}
         """
         # Strip whitespace from node display name
         node_display_name = node_display_name.strip()
@@ -271,16 +309,18 @@ def generate_node_dict(
 
         return node_dict
 
-    def generate_node(self, G: nx.MultiDiGraph, node_dict: dict) -> nx.MultiDiGraph:
+    def generate_node(self, graph: nx.MultiDiGraph, node_dict: dict) -> nx.MultiDiGraph:
         """Create a node and add it to the networkx multidigraph being built
         Args:
-            G, nx.MultiDigraph: networkx multidigraph object, that is in the process of being fully built.
+            graph, nx.MultiDigraph: networkx multidigraph object, that is in the process of
+              being fully built.
             node_dict, dict: dictionary of relationship information about the current node
         Returns:
-            G, nx.MultiDigraph: networkx multidigraph object, that has had an additional node added to it.
+            nx.MultiDigraph: networkx multidigraph object, that has had an additional
+              node added to it.
         """
-        G.add_node(node_dict["label"], **node_dict)
-        return G
+        graph.add_node(node_dict["label"], **node_dict)
+        return graph
 
     def edit_node(self):
         """Stub for future node editor."""
diff --git a/schematic/schemas/data_model_parser.py b/schematic/schemas/data_model_parser.py
index c5dbacb3e..2ddda42bd 100644
--- a/schematic/schemas/data_model_parser.py
+++ b/schematic/schemas/data_model_parser.py
@@ -1,8 +1,10 @@
-import logging
-import pandas as pd
+"Data Model Parser"
+
 import pathlib
+from typing import Any, Union, Optional
 
-from typing import Any, Dict, Optional, Text, List, Union
+import logging
+import pandas as pd
 
 from schematic.utils.df_utils import load_df
 from schematic.utils.io_utils import load_json
@@ -33,9 +35,9 @@ def __init__(
 
         self.path_to_data_model = path_to_data_model
         self.model_type = self.get_model_type()
-        self.base_schema_path = None
+        self.base_schema_path: Optional[str] = None
 
-    def _get_base_schema_path(self, base_schema: str = None) -> str:
+    def _get_base_schema_path(self, base_schema: Optional[str] = None) -> str:
         """Evaluate path to base schema.
 
         Args:
@@ -52,7 +54,10 @@ def _get_base_schema_path(self, base_schema: str = None) -> str:
         return self.base_schema_path
 
     def get_model_type(self) -> str:
-        """Parses the path to the data model to extract the extension and determine the data model type.
+        """
+        Parses the path to the data model to extract the extension and determine the
+          data model type.
+
         Args:
             path_to_data_model, str: path to data model
         Returns:
@@ -61,7 +66,7 @@ def get_model_type(self) -> str:
         """
         return pathlib.Path(self.path_to_data_model).suffix.replace(".", "").upper()
 
-    def parse_base_model(self) -> Dict:
+    def parse_base_model(self) -> dict:
         """Parse base data model that new model could be built upon.
         Returns:
             base_model, dict:
@@ -79,7 +84,7 @@ def parse_base_model(self) -> Dict:
         base_model = jsonld_parser.parse_jsonld_model(base_model_path)
         return base_model
 
-    def parse_model(self) -> Dict[str, dict[str, Any]]:
+    def parse_model(self) -> dict[str, dict[str, Any]]:
         """Given a data model type, instantiate and call the appropriate data model parser.
         Returns:
             model_dict, dict:
@@ -88,7 +93,8 @@ def parse_model(self) -> Dict[str, dict[str, Any]]:
                                     CSV Header: Value}}}
         Raises:
             Value Error if an incorrect model type is passed.
-        Note: in future will add base model parsing in this step too and extend new model off base model.
+        Note: in future will add base model parsing in this step too and extend new model
+          off base model.
         """
         # base_model = self.parse_base_model()
 
@@ -101,12 +107,18 @@ def parse_model(self) -> Dict[str, dict[str, Any]]:
             model_dict = jsonld_parser.parse_jsonld_model(self.path_to_data_model)
         else:
             raise ValueError(
-                f"Schematic only accepts models of type CSV or JSONLD, you provided a model type {self.model_type}, please resubmit in the proper format."
+                (
+                    "Schematic only accepts models of type CSV or JSONLD, "
+                    "you provided a model type "
+                    f"{self.model_type}, please resubmit in the proper format."
+                )
             )
         return model_dict
 
 
 class DataModelCSVParser:
+    """DataModelCSVParser"""
+
     def __init__(self):
         # Instantiate DataModelRelationships
         self.dmr = DataModelRelationships()
@@ -121,21 +133,19 @@ def __init__(self):
         # Get the type for each value that needs to be submitted.
         # using csv_headers as keys to match required_headers/relationship_types
         self.rel_val_types = {
-            v["csv_header"]: v["type"]
-            for k, v in self.rel_dict.items()
-            if "type" in v.keys()
+            value["csv_header"]: value["type"]
+            for value in self.rel_dict.values()
+            if "type" in value
         }
 
-    def check_schema_definition(self, model_df: pd.DataFrame) -> bool:
+    def check_schema_definition(self, model_df: pd.DataFrame) -> None:
         """Checks if a schema definition data frame contains the right required headers.
         Args:
-            model_df: a pandas dataframe containing schema definition; see example here: https://docs.google.com/spreadsheets/d/1J2brhqO4kpeHIkNytzlqrdIiRanXDr6KD2hqjOTC9hs/edit#gid=0
+            model_df: a pandas dataframe containing schema definition; see example here:
+              https://docs.google.com/spreadsheets/d/1J2brhqO4kpeHIkNytzlqrdIiRanXDr6KD2hqjOTC9hs/edit#gid=0
         Raises: Exception if model_df does not have the required headers.
         """
-        if set(self.required_headers).issubset(set(list(model_df.columns))):
-            logger.debug("Schema definition csv ready for processing!")
-            return
-        elif "Requires" in list(model_df.columns) or "Requires Component" in list(
+        if "Requires" in list(model_df.columns) or "Requires Component" in list(
             model_df.columns
         ):
             raise ValueError(
@@ -143,17 +153,19 @@ def check_schema_definition(self, model_df: pd.DataFrame) -> bool:
                 "Component' column headers. These columns were renamed to 'DependsOn' and "
                 "'DependsOn Component', respectively. Switch to the new column names."
             )
-        elif not set(self.required_headers).issubset(set(list(model_df.columns))):
+        if not set(self.required_headers).issubset(set(list(model_df.columns))):
             raise ValueError(
                 f"Schema extension headers: {set(list(model_df.columns))} "
                 f"do not match required schema headers: {self.required_headers}"
             )
-        return
+        if set(self.required_headers).issubset(set(list(model_df.columns))):
+            logger.debug("Schema definition csv ready for processing!")
 
     def parse_entry(self, attr: dict, relationship: str) -> Any:
         """Parse attr entry baed on type
         Args:
-            attr, dict: single row of a csv model in dict form, where only the required headers are keys. Values are the entries under each header.
+            attr, dict: single row of a csv model in dict form, where only the required
+              headers are keys. Values are the entries under each header.
             relationship, str: one of the header relationships to parse the entry of.
         Returns:
             parsed_rel_entry, any: parsed entry for downstream processing based on the entry type.
@@ -162,9 +174,10 @@ def parse_entry(self, attr: dict, relationship: str) -> Any:
         rel_val_type = self.rel_val_types[relationship]
         # Parse entry based on type:
         # If the entry should be preserved as a bool dont convert to str.
-        if rel_val_type == bool and type(attr[relationship]) == bool:
+        if rel_val_type == bool and isinstance(attr[relationship], bool):
             parsed_rel_entry = attr[relationship]
-        # Move strings to list if they are comma separated. Schema order is preserved, remove any empty strings added by trailing commas
+        # Move strings to list if they are comma separated. Schema order is preserved,
+        # remove any empty strings added by trailing commas
         elif rel_val_type == list:
             parsed_rel_entry = attr[relationship].strip().split(",")
             parsed_rel_entry = [r.strip() for r in parsed_rel_entry if r]
@@ -173,13 +186,16 @@ def parse_entry(self, attr: dict, relationship: str) -> Any:
             parsed_rel_entry = str(attr[relationship]).strip()
         else:
             raise ValueError(
-                "The value type recorded for this relationship, is not currently supported for CSV parsing. Please check with your DCC."
+                (
+                    "The value type recorded for this relationship, is not currently "
+                    "supported for CSV parsing. Please check with your DCC."
+                )
             )
         return parsed_rel_entry
 
     def gather_csv_attributes_relationships(
         self, model_df: pd.DataFrame
-    ) -> Dict[str, dict[str, Any]]:
+    ) -> dict[str, dict[str, Any]]:
         """Parse csv into a attributes:relationshps dictionary to be used in downstream efforts.
         Args:
             model_df: pd.DataFrame, data model that has been loaded into pandas DataFrame.
@@ -236,6 +252,8 @@ def parse_csv_model(
 
 
 class DataModelJSONLDParser:
+    """DataModelJSONLDParser"""
+
     def __init__(
         self,
     ):
@@ -244,30 +262,63 @@ def __init__(
         # Load relationships dictionary.
         self.rel_dict = self.dmr.define_data_model_relationships()
 
+    def parse_jsonld_dicts(
+        self, rel_entry: dict[str, str]
+    ) -> Union[str, dict[str, str]]:
+        """Parse incoming JSONLD dictionaries, only supported dictionaries are non-edge
+            dictionaries.
+        Note:
+            The only two dictionaries we expect are a single entry dictionary containing
+            id information and dictionaries where the key is the attribute label
+            (and it is expected to stay as the label). The individual rules per component are not
+            attached to nodes but rather parsed later in validation rule parsing.
+            So the keys do not need to be converted to display names.
+        Args:
+            rel_entry, Any: Given a single entry and relationship in a JSONLD data model,
+                the recorded value
+        Returns:
+            str, the JSONLD entry ID
+            dict, JSONLD dictionary entry returned.
+        """
+
+        # Retrieve ID from a dictionary recording the ID
+        if set(rel_entry.keys()) == {"@id"}:
+            parsed_rel_entry = rel_entry["@id"]
+        # Parse any remaining dictionaries
+        else:
+            parsed_rel_entry = rel_entry
+        return parsed_rel_entry
+
     def parse_entry(
         self,
-        rel_entry: any,
+        rel_entry: Any,
         id_jsonld_key: str,
-        dn_label_dict: dict[str:str],
-        model_jsonld: dict,
+        model_jsonld: list[dict],
     ) -> Any:
         """Parse an input entry based on certain attributes
+
         Args:
-            rel_entry: Given a single entry and relationship in a JSONLD data model, the recorded value
-            id_jsonld_key, str: the jsonld key for id
+            rel_entry: Given a single entry and relationship in a JSONLD data model,
+                the recorded value
+            id_jsonld_key: str, the jsonld key for id
+            model_jsonld: list[dict], list of dictionaries, each dictionary is an entry
+                in the jsonld data model
         Returns:
-            parsed_rel_entry: an entry that has been parsed base on its input type and characteristics.
+            Any: n entry that has been parsed base on its input type and
+              characteristics.
         """
-        # Retrieve ID from single value dictionary
-        if type(rel_entry) == dict and len(rel_entry.keys()) == 1:
-            parsed_rel_entry = rel_entry["@id"]
-        # Parse list of dictionaries to make a list of entries with context stripped (will update this section when contexts added.)
-        elif type(rel_entry) == list and type(rel_entry[0]) == dict:
+        # Parse dictionary entries
+        if isinstance(rel_entry, dict):
+            parsed_rel_entry = self.parse_jsonld_dicts(rel_entry)
+
+        # Parse list of dictionaries to make a list of entries with context stripped (will update
+        # this section when contexts added.)
+        elif isinstance(rel_entry, list) and isinstance(rel_entry[0], dict):
             parsed_rel_entry = self.convert_entry_to_dn_label(
                 [r[id_jsonld_key].split(":")[1] for r in rel_entry], model_jsonld
             )
         # Strip context from string and convert true/false to bool
-        elif type(rel_entry) == str:
+        elif isinstance(rel_entry, str):
             # Remove contexts and treat strings as appropriate.
             if ":" in rel_entry and "http:" not in rel_entry:
                 parsed_rel_entry = rel_entry.split(":")[1]
@@ -288,7 +339,9 @@ def parse_entry(
         return parsed_rel_entry
 
     def label_to_dn_dict(self, model_jsonld: list[dict]):
-        """Generate a dictionary of labels to display name, so can easily look up display names using the label.
+        """
+        Generate a dictionary of labels to display name, so can easily look up
+          display names using the label.
         Args:
             model_jsonld: list of dictionaries, each dictionary is an entry in the jsonld data model
         Returns:
@@ -308,26 +361,28 @@ def convert_entry_to_dn_label(
     ) -> Union[str, list]:
         """Convert a parsed entry to display name, taking into account the entry type
         Args:
-            parsed_rel_entry: an entry that has been parsed base on its input type and characteristics.
+            parsed_rel_entry: an entry that has been parsed base on its input type
+              and characteristics.
             model_jsonld: list of dictionaries, each dictionary is an entry in the jsonld data model
         Returns:
-            parsed_rel_entry: an entry that has been parsed based on its input type and characteristics, and converted to display names.
+            parsed_rel_entry: an entry that has been parsed based on its input type and
+              characteristics, and converted to display names.
         """
         # Get a dictionary of display_names mapped to labels
         dn_label_dict = self.label_to_dn_dict(model_jsonld=model_jsonld)
         # Handle if using the display name as the label
-        if type(parsed_rel_entry) == list:
+        if isinstance(parsed_rel_entry, list):
             parsed_rel_entry = [
                 dn_label_dict.get(entry) if dn_label_dict.get(entry) else entry
                 for entry in parsed_rel_entry
             ]
-        elif type(parsed_rel_entry) == str:
+        elif isinstance(parsed_rel_entry, str):
             converted_label = dn_label_dict.get(parsed_rel_entry)
             if converted_label:
                 parsed_rel_entry = dn_label_dict.get(parsed_rel_entry)
         return parsed_rel_entry
 
-    def gather_jsonld_attributes_relationships(self, model_jsonld: List[dict]) -> Dict:
+    def gather_jsonld_attributes_relationships(self, model_jsonld: list[dict]) -> dict:
         """
         Args:
             model_jsonld: list of dictionaries, each dictionary is an entry in the jsonld data model
@@ -339,24 +394,26 @@ def gather_jsonld_attributes_relationships(self, model_jsonld: List[dict]) -> Di
         Notes:
             - Unlike a CSV the JSONLD might already have a base schema attached to it.
               So the attributes:relationship dictionary for importing a CSV vs JSONLD may not match.
-            - It is also just about impossible to extract attributes explicitly. Using a dictionary should avoid duplications.
+            - It is also just about impossible to extract attributes explicitly. Using a dictionary
+              should avoid duplications.
             - This is a promiscuous capture and will create an attribute for each model entry.
             - Currently only designed to capture the same information that would be encoded in CSV,
                 can be updated in the future.
         TODO:
-            - Find a way to delete non-attribute keys, is there a way to reliable distinguish after the fact?
+            - Find a way to delete non-attribute keys, is there a way to reliable distinguish
+              after the fact?
             - Right now, here we are stripping contexts, will need to track them in the future.
         """
+        # pylint:disable=too-many-locals
+        # pylint:disable=too-many-branches
+        # pylint:disable=too-many-nested-blocks
 
         # Retrieve relevant JSONLD keys.
         jsonld_keys_to_extract = ["label", "subClassOf", "id", "displayName"]
-        label_jsonld_key, subclassof_jsonld_key, id_jsonld_key, dn_jsonld_key = [
+        label_jsonld_key, _, id_jsonld_key, dn_jsonld_key = [
             self.rel_dict[key]["jsonld_key"] for key in jsonld_keys_to_extract
         ]
 
-        # Get a dictionary of display names to labels to identify values explicitly recorded
-        dn_label_dict = self.label_to_dn_dict(model_jsonld=model_jsonld)
-
         # Build the attr_rel_dictionary
         attr_rel_dictionary = {}
         # Move through each entry in the jsonld model
@@ -370,41 +427,49 @@ def gather_jsonld_attributes_relationships(self, model_jsonld: List[dict]) -> Di
                 attr_key = entry[label_jsonld_key]
 
             # If the entry has not already been added to the dictionary, add it.
-            if attr_key not in attr_rel_dictionary.keys():
+            if attr_key not in attr_rel_dictionary:
                 attr_rel_dictionary.update(attr_dict_template(attr_key))
 
             # Add relationships for each entry
             # Go through each defined relationship type (rel_key) and its attributes (rel_vals)
             for rel_key, rel_vals in self.rel_dict.items():
-                # Determine if current entry in the for loop, can be described by the current relationship that is being cycled through.
-                # used to also check "csv_header" in rel_vals.keys() which allows all JSONLD values through even if it does not have a CSV counterpart, will allow other values thorough in the else statement now
+                # Determine if current entry in the for loop, can be described by the current
+                # relationship that is being cycled through.
+                # used to also check "csv_header" in rel_vals.keys() which allows all JSONLD
+                # values through even if it does not have a CSV counterpart, will allow other
+                # values thorough in the else statement now
                 if rel_vals["jsonld_key"] in entry.keys() and rel_vals["csv_header"]:
                     # Retrieve entry value associated with the given relationship
                     rel_entry = entry[rel_vals["jsonld_key"]]
-                    # If there is an entry parse it by type and add to the attr:relationships dictionary.
+                    # If there is an entry parse it by type and add to the attr:relationships
+                    # dictionary.
                     if rel_entry:
                         parsed_rel_entry = self.parse_entry(
                             rel_entry=rel_entry,
                             id_jsonld_key=id_jsonld_key,
-                            dn_label_dict=dn_label_dict,
                             model_jsonld=model_jsonld,
                         )
-                        rel_csv_header = self.rel_dict[rel_key]["csv_header"]
+                        rel_csv_header = rel_vals["csv_header"]
                         if rel_key == "domainIncludes":
-                            # In the JSONLD the domain includes field contains the ids of attributes that the current attribute is the property/parent of.
+                            # In the JSONLD the domain includes field contains the ids of
+                            # attributes that the current attribute is the property/parent of.
                             # Because of this we need to handle these values differently.
-                            # We will get the values in the field (parsed_val), then add the current attribute as to the property key in the attr_rel_dictionary[p_attr_key].
+                            # We will get the values in the field (parsed_val), then add the
+                            # current attribute as to the property key in the
+                            # attr_rel_dictionary[p_attr_key].
                             for parsed_val in parsed_rel_entry:
                                 attr_in_dict = False
                                 # Get propert/parent key (displayName)
                                 p_attr_key = ""
-                                # Check if the parsed value is already a part of the attr_rel_dictionary
-                                for attr_dn, rels in attr_rel_dictionary.items():
+                                # Check if the parsed value is already a part of the
+                                # attr_rel_dictionary
+                                for attr_dn in attr_rel_dictionary:
                                     if parsed_val == attr_dn:
                                         p_attr_key = attr_dn
                                         attr_in_dict = True
-                                # If it is part of the dictionary update add current attribute as a property of the parsed value
-                                if attr_in_dict == True:
+                                # If it is part of the dictionary update add current
+                                # attribute as a property of the parsed value
+                                if attr_in_dict:
                                     if (
                                         not rel_csv_header
                                         in attr_rel_dictionary[p_attr_key][
@@ -420,8 +485,9 @@ def gather_jsonld_attributes_relationships(self, model_jsonld: List[dict]) -> Di
                                         attr_rel_dictionary[p_attr_key][
                                             "Relationships"
                                         ][rel_csv_header].extend([entry[dn_jsonld_key]])
-                                # If the parsed_val is not already recorded in the dictionary, add it
-                                elif attr_in_dict == False:
+                                # If the parsed_val is not already recorded in the dictionary,
+                                # add it
+                                elif not attr_in_dict:
                                     # Get the display name for the parsed value
                                     p_attr_key = self.convert_entry_to_dn_label(
                                         parsed_val, model_jsonld
@@ -447,12 +513,12 @@ def gather_jsonld_attributes_relationships(self, model_jsonld: List[dict]) -> Di
                 ):
                     # Retrieve entry value associated with the given relationship
                     rel_entry = entry[rel_vals["jsonld_key"]]
-                    # If there is an entry parset it by type and add to the attr:relationships dictionary.
+                    # If there is an entry parset it by type and add to the
+                    # attr:relationships dictionary.
                     if rel_entry:
                         parsed_rel_entry = self.parse_entry(
                             rel_entry=rel_entry,
                             id_jsonld_key=id_jsonld_key,
-                            dn_label_dict=dn_label_dict,
                             model_jsonld=model_jsonld,
                         )
                         # Add relationships for each attribute and relationship to the dictionary
@@ -476,7 +542,10 @@ def parse_jsonld_model(
         """
         # Log warning that JSONLD parsing is in beta mode.
         logger.warning(
-            "JSONLD parsing is in Beta Mode. Please inspect outputs carefully and report any errors."
+            (
+                "JSONLD parsing is in Beta Mode. "
+                "Please inspect outputs carefully and report any errors."
+            )
         )
         # Load the json_ld model to df
         json_load = load_json(path_to_data_model)
diff --git a/schematic/schemas/data_model_relationships.py b/schematic/schemas/data_model_relationships.py
index 9be7952dc..f83de5d8e 100644
--- a/schematic/schemas/data_model_relationships.py
+++ b/schematic/schemas/data_model_relationships.py
@@ -1,19 +1,22 @@
-from typing import Dict
+"""Data Model Relationships"""
+
 from schematic.utils.schema_utils import (
     get_label_from_display_name,
     get_attribute_display_name_from_label,
     convert_bool_to_str,
     parse_validation_rules,
 )
-from schematic.schemas.curie import uri2curie, curie2uri
 
 
 class DataModelRelationships:
+    """Data Model Relationships"""
+
     def __init__(self) -> None:
         self.relationships_dictionary = self.define_data_model_relationships()
 
-    def define_data_model_relationships(self) -> Dict:
-        """Define the relationships and their attributes so they can be accessed through other classes.
+    def define_data_model_relationships(self) -> dict:
+        """Define the relationships and their attributes so they can be accessed
+          through other classes.
         The key is how it the relationship will be referenced througout Schematic.
         Note: Though we could use other keys to determine which keys define nodes and edges,
             edge_rel is used as an explicit definition, for easier code readablity.
@@ -29,17 +32,20 @@ def define_data_model_relationships(self) -> Dict:
                       False, if is a value relationship
                       Include in all sub-dictionaries.
             required_header: True, if relationship header is required for the csv
-            jsonld_default: Defines default values to fill for JSONLD generation.
-                        Used during func DataModelJsonLD.clean_template(), to fill value with a default, if not supplied in the data model.
-            node_attr_dict: This is used to add information to nodes in the model. Only include for nodes not edges.
-                            set default values for this relationship
-                                key is the node relationship name, value is the default value.
-                                If want to set default as a function create a nested dictionary.
-                                    {'default': default_function,
-                                     'standard': alternative function to call if relationship is present for a node}
-                                    }
-                                If adding new functions to node_dict will
-                                    need to modify data_model_nodes.generate_node_dict in
+            jsonld_default:
+                Defines default values to fill for JSONLD generation.
+                Used during func DataModelJsonLD.clean_template(), to fill value with a default,
+                  if not supplied in the data model.
+            node_attr_dict: This is used to add information to nodes in the model.
+                Only include for nodes not edges.
+                set default values for this relationship
+                key is the node relationship name, value is the default value.
+                If want to set default as a function create a nested dictionary.
+                    {'default': default_function,
+                     'standard': alternative function to call if relationship is present for a node
+                    }
+                If adding new functions to node_dict will
+                    need to modify data_model_nodes.generate_node_dict in
             edge_dir: str, 'in'/'out' is the edge an in or out edge. Define for edge relationships
             jsonld_dir: str, 'in'/out is the direction in or out in the JSONLD.
 
@@ -187,24 +193,32 @@ def define_data_model_relationships(self) -> Dict:
         return map_data_model_relationships
 
     def define_required_csv_headers(self):
-        """Helper function to retrieve required CSV headers, alert if required header was not provided.
+        """
+        Helper function to retrieve required CSV headers, alert if required header was
+          not provided.
         Returns:
             required_headers: lst, Required CSV headers.
         """
         required_headers = []
-        for k, v in self.relationships_dictionary.items():
+        for key, value in self.relationships_dictionary.items():
             try:
-                if v["required_header"]:
-                    required_headers.append(v["csv_header"])
+                if value["required_header"]:
+                    required_headers.append(value["csv_header"])
             except KeyError:
                 print(
-                    f"Did not provide a 'required_header' key, value pair for the nested dictionary {k} : {key}"
+                    (
+                        "Did not provide a 'required_header' key, value pair for the "
+                        f"nested dictionary {key} : {value}"
+                    )
                 )
 
         return required_headers
 
-    def retreive_rel_headers_dict(self, edge: bool) -> Dict[str, str]:
-        """Helper function to retrieve CSV headers for edge and non-edge relationships defined by edge_type.
+    def retreive_rel_headers_dict(self, edge: bool) -> dict[str, str]:
+        """
+        Helper function to retrieve CSV headers for edge and non-edge relationships
+          defined by edge_type.
+
         Args:
             edge, bool: True if looking for edge relationships
         Returns:
@@ -215,7 +229,7 @@ def retreive_rel_headers_dict(self, edge: bool) -> Dict[str, str]:
             if "edge_rel" in rel_dict:
                 if rel_dict["edge_rel"] and edge:
                     rel_headers_dict.update({rel: rel_dict["csv_header"]})
-                elif rel_dict["edge_rel"] == False and edge == False:
+                elif not rel_dict["edge_rel"] and not edge:
                     rel_headers_dict.update({rel: rel_dict["csv_header"]})
             else:
                 raise ValueError(f"Did not provide a 'edge_rel' for relationship {rel}")
diff --git a/schematic/schemas/data_model_validator.py b/schematic/schemas/data_model_validator.py
index f0939999b..897c33738 100644
--- a/schematic/schemas/data_model_validator.py
+++ b/schematic/schemas/data_model_validator.py
@@ -1,8 +1,10 @@
+"""Data Model Validator"""
+
+import time
 import logging
+
 import multiprocessing
-import networkx as nx
-import time
-from typing import Any, Dict, Optional, Text, List, Tuple
+import networkx as nx  # type: ignore
 
 from schematic.schemas.data_model_relationships import DataModelRelationships
 
@@ -20,20 +22,25 @@ def __init__(
     ):
         """
         Args:
-                graph, nx.MultiDiGraph: Graph representation of the data model.
-        TODO: put blacklisted chars and reserved_names in some global space where they can be accessed centrally
+            graph (nx.MultiDiGraph): Graph representation of the data model.
+        TODO: put blacklisted chars and reserved_names in some global space where
+          they can be accessed centrally
         """
         self.graph = graph
-        self.DMR = DataModelRelationships()
+        self.dmr = DataModelRelationships()
         # Define blacklisted characters, taken from store.synapse
         self.blacklisted_chars = ["(", ")", ".", "-"]
         # Define reserved_names, taken from Documentation
         self.reserved_names = {"entityId"}
 
-    def run_checks(self) -> Tuple[list, list]:
+    def run_checks(self) -> tuple[list, list]:
         """Run all validation checks on the data model graph.
-        Returns, tuple(list, list): Returns a tuple of errors and warnings generated.
-        TODO: In future could design a way for groups to customize tests run for their groups, run additional tests, or move some to issuing only warnings, vice versa.
+
+        Returns:
+            tuple[list, list]:  Returns a tuple of errors and warnings generated.
+
+        TODO: In future could design a way for groups to customize tests run for their groups,
+           run additional tests, or move some to issuing only warnings, vice versa.
         """
         error_checks = [
             self.check_graph_has_required_node_fields(),
@@ -47,17 +54,17 @@ def run_checks(self) -> Tuple[list, list]:
         warnings = [warning for warning in warning_checks if warning]
         return errors, warnings
 
-    def check_graph_has_required_node_fields(self) -> List[str]:
+    def check_graph_has_required_node_fields(self) -> list[str]:
         """Checks that the graph has the required node fields for all nodes.
         Returns:
                 error, list: List of error messages for each missing field.
         """
         # Get all the fields that should be recorded per node
-        rel_dict = self.DMR.relationships_dictionary
+        rel_dict = self.dmr.relationships_dictionary
         node_fields = []
-        for k, v in rel_dict.items():
-            if "node_label" in v.keys():
-                node_fields.append(v["node_label"])
+        for value in rel_dict.values():
+            if "node_label" in value.keys():
+                node_fields.append(value["node_label"])
 
         error = []
         missing_fields = []
@@ -68,29 +75,43 @@ def check_graph_has_required_node_fields(self) -> List[str]:
             )
 
         if missing_fields:
-            for mf in missing_fields:
+            for missing_field in missing_fields:
                 error.append(
-                    f"For entry: {mf[0]}, the required field {mf[1]} is missing in the data model graph, please double check your model and generate the graph again."
+                    (
+                        f"For entry: {missing_field[0]}, the required field {missing_field[1]} "
+                        "is missing in the data model graph, please double check your model and "
+                        "generate the graph again."
+                    )
                 )
         return error
 
-    def run_cycles(self, graph):
+    def run_cycles(self):
+        """run_cycles"""
         cycles = nx.simple_cycles(self.graph)
-        if cycles:
+        if cycles:  # pylint:disable=using-constant-test
             for cycle in cycles:
-                logger.warning(
-                    f"Schematic requires models be a directed acyclic graph (DAG). Your graph is not a DAG, we found a loop between: {cycle[0]} and {cycle[1]}, please remove this loop from your model and submit again."
+                logger.warning(  # pylint:disable=logging-fstring-interpolation
+                    (
+                        f"Schematic requires models be a directed acyclic graph (DAG). Your graph "
+                        f"is not a DAG, we found a loop between: {cycle[0]} and {cycle[1]}, "
+                        "please remove this loop from your model and submit again."
+                    )
                 )
 
-    def check_is_dag(self) -> List[str]:
+    def check_is_dag(self) -> list[str]:
         """Check that generated graph is a directed acyclic graph
+
         Returns:
-                error, list: List of error messages if graph is not a DAG. List will include a message for each cycle found, if not there is a more generic message for the graph as a whole.
+            list[str]:
+              List of error messages if graph is not a DAG. List will include a message
+                for each cycle found, if not there is a more generic message for the
+                graph as a whole.
         """
         error = []
         if not nx.is_directed_acyclic_graph(self.graph):
             cycles = multiprocessing.Process(
-                target=self.run_cycles, name="Get Cycles", args=(self.graph,)
+                target=self.run_cycles,
+                name="Get Cycles",
             )
             cycles.start()
 
@@ -105,18 +126,25 @@ def check_is_dag(self) -> List[str]:
                 cycles.join()
 
             error.append(
-                f"Schematic requires models be a directed acyclic graph (DAG). Please inspect your model."
+                (
+                    "Schematic requires models be a directed acyclic graph (DAG). "
+                    "Please inspect your model."
+                )
             )
 
         return error
 
-    def check_blacklisted_characters(self) -> List[str]:
-        """We strip these characters in store, so not sure if it matter if we have them now, maybe add warning
+    def check_blacklisted_characters(self) -> list[str]:
+        """
+        We strip these characters in store, so not sure if it matter if we have them now,
+         maybe add warning
+
         Returns:
-                warning, list: list of warnings for each node in the graph, that has a Display name that contains blacklisted characters.
+            list[str]: list of warnings for each node in the graph, that has a Display
+              name that contains blacklisted characters.
         """
         warning = []
-        for node, node_dict in self.graph.nodes(data=True):
+        for _, node_dict in self.graph.nodes(data=True):
             if any(
                 bl_char in node_dict["displayName"]
                 for bl_char in self.blacklisted_chars
@@ -129,14 +157,19 @@ def check_blacklisted_characters(self) -> List[str]:
                 ]
                 blacklisted_characters_str = ",".join(blacklisted_characters_found)
                 warning.append(
-                    f"Node: {node_display_name} contains a blacklisted character(s): {blacklisted_characters_str}, they will be striped if used in Synapse annotations."
+                    (
+                        f"Node: {node_display_name} contains a blacklisted character(s): "
+                        f"{blacklisted_characters_str}, they will be striped if used in "
+                        "Synapse annotations."
+                    )
                 )
         return warning
 
-    def check_reserved_names(self) -> List[str]:
+    def check_reserved_names(self) -> list[str]:
         """Identify if any names nodes in the data model graph are the same as reserved name.
         Returns:
-                error, list: List of erros for every node in the graph whose name overlaps with the reserved names.
+            error, list: List of erros for every node in the graph whose name overlaps
+              with the reserved names.
         """
         error = []
         reserved_names_found = [
@@ -148,7 +181,10 @@ def check_reserved_names(self) -> List[str]:
         if reserved_names_found:
             for reserved_name, node_name in reserved_names_found:
                 error.append(
-                    f"Your data model entry name: {node_name} overlaps with the reserved name: {reserved_name}. Please change this name in your data model."
+                    (
+                        f"Your data model entry name: {node_name} overlaps with the reserved name: "
+                        f"{reserved_name}. Please change this name in your data model."
+                    )
                 )
         return error
 
@@ -170,7 +206,8 @@ def check_for_orphan_attributes(self):
 
     def check_namespace_similarity(self):
         """
-        Using AI, check if submitted attributes or valid values are similar to other ones, warn users.
+        Using AI, check if submitted attributes or valid values are similar to other ones,
+          warn users.
         Implement in future
         """
         warning = []
diff --git a/schematic/schemas/json_schema_validator.py b/schematic/schemas/json_schema_validator.py
index 301dcbfe3..7369c1f45 100644
--- a/schematic/schemas/json_schema_validator.py
+++ b/schematic/schemas/json_schema_validator.py
@@ -1,17 +1,14 @@
+"""Json Schema Validator"""
+
 import os
 from jsonschema import validate
 
-from schematic.utils.io_utils import load_schemaorg, load_json, load_default
+from schematic.utils.io_utils import load_schemaorg, load_json
 from schematic.utils.general import str2list, dict2list, find_duplicates
 from schematic.utils.curie_utils import (
     expand_curies_in_schema,
     extract_name_from_uri_or_curie,
 )
-from schematic.utils.validate_utils import (
-    validate_class_schema,
-    validate_property_schema,
-    validate_schema,
-)
 
 
 class SchemaValidator:
@@ -34,15 +31,18 @@ class SchemaValidator:
           word for a class;
         > the value of "rdfs:subClassOf" should be present in the schema or in
           the core vocabulary
-        > sms:displayName ideally should contain capitalized words separated by space, but that's not enforced by validation
+        > sms:displayName ideally should contain capitalized words separated by space,
+          but that's not enforced by validation
       > Property specific
         > rdfs:label field should be cammelCase
         > the value of "schema:domainIncludes" should be present in the schema
           or in the core vocabulary
         > the value of "schema:rangeIncludes" should be present in the schema
           or in the core vocabulary
-        > sms:displayName ideally should contain capitalized words separated by space, but that's not enforced by validation
-        TODO: add dependencies and component dependencies to class structure documentation; as well as value range and required property
+        > sms:displayName ideally should contain capitalized words separated
+          by space, but that's not enforced by validation
+        TODO: add dependencies and component dependencies to class
+          structure documentation; as well as value range and required property
 
     """
 
@@ -85,18 +85,17 @@ def validate_subclassof_field(self, subclassof_value):
         for record in subclassof_value:
             assert record["@id"] in self.all_classes
 
-    def validate_domainIncludes_field(self, domainincludes_value):
+    def validate_domain_includes_field(self, domainincludes_value):
         """Check if the value of "domainincludes" is included in the schema
         file
         """
         domainincludes_value = dict2list(domainincludes_value)
         for record in domainincludes_value:
-            assert record["@id"] in self.all_classes, (
-                "value of domainincludes not recorded in schema: %r"
-                % domainincludes_value
-            )
+            assert (
+                record["@id"] in self.all_classes
+            ), f"value of domainincludes not recorded in schema: {domainincludes_value}"
 
-    def validate_rangeIncludes_field(self, rangeincludes_value):
+    def validate_range_includes_field(self, rangeincludes_value):
         """Check if the value of "rangeincludes" is included in the schema
         file
         """
@@ -107,7 +106,7 @@ def validate_rangeIncludes_field(self, rangeincludes_value):
     def check_whether_atid_and_label_match(self, record):
         """Check if @id field matches with the "rdfs:label" field"""
         _id = extract_name_from_uri_or_curie(record["@id"])
-        assert _id == record["rdfs:label"], "id and label not match: %r" % record
+        assert _id == record["rdfs:label"], f"id and label not match: {record}"
 
     def check_duplicate_labels(self):
         """Check for duplication in the schema"""
@@ -116,10 +115,8 @@ def check_duplicate_labels(self):
             for _record in self.extension_schema["schema"]["@graph"]
         ]
         duplicates = find_duplicates(labels)
-        try:
-            assert len(duplicates) == 0
-        except:
-            raise Exception("Duplicates detected in graph: ", duplicates)
+        if len(duplicates) == 0:
+            raise ValueError("Duplicates detected in graph: ", duplicates)
 
     def validate_schema(self, schema):
         """Validate schema against SchemaORG standard"""
@@ -142,6 +139,7 @@ def validate_class_schema(self, schema):
         return validate(schema, json_schema)
 
     def validate_full_schema(self):
+        """validate full schema"""
         self.check_duplicate_labels()
         for record in self.extension_schema["schema"]["@graph"]:
             self.check_whether_atid_and_label_match(record)
@@ -151,10 +149,10 @@ def validate_full_schema(self):
             elif record["@type"] == "rdf:Property":
                 self.validate_property_schema(record)
                 self.validate_property_label(record["@id"])
-                self.validate_domainIncludes_field(
+                self.validate_domain_includes_field(
                     record["http://schema.org/domainIncludes"]
                 )
                 if "http://schema.org/rangeIncludes" in record:
-                    self.validate_rangeIncludes_field(
+                    self.validate_range_includes_field(
                         record["http://schema.org/rangeIncludes"]
                     )
diff --git a/schematic/store/synapse.py b/schematic/store/synapse.py
index 1239ae52a..a15137ae8 100644
--- a/schematic/store/synapse.py
+++ b/schematic/store/synapse.py
@@ -1,9 +1,8 @@
+"""Synapse storage class"""
+
 import atexit
-from collections import OrderedDict
 from copy import deepcopy
-from datetime import datetime, timedelta
 from dataclasses import dataclass
-import json
 import logging
 import numpy as np
 import pandas as pd
@@ -26,7 +25,6 @@
 # allows specifying explicit variable types
 from typing import Dict, List, Tuple, Sequence, Union, Optional
 
-
 from synapseclient import (
     Synapse,
     File,
@@ -40,16 +38,13 @@
 )
 from synapseclient.entity import File
 from synapseclient.table import CsvFileTable, build_table, Schema
-from synapseclient.annotations import from_synapse_annotations
 from synapseclient.core.exceptions import (
     SynapseHTTPError,
     SynapseAuthenticationError,
     SynapseUnmetAccessRestrictions,
+    SynapseHTTPError,
 )
 import synapseutils
-from synapseutils.copy_functions import changeFileMetaData
-
-import uuid
 
 from schematic_db.rdb.synapse_database import SynapseDatabase
 
@@ -63,18 +58,15 @@
 from schematic.utils.general import (
     entity_type_mapping,
     get_dir_size,
-    convert_gb_to_bytes,
     create_temp_folder,
     check_synapse_cache_size,
     clear_synapse_cache,
-    profile,
-    calculate_datetime,
 )
 
 from schematic.utils.schema_utils import get_class_label_from_display_name
 
 from schematic.store.base import BaseStorage
-from schematic.exceptions import MissingConfigValueError, AccessCredentialsError
+from schematic.exceptions import AccessCredentialsError
 from schematic.configuration.configuration import CONFIG
 
 logger = logging.getLogger("Synapse storage")
@@ -202,46 +194,52 @@ def __init__(
         token: Optional[str] = None,  # optional parameter retrieved from browser cookie
         access_token: Optional[str] = None,
         project_scope: Optional[list] = None,
+        synapse_cache_path: Optional[str] = None,
     ) -> None:
         """Initializes a SynapseStorage object.
+
         Args:
-            syn: an object of type synapseclient.
-            token: optional token parameter (typically a 'str') as found in browser cookie upon login to synapse.
-            access_token: optional access token (personal or oauth)
-            TODO: move away from specific project setup and work with an interface that Synapse specifies (e.g. based on schemas).
-        Exceptions:
-            KeyError: when the 'storage' config object is missing values for essential keys.
-            AttributeError: when the 'storageFileview' attribute (of class SynapseStorage) does not have a value associated with it.
-            synapseclient.core.exceptions.SynapseHTTPError: check if the current user has permission to access the Synapse entity.
-            ValueError: when Admin fileview cannot be found (describe further).
-        Typical usage example:
-            syn_store = SynapseStorage()
-        """
-        # TODO: turn root_synapse_cache to a parameter in init
-        self.syn = self.login(token, access_token)
+            token (Optional[str], optional):
+              Optional token parameter as found in browser cookie upon login to synapse.
+              Defaults to None.
+            access_token (Optional[list], optional):
+              Optional access token (personal or oauth).
+              Defaults to None.
+            project_scope (Optional[list], optional): Defaults to None.
+            synapse_cache_path (Optional[str], optional):
+              Location of synapse cache.
+              Defaults to None.
+        """
+        self.syn = self.login(synapse_cache_path, token, access_token)
         self.project_scope = project_scope
         self.storageFileview = CONFIG.synapse_master_fileview_id
         self.manifest = CONFIG.synapse_manifest_basename
-        self.root_synapse_cache = "/root/.synapseCache"
+        self.root_synapse_cache = self.syn.cache.cache_root_dir
         self._query_fileview()
 
-    def _purge_synapse_cache(self, maximum_storage_allowed_cache_gb=1):
+    def _purge_synapse_cache(
+        self, maximum_storage_allowed_cache_gb: int = 1, minute_buffer: int = 15
+    ) -> None:
         """
         Purge synapse cache if it exceeds a certain size. Default to 1GB.
         Args:
-            maximum_storage_allowed_cache_gb: the maximum storage allowed before purging cache. Default is 1 GB.
+            maximum_storage_allowed_cache_gb (int): the maximum storage allowed
+              before purging cache. Default is 1 GB.
+            minute_buffer (int): All files created this amount of time or older will be deleted
         """
         # try clearing the cache
         # scan a directory and check size of files
         if os.path.exists(self.root_synapse_cache):
-            maximum_storage_allowed_cache_bytes = convert_gb_to_bytes(
-                maximum_storage_allowed_cache_gb
+            maximum_storage_allowed_cache_bytes = maximum_storage_allowed_cache_gb * (
+                1024**3
             )
             nbytes = get_dir_size(self.root_synapse_cache)
             dir_size_bytes = check_synapse_cache_size(directory=self.root_synapse_cache)
             # if 1 GB has already been taken, purge cache before 15 min
             if dir_size_bytes >= maximum_storage_allowed_cache_bytes:
-                num_of_deleted_files = clear_synapse_cache(self.syn.cache, minutes=15)
+                num_of_deleted_files = clear_synapse_cache(
+                    self.syn.cache, minutes=minute_buffer
+                )
                 logger.info(
                     f"{num_of_deleted_files}  files have been deleted from {self.root_synapse_cache}"
                 )
@@ -268,30 +266,52 @@ def _query_fileview(self):
             raise AccessCredentialsError(self.storageFileview)
 
     @staticmethod
-    def login(token=None, access_token=None):
+    def login(
+        synapse_cache_path: Optional[str] = None,
+        token: Optional[str] = None,
+        access_token: Optional[str] = None,
+    ) -> synapseclient.Synapse:
+        """Login to Synapse
+
+        Args:
+            token (Optional[str], optional): A Synapse token. Defaults to None.
+            access_token (Optional[str], optional): A synapse access token. Defaults to None.
+            synapse_cache_path (Optional[str]): location of synapse cache
+
+        Raises:
+            ValueError: If unable to login with token
+            ValueError: If unable to loging with access token
+
+        Returns:
+            synapseclient.Synapse: A Synapse object that is logged in
+        """
         # If no token is provided, try retrieving access token from environment
         if not token and not access_token:
             access_token = os.getenv("SYNAPSE_ACCESS_TOKEN")
 
         # login using a token
         if token:
-            syn = synapseclient.Synapse()
-
+            syn = synapseclient.Synapse(cache_root_dir=synapse_cache_path)
             try:
                 syn.login(sessionToken=token, silent=True)
-            except synapseclient.core.exceptions.SynapseHTTPError:
-                raise ValueError("Please make sure you are logged into synapse.org.")
+            except SynapseHTTPError as exc:
+                raise ValueError(
+                    "Please make sure you are logged into synapse.org."
+                ) from exc
         elif access_token:
             try:
-                syn = synapseclient.Synapse()
+                syn = synapseclient.Synapse(cache_root_dir=synapse_cache_path)
                 syn.default_headers["Authorization"] = f"Bearer {access_token}"
-            except synapseclient.core.exceptions.SynapseHTTPError:
+            except SynapseHTTPError as exc:
                 raise ValueError(
                     "No access to resources. Please make sure that your token is correct"
-                )
+                ) from exc
         else:
             # login using synapse credentials provided by user in .synapseConfig (default) file
-            syn = synapseclient.Synapse(configPath=CONFIG.synapse_configuration_path)
+            syn = synapseclient.Synapse(
+                configPath=CONFIG.synapse_configuration_path,
+                cache_root_dir=synapse_cache_path,
+            )
             syn.login(silent=True)
         return syn
 
@@ -574,7 +594,9 @@ def getDataTypeFromManifest(self, manifestId: str):
 
         # load manifest dataframe
         manifest = load_df(
-            manifest_filepath, preserve_raw_input=False, data_model=False
+            manifest_filepath,
+            preserve_raw_input=False,
+            data_model=False,
         )
 
         # convert the dataFrame to use best possible dtypes.
@@ -1303,11 +1325,11 @@ def upload_manifest_file(
             parent=datasetId,
             name=file_name_new,
         )
-
         manifest_synapse_file_id = self.syn.store(
             manifestSynapseFile, isRestricted=restrict_manifest
         ).id
-        changeFileMetaData(
+
+        synapseutils.copy_functions.changeFileMetaData(
             syn=self.syn, entity=manifest_synapse_file_id, downloadAs=file_name_new
         )
 
@@ -1508,7 +1530,10 @@ def _read_manifest(self, metadataManifestPath: str) -> pd.DataFrame:
                 "dtype": "string",
             }
             manifest = load_df(
-                metadataManifestPath, preserve_raw_input=False, **load_args
+                metadataManifestPath,
+                preserve_raw_input=False,
+                allow_na_values=False,
+                **load_args,
             )
         except FileNotFoundError as err:
             raise FileNotFoundError(
@@ -1650,11 +1675,11 @@ def add_annotations_to_entities_files(
                 name as upper camelcase, and strip blacklisted characters, display_label will strip blacklisted characters including spaces, to retain
                 display label formatting while ensuring the label is formatted properly for Synapse annotations.
         Returns:
-            manifest (pd.DataFrame): modified to add entitiyId as appropriate.
+            manifest (pd.DataFrame): modified to add entitiyId as appropriate
 
         """
 
-        # Expected behavior is to annotate files if `Filename` is present regardless of `-mrt` setting
+        # Expected behavior is to annotate files if `Filename` is present and if file_annotations_upload is set to True regardless of `-mrt` setting
         if "filename" in [col.lower() for col in manifest.columns]:
             # get current list of files and store as dataframe
             dataset_files = self.getFilesInStorageDataset(datasetId)
@@ -1706,6 +1731,7 @@ def upload_manifest_as_table(
         table_manipulation: str,
         table_column_names: str,
         annotation_keys: str,
+        file_annotations_upload: bool = True,
     ):
         """Upload manifest to Synapse as a table and csv.
         Args:
@@ -1725,6 +1751,7 @@ def upload_manifest_as_table(
             annotation_keys: (str) display_label/class_label (default), Sets labeling syle for annotation keys. class_label will format the display
                 name as upper camelcase, and strip blacklisted characters, display_label will strip blacklisted characters including spaces, to retain
                 display label formatting while ensuring the label is formatted properly for Synapse annotations.
+            file_annotations_upload (bool): Default to True. If false, do not add annotations to files.
         Return:
             manifest_synapse_file_id: SynID of manifest csv uploaded to synapse.
         """
@@ -1739,15 +1766,16 @@ def upload_manifest_as_table(
             table_column_names=table_column_names,
         )
 
-        manifest = self.add_annotations_to_entities_files(
-            dmge,
-            manifest,
-            manifest_record_type,
-            datasetId,
-            hideBlanks,
-            manifest_synapse_table_id,
-            annotation_keys,
-        )
+        if file_annotations_upload:
+            manifest = self.add_annotations_to_entities_files(
+                dmge,
+                manifest,
+                manifest_record_type,
+                datasetId,
+                hideBlanks,
+                manifest_synapse_table_id,
+                annotation_keys,
+            )
         # Load manifest to synapse as a CSV File
         manifest_synapse_file_id = self.upload_manifest_file(
             manifest,
@@ -1793,6 +1821,7 @@ def upload_manifest_as_csv(
         hideBlanks,
         component_name,
         annotation_keys: str,
+        file_annotations_upload: bool = True,
     ):
         """Upload manifest to Synapse as a csv only.
         Args:
@@ -1806,17 +1835,19 @@ def upload_manifest_as_csv(
             annotation_keys: (str) display_label/class_label (default), Sets labeling syle for annotation keys. class_label will format the display
                 name as upper camelcase, and strip blacklisted characters, display_label will strip blacklisted characters including spaces, to retain
                 display label formatting while ensuring the label is formatted properly for Synapse annotations.
+            file_annotations_upload (bool): Default to True. If false, do not add annotations to files.
         Return:
             manifest_synapse_file_id (str): SynID of manifest csv uploaded to synapse.
         """
-        manifest = self.add_annotations_to_entities_files(
-            dmge,
-            manifest,
-            manifest_record_type,
-            datasetId,
-            hideBlanks,
-            annotation_keys=annotation_keys,
-        )
+        if file_annotations_upload:
+            manifest = self.add_annotations_to_entities_files(
+                dmge,
+                manifest,
+                manifest_record_type,
+                datasetId,
+                hideBlanks,
+                annotation_keys=annotation_keys,
+            )
 
         # Load manifest to synapse as a CSV File
         manifest_synapse_file_id = self.upload_manifest_file(
@@ -1851,6 +1882,7 @@ def upload_manifest_combo(
         table_manipulation,
         table_column_names: str,
         annotation_keys: str,
+        file_annotations_upload: bool = True,
     ):
         """Upload manifest to Synapse as a table and CSV with entities.
         Args:
@@ -1870,6 +1902,7 @@ def upload_manifest_combo(
             annotation_keys: (str) display_label/class_label (default), Sets labeling syle for annotation keys. class_label will format the display
                 name as upper camelcase, and strip blacklisted characters, display_label will strip blacklisted characters including spaces, to retain
                 display label formatting while ensuring the label is formatted properly for Synapse annotations.
+            file_annotations_upload (bool): Default to True. If false, do not add annotations to files.
         Return:
             manifest_synapse_file_id (str): SynID of manifest csv uploaded to synapse.
         """
@@ -1883,15 +1916,16 @@ def upload_manifest_combo(
             table_column_names=table_column_names,
         )
 
-        manifest = self.add_annotations_to_entities_files(
-            dmge,
-            manifest,
-            manifest_record_type,
-            datasetId,
-            hideBlanks,
-            manifest_synapse_table_id,
-            annotation_keys=annotation_keys,
-        )
+        if file_annotations_upload:
+            manifest = self.add_annotations_to_entities_files(
+                dmge,
+                manifest,
+                manifest_record_type,
+                datasetId,
+                hideBlanks,
+                manifest_synapse_table_id,
+                annotation_keys=annotation_keys,
+            )
 
         # Load manifest to synapse as a CSV File
         manifest_synapse_file_id = self.upload_manifest_file(
@@ -1934,6 +1968,7 @@ def associateMetadataWithFiles(
         table_manipulation: str = "replace",
         table_column_names: str = "class_label",
         annotation_keys: str = "class_label",
+        file_annotations_upload: bool = True,
     ) -> str:
         """Associate metadata with files in a storage dataset already on Synapse.
         Upload metadataManifest in the storage dataset folder on Synapse as well. Return synapseId of the uploaded manifest file.
@@ -1973,7 +2008,6 @@ def associateMetadataWithFiles(
         table_name, component_name = self._generate_table_name(manifest)
 
         # Upload manifest to synapse based on user input (manifest_record_type)
-
         if manifest_record_type == "file_only":
             manifest_synapse_file_id = self.upload_manifest_as_csv(
                 dmge,
@@ -1985,6 +2019,7 @@ def associateMetadataWithFiles(
                 manifest_record_type=manifest_record_type,
                 component_name=component_name,
                 annotation_keys=annotation_keys,
+                file_annotations_upload=file_annotations_upload,
             )
         elif manifest_record_type == "table_and_file":
             manifest_synapse_file_id = self.upload_manifest_as_table(
@@ -2000,6 +2035,7 @@ def associateMetadataWithFiles(
                 table_manipulation=table_manipulation,
                 table_column_names=table_column_names,
                 annotation_keys=annotation_keys,
+                file_annotations_upload=file_annotations_upload,
             )
         elif manifest_record_type == "file_and_entities":
             manifest_synapse_file_id = self.upload_manifest_as_csv(
@@ -2012,6 +2048,7 @@ def associateMetadataWithFiles(
                 manifest_record_type=manifest_record_type,
                 component_name=component_name,
                 annotation_keys=annotation_keys,
+                file_annotations_upload=file_annotations_upload,
             )
         elif manifest_record_type == "table_file_and_entities":
             manifest_synapse_file_id = self.upload_manifest_combo(
@@ -2027,6 +2064,7 @@ def associateMetadataWithFiles(
                 table_manipulation=table_manipulation,
                 table_column_names=table_column_names,
                 annotation_keys=annotation_keys,
+                file_annotations_upload=file_annotations_upload,
             )
         else:
             raise ValueError("Please enter a valid manifest_record_type.")
diff --git a/schematic/utils/cli_utils.py b/schematic/utils/cli_utils.py
index 52debd51c..07c97af90 100644
--- a/schematic/utils/cli_utils.py
+++ b/schematic/utils/cli_utils.py
@@ -32,7 +32,7 @@ def extract(dictionary: Any, key: Any) -> Union[Any, None]:
             return None
         return dictionary.get(key)
 
-    return reduce(extract, keys, dictionary)
+    return reduce(extract, keys, dictionary)  # type: ignore
 
 
 def log_value_from_config(arg_name: str, config_value: Any) -> None:
diff --git a/schematic/utils/curie_utils.py b/schematic/utils/curie_utils.py
index 9345f2b87..326447fd0 100644
--- a/schematic/utils/curie_utils.py
+++ b/schematic/utils/curie_utils.py
@@ -1,7 +1,12 @@
 """Curie utils"""
 
 import logging
+from typing import Any, Union
 
+Context = dict[str, str]
+Record = dict[str, Union[str, list, dict, None]]
+Graph = list[Record]
+Schema = dict[str, Any]
 
 logger = logging.getLogger(__name__)
 
@@ -15,7 +20,7 @@ def extract_name_from_uri_or_curie(item: str) -> str:
     raise ValueError("Error extracting name from URI or Curie.")
 
 
-def expand_curie_to_uri(curie: str, context_info: dict[str, str]) -> str:
+def expand_curie_to_uri(curie: str, context_info: Context) -> str:
     """Expand curie to uri based on the context given
 
     parmas
@@ -36,27 +41,27 @@ def expand_curie_to_uri(curie: str, context_info: dict[str, str]) -> str:
     return curie
 
 
-def expand_curies_in_schema(schema):
+def expand_curies_in_schema(schema: Schema) -> Schema:
     """Expand all curies in a SchemaOrg JSON-LD file into URI"""
-    context = schema["@context"]
-    graph = schema["@graph"]
+    context: Context = schema["@context"]
+    graph: Graph = schema["@graph"]
     new_schema = {"@context": context, "@graph": [], "@id": schema["@id"]}
     for record in graph:
-        new_record = {}
+        new_record: Record = {}
         for key, value in record.items():
             if isinstance(value, str):
                 new_record[expand_curie_to_uri(key, context)] = expand_curie_to_uri(
                     value, context
                 )
             elif isinstance(value, list):
+                uri = expand_curie_to_uri(key, context)
                 if isinstance(value[0], dict):
-                    new_record[expand_curie_to_uri(key, context)] = []
+                    lst: list[dict[str, str]] = []
+                    new_record[uri] = lst
                     for _item in value:
-                        new_record[expand_curie_to_uri(key, context)].append(
-                            {"@id": expand_curie_to_uri(_item["@id"], context)}
-                        )
+                        lst.append({"@id": expand_curie_to_uri(_item["@id"], context)})
                 else:
-                    new_record[expand_curie_to_uri(key, context)] = [
+                    new_record[uri] = [
                         expand_curie_to_uri(_item, context) for _item in value
                     ]
             elif isinstance(value, dict) and "@id" in value:
@@ -69,7 +74,7 @@ def expand_curies_in_schema(schema):
     return new_schema
 
 
-def uri2label(uri, schema):
+def uri2label(uri: str, schema: Schema) -> list:
     """Given a URI, return the label"""
     return [
         record["rdfs:label"] for record in schema["@graph"] if record["@id"] == uri
diff --git a/schematic/utils/df_utils.py b/schematic/utils/df_utils.py
index 1bba83c7a..b25e7db82 100644
--- a/schematic/utils/df_utils.py
+++ b/schematic/utils/df_utils.py
@@ -5,7 +5,7 @@
 import logging
 from copy import deepcopy
 from time import perf_counter
-from typing import Union
+from typing import Union, Any, Optional
 from datetime import datetime
 import dateparser as dp
 import pandas as pd
@@ -19,7 +19,8 @@ def load_df(
     file_path: str,
     preserve_raw_input: bool = True,
     data_model: bool = False,
-    **load_args: dict,
+    allow_na_values: bool = False,
+    **load_args: Any,
 ) -> pd.DataFrame:
     """
     Universal function to load CSVs and return DataFrames
@@ -30,6 +31,7 @@ def load_df(
         file_path (str): path of csv to open
         preserve_raw_input (bool, optional): If false, convert cell datatypes to an inferred type
         data_model (bool, optional): bool, indicates if importing a data model
+        allow_na_values (bool, optional): If true, allow pd.NA values in the dataframe
         **load_args(dict): dict of key value pairs to be passed to the pd.read_csv function
 
     Raises:
@@ -56,15 +58,12 @@ def load_df(
 
     # only trim if not data model csv
     if not data_model:
-        org_df = trim_commas_df(org_df)
+        org_df = trim_commas_df(org_df, allow_na_values=allow_na_values)
 
     if preserve_raw_input:
         logger.debug(f"Load Elapsed time {perf_counter()-t_load_df}")
         return org_df
 
-    is_null = org_df.isnull()
-    org_df = org_df.astype(str).mask(is_null, "")
-
     ints, is_int = find_and_convert_ints(org_df)
 
     float_df = convert_floats(org_df)
@@ -92,18 +91,21 @@ def find_and_convert_ints(dataframe: pd.DataFrame) -> tuple[pd.DataFrame, pd.Dat
     if (
         dataframe.size < large_manifest_cutoff_size
     ):  # If small manifest, iterate as normal for improved performance
-        ints = dataframe.map(
+        ints = dataframe.map(  # type:ignore
             lambda cell: convert_ints(cell), na_action="ignore"
         ).fillna(False)
 
     else:  # parallelize iterations for large manifests
         pandarallel.initialize(verbose=1)
-        ints = dataframe.parallel_applymap(
+        ints = dataframe.parallel_applymap(  # type:ignore
             lambda cell: convert_ints(cell), na_action="ignore"
         ).fillna(False)
 
     # Identify cells converted to integers
-    is_int = ints.map(pd.api.types.is_integer)
+    is_int = ints.map(pd.api.types.is_integer)  # type:ignore
+
+    assert isinstance(ints, pd.DataFrame)
+    assert isinstance(is_int, pd.DataFrame)
 
     return ints, is_int
 
@@ -116,7 +118,9 @@ def convert_ints(string: str) -> Union[np.int64, bool]:
     Returns:
         string converted to type int if possible, otherwise False
     """
-    return np.int64(string) if str.isdigit(string) else False
+    if isinstance(string, str) and str.isdigit(string):
+        return np.int64(string)
+    return False
 
 
 def convert_floats(dataframe: pd.DataFrame) -> pd.DataFrame:
@@ -234,11 +238,15 @@ def update_df(
     return input_df_idx
 
 
-def trim_commas_df(dataframe: pd.DataFrame) -> pd.DataFrame:
+def trim_commas_df(
+    dataframe: pd.DataFrame,
+    allow_na_values: Optional[bool] = False,
+) -> pd.DataFrame:
     """Removes empty (trailing) columns and empty rows from pandas dataframe (manifest data).
 
     Args:
         dataframe: pandas dataframe with data from manifest file.
+        allow_na_values (bool, optional): If true, allow pd.NA values in the dataframe
 
     Returns:
         df: cleaned-up pandas dataframe.
@@ -249,8 +257,9 @@ def trim_commas_df(dataframe: pd.DataFrame) -> pd.DataFrame:
     # remove all completely empty rows
     dataframe = dataframe.dropna(how="all", axis=0)
 
-    # Fill in nan cells with empty strings
-    dataframe.fillna("", inplace=True)
+    if allow_na_values is False:
+        # Fill in nan cells with empty strings
+        dataframe.fillna("", inplace=True)
     return dataframe
 
 
diff --git a/schematic/utils/general.py b/schematic/utils/general.py
index 246d0bc9e..974805043 100644
--- a/schematic/utils/general.py
+++ b/schematic/utils/general.py
@@ -66,14 +66,17 @@ def str2list(item: Any) -> Optional[list]:
     return None
 
 
-def unlist(seq: Sequence) -> Any:
+X = TypeVar("X")
+
+
+def unlist(seq: Sequence[X]) -> Union[Sequence[X], X]:
     """Returns the first item of a sequence
 
     Args:
-        seq (Sequence): Any sequence
+        seq (Sequence[X]): A Sequence of any type
 
     Returns:
-        Any:
+        Union[Sequence[X], X]:
           if sequence is length one, return the first item
           otherwise return the sequence
     """
@@ -126,14 +129,14 @@ def calculate_datetime(
 
 def check_synapse_cache_size(
     directory: str = "/root/.synapseCache",
-) -> Union[float, int]:
+) -> float:
     """use du --sh command to calculate size of .synapseCache.
 
     Args:
         directory (str, optional): .synapseCache directory. Defaults to '/root/.synapseCache'
 
     Returns:
-        float or integer: returns size of .synapsecache directory in bytes
+        float: returns size of .synapsecache directory in bytes
     """
     # Note: this command might fail on windows user.
     # But since this command is primarily for running on AWS, it is fine.
@@ -152,11 +155,11 @@ def check_synapse_cache_size(
         byte_size = size_in_mb * 1000000
     elif "G" in size:
         size_in_gb = float(size.rstrip("G"))
-        byte_size = convert_gb_to_bytes(size_in_gb)
+        byte_size = size_in_gb * (1024**3)
     elif "B" in size:
         byte_size = float(size.rstrip("B"))
     else:
-        logger.error("Cannot recongize the file size unit")
+        logger.error("Cannot recognize the file size unit")
     return byte_size
 
 
@@ -177,15 +180,6 @@ def clear_synapse_cache(synapse_cache: cache.Cache, minutes: int) -> int:
     return num_of_deleted_files
 
 
-def convert_gb_to_bytes(g_bytes: int) -> int:
-    """convert gb to bytes
-    Args:
-        g_bytes: number of gb
-    return: total number of bytes
-    """
-    return g_bytes * 1024 * 1024 * 1024
-
-
 def entity_type_mapping(syn: Synapse, entity_id: str) -> str:
     """Return the entity type of manifest
 
@@ -275,9 +269,9 @@ def profile(
         Callable: Profile of the decorated function
     """
 
-    def inner(func):
+    def inner(func: Callable) -> Callable:
         @wraps(func)
-        def wrapper(*args, **kwargs):
+        def wrapper(*args: Any, **kwargs: Any) -> Callable:
             _output_file = output_file or func.__name__ + ".prof"
             profiler = Profile()
             profiler.enable()
@@ -299,7 +293,7 @@ def wrapper(*args, **kwargs):
                         p_stats.sort_stats(*sort_by)
                     else:
                         p_stats.sort_stats(sort_by)
-                    p_stats.print_stats(lines_to_print)
+                    p_stats.print_stats(lines_to_print)  # type: ignore
             return retval
 
         return wrapper
diff --git a/schematic/utils/google_api_utils.py b/schematic/utils/google_api_utils.py
index 4cc743e6b..9a5d870ca 100644
--- a/schematic/utils/google_api_utils.py
+++ b/schematic/utils/google_api_utils.py
@@ -5,10 +5,10 @@
 import os
 import logging
 import json
-from typing import Any, Union
+from typing import Any, Union, no_type_check, TypedDict
 
 import pandas as pd
-from googleapiclient.discovery import build  # type: ignore
+from googleapiclient.discovery import build, Resource  # type: ignore
 from google.oauth2 import service_account  # type: ignore
 from schematic.configuration.configuration import CONFIG
 from schematic.store.synapse import SynapseStorage
@@ -23,11 +23,18 @@
 ]
 
 
-def build_service_account_creds() -> dict[str, Any]:
+class GoogleServiceAcountCreds(TypedDict):
+    "Service account credentials for Google sheets"
+    sheet_service: Resource
+    drive_service: Resource
+    creds: service_account.Credentials
+
+
+def build_service_account_creds() -> GoogleServiceAcountCreds:
     """Build Google service account credentials
 
     Returns:
-        dict[str, Any]: The credentials
+        GoogleServiceAcountCreds: The credentials
     """
     if "SERVICE_ACCOUNT_CREDS" in os.environ:
         dict_creds = json.loads(os.environ["SERVICE_ACCOUNT_CREDS"])
@@ -48,15 +55,16 @@ def build_service_account_creds() -> dict[str, Any]:
         )
 
     # get a Google Sheet API service
-    sheet_service = build("sheets", "v4", credentials=credentials)
+    sheet_service: Resource = build("sheets", "v4", credentials=credentials)
     # get a Google Drive API service
-    drive_service = build("drive", "v3", credentials=credentials)
+    drive_service: Resource = build("drive", "v3", credentials=credentials)
 
-    return {
+    creds: GoogleServiceAcountCreds = {
         "sheet_service": sheet_service,
         "drive_service": drive_service,
         "creds": credentials,
     }
+    return creds
 
 
 def download_creds_file() -> None:
@@ -92,7 +100,8 @@ def download_creds_file() -> None:
         )
 
 
-def execute_google_api_requests(service: Any, requests_body: Any, **kwargs) -> Any:
+@no_type_check
+def execute_google_api_requests(service, requests_body, **kwargs) -> Any:
     """
     Execute google API requests batch; attempt to execute in parallel.
 
diff --git a/schematic/utils/schema_utils.py b/schematic/utils/schema_utils.py
index 6caba81c6..a4565d420 100644
--- a/schematic/utils/schema_utils.py
+++ b/schematic/utils/schema_utils.py
@@ -1,22 +1,34 @@
-import inflection
+"""Schema utils"""
+
+# pylint: disable=logging-fstring-interpolation
+
 import json
 import logging
-import networkx as nx
-import re
 import string
-from typing import List, Literal, Dict, Tuple, Union
+from typing import Literal, Union, Optional
+
+import inflection
 
 
 logger = logging.getLogger(__name__)
 
 DisplayLabelType = Literal["class_label", "display_label"]
+EntryType = Literal["class", "property"]
 BLACKLISTED_CHARS = ["(", ")", ".", " ", "-"]
 COMPONENT_NAME_DELIMITER = "#"
 COMPONENT_RULES_DELIMITER = "^^"
 RULE_DELIMITER = "::"
 
 
-def attr_dict_template(key_name: str) -> Dict[str, dict[str, dict]]:
+def attr_dict_template(key_name: str) -> dict[str, dict[str, dict]]:
+    """Create a single empty attribute_dict template.
+
+    Args:
+        key_name (str): Attribute/node to use as the key in the dict.
+
+    Returns:
+        dict[str, dict[str, dict]]: template single empty attribute_relationships dictionary
+    """
     return {key_name: {"Relationships": {}}}
 
 
@@ -26,7 +38,8 @@ def get_property_label_from_display_name(
     """Convert a given display name string into a proper property label string
     Args:
         display_name, str: node display name
-        strict_camel_case, bool: Default, False; defines whether or not to use strict camel case or not for conversion.
+        strict_camel_case, bool: Default, False; defines whether or not to use
+          strict camel case or not for conversion.
     Returns:
         label, str: property label of display name
     """
@@ -51,7 +64,8 @@ def get_class_label_from_display_name(
     """Convert a given display name string into a proper class label string
     Args:
         display_name, str: node display name
-        strict_camel_case, bool: Default, False; defines whether or not to use strict camel case or not for conversion.
+        strict_camel_case, bool: Default, False; defines whether or not to
+         use strict camel case or not for conversion.
     Returns:
         label, str: class label of display name
     """
@@ -73,10 +87,14 @@ def get_class_label_from_display_name(
 def get_attribute_display_name_from_label(
     node_name: str, attr_relationships: dict
 ) -> str:
-    """Get attribute display name for a node, using the node label, requires the attr_relationships dicitonary from the data model parser
+    """
+    Get attribute display name for a node, using the node label, requires the attr_relationships
+      dictionary from the data model parser
+
     Args:
         node_name, str: node label
-        attr_relationships, dict: dictionary defining attributes and relationships, generated in data model parser.
+        attr_relationships, dict: dictionary defining attributes and relationships,
+          generated in data model parser.
     Returns:
         display_name, str: node display name, recorded in attr_relationships.
     """
@@ -89,34 +107,42 @@ def get_attribute_display_name_from_label(
 
 def check_if_display_name_is_valid_label(
     display_name: str,
-    blacklisted_chars: list[str] = BLACKLISTED_CHARS,
+    blacklisted_chars: Optional[list[str]] = None,
 ) -> bool:
     """Check if the display name can be used as a display label
+
     Args:
-        display_name, str: node display name
-        blacklisted_chars, list[str]: characters that are not permitted for synapse annotations uploads.
+        display_name (str): node display name
+        blacklisted_chars (Optional[list[str]], optional):
+          characters that are not permitted for synapse annotations uploads.
+          Defaults to None.
+
     Returns:
-        valid_label, bool: True, if the display name can be used as a label, False, if it cannot.
+        bool: True, if the display name can be used as a label, False, if it cannot.
     """
-    valid_label = True
-    if any(map(display_name.__contains__, blacklisted_chars)):
-        valid_label = False
+    if blacklisted_chars is None:
+        blacklisted_chars = BLACKLISTED_CHARS
+    valid_label = not any(char in display_name for char in blacklisted_chars)
     return valid_label
 
 
 def get_stripped_label(
     display_name: str,
-    entry_type: str,
-    blacklisted_chars: list[str] = BLACKLISTED_CHARS,
+    entry_type: EntryType,
+    blacklisted_chars: Optional[list[str]] = None,
 ) -> str:
     """
     Args:
         display_name, str: node display name
-        entry_type, str: 'class' or 'property', defines what type the entry is.
-        blacklisted_chars, list[str]: characters that are not permitted for synapse annotations uploads.
+        entry_type, EntryType: 'class' or 'property', defines what type the entry is.
+        blacklisted_chars, list[str]: characters that are not permitted for
+          synapse annotations uploads.
     Returns:
-        stripped_label, str: class or property label that has been stripped of blacklisted characters.
+        stripped_label, str: class or property label that has been stripped
+          of blacklisted characters.
     """
+    if blacklisted_chars is None:
+        blacklisted_chars = BLACKLISTED_CHARS
     if entry_type.lower() == "class":
         stripped_label = [
             get_class_label_from_display_name(str(display_name)).translate(
@@ -132,19 +158,25 @@ def get_stripped_label(
         ][0]
 
     logger.warning(
-        f"Cannot use display name {display_name} as the data model label, becaues it is not formatted properly. Please remove all spaces and blacklisted characters: {str(blacklisted_chars)}. The following label was assigned instead: {stripped_label}"
+        (
+            f"Cannot use display name {display_name} as the data model label, "
+            "because it is not formatted properly. Please remove all spaces and "
+            f"blacklisted characters: {str(blacklisted_chars)}. "
+            f"The following label was assigned instead: {stripped_label}"
+        )
     )
     return stripped_label
 
 
 def get_schema_label(
-    display_name: str, entry_type: str, strict_camel_case: bool
+    display_name: str, entry_type: EntryType, strict_camel_case: bool
 ) -> str:
     """Get the class or property label for a given display name
     Args:
         display_name, str: node display name
-        entry_type, str: 'class' or 'property', defines what type the entry is.
-        strict_camel_case, bool: Default, False; defines whether or not to use strict camel case or not for conversion.
+        entry_type, EntryType: 'class' or 'property', defines what type the entry is.
+        strict_camel_case, bool: Default, False; defines whether or not to use strict
+          camel case or not for conversion.
     Returns:
         label, str: class label of display name
     Raises:
@@ -161,22 +193,26 @@ def get_schema_label(
         )
     else:
         logger.error(
-            f"The entry type submitted: {entry_type}, is not one of the permitted types: 'class' or 'property'"
+            (
+                f"The entry type submitted: {entry_type}, is not one of the "
+                "permitted types: 'class' or 'property'"
+            )
         )
     return label
 
 
 def get_label_from_display_name(
     display_name: str,
-    entry_type: str,
+    entry_type: EntryType,
     strict_camel_case: bool = False,
     data_model_labels: DisplayLabelType = "class_label",
 ) -> str:
     """Get node label from provided display name, based on whether the node is a class or property
     Args:
         display_name, str: node display name
-        entry_type, str: 'class' or 'property', defines what type the entry is.
-        strict_camel_case, bool: Default, False; defines whether or not to use strict camel case or not for conversion.
+        entry_type, EntryType: 'class' or 'property', defines what type the entry is.
+        strict_camel_case, bool: Default, False; defines whether or not to use strict camel
+          case or not for conversion.
     Returns:
         label, str: label to be used for the provided display name.
     """
@@ -212,16 +248,14 @@ def convert_bool_to_str(provided_bool: bool) -> str:
     return str(provided_bool)
 
 
-def get_individual_rules(
-    rule: str, validation_rules: list[Union[str, None]]
-) -> Union[str, list]:
+def get_individual_rules(rule: str, validation_rules: list) -> list:
     """Extract individual rules from a string and add to a list of rules
     Args:
-        rule, str: valdation rule that has been parsed from a component rule.
-        validaiton_rules, list: list of rules being collected,
+        rule, str: validation rule that has been parsed from a component rule.
+        validation_rules, list: list of rules being collected,
             if this is the first time the list is being added to, it will be empty
     Returns:
-        validaiton_rules, list: list of rules being collected.
+        validation_rules, list: list of rules being collected.
     """
     # Separate multiple rules (defined by addition of the rule delimiter)
     if RULE_DELIMITER in rule:
@@ -233,17 +267,23 @@ def get_individual_rules(
 
 
 def get_component_name_rules(
-    component_names: list[Union[str, None]], component_rule: str
-) -> Tuple[list, str]:
-    """Get component name and rule from an string that was initilly split by the COMPONENT_RULES_DELIMITER
+    component_names: list[str], component_rule: str
+) -> tuple[list[str], str]:
+    """
+    Get component name and rule from an string that was initially split by the
+      COMPONENT_RULES_DELIMITER
+
     Args:
-        component_names, list[Union[str,None]]: list of components, will be empty if being added to for the first time.
-        component_rule, str: component rule string that has only been split by the COMPONENT_RULES_DELIMETER
+        component_names, list[str]: list of components, will be empty
+          if being added to for the first time.
+        component_rule, str: component rule string that has only been split by
+          the COMPONENT_RULES_DELIMITER
     Returns:
         Tuple[list,str]: list with the a new component name or 'all_other_components' appended,
             rule with the component name stripped off.
     Raises:
-        Error Logged if it looks like a component name should have been added to the list, but wass not.
+        Error Logged if it looks like a component name should have been added to the
+          list, but was not.
     """
     # If a component name is not attached to the rule, have it apply to all other components
     if COMPONENT_NAME_DELIMITER != component_rule[0]:
@@ -268,34 +308,40 @@ def get_component_name_rules(
 def check_for_duplicate_components(
     component_names: list[str], validation_rule_string: str
 ) -> None:
-    """Check if component names are repeated in a validation rule
+    """
+    Check if component names are repeated in a validation rule
+    Error Logged if a component name is duplicated.
+
     Args:
-        component_names, list[str]: list of components identified in the validation rule
-        validation_rule_str, str: validation rule, used if error needs to be raised.
-    Returns:
-        None
-    Raises: Error Logged if a component name is duplicated.
+        component_names (list[str]): list of components identified in the validation rule
+        validation_rule_string (str): validation rule, used if error needs to be raised.
     """
     duplicated_entries = [cn for cn in component_names if component_names.count(cn) > 1]
     if duplicated_entries:
         logger.error(
-            f"Oops, it looks like the following rule {validation_rule_string}, contains the same component "
-            f"name more than once. An attribute can only have a single rule applied per manifest/component."
+            f"Oops, it looks like the following rule {validation_rule_string}, "
+            "contains the same component name more than once. An attribute can "
+            "only have a single rule applied per manifest/component."
         )
-    return
 
 
-def parse_component_validation_rules(validation_rule_string: str) -> Dict:
-    """If a validation rule is identified to be fomatted as a component validation rule, parse to a dictionary of components:rules
+def parse_component_validation_rules(
+    validation_rule_string: str,
+) -> dict[str, list[str]]:
+    """
+    If a validation rule is identified to be formatted as a component validation rule,
+      parse to a dictionary of components:rules
+
     Args:
-        validation_rule_string, str: validation rule provided by user.
+        validation_rule_string (str):  validation rule provided by user.
+
     Returns:
-        validation_rules_dict, dict: validation rules parsed to a dictionary where
-            the key is the component name (or 'all_other_components') and the value is the parsed validaiton rule for
-            the given component.
+        dict[str, list[str]]: validation rules parsed to a dictionary where the key
+          is the component name (or 'all_other_components') and the value is the parsed
+          validation rule for the given component.
     """
-    component_names = []
-    validation_rules = []
+    component_names: list[str] = []
+    validation_rules: list[list[str]] = []
 
     component_rules = validation_rule_string.split(COMPONENT_RULES_DELIMITER)
     # Extract component rules, per component
@@ -327,22 +373,21 @@ def parse_component_validation_rules(validation_rule_string: str) -> Dict:
     return validation_rules_dict
 
 
-def parse_single_set_validation_rules(validation_rule_string: str) -> list:
+def parse_single_set_validation_rules(validation_rule_string: str) -> list[str]:
     """Parse a single set of validation rules.
+
     Args:
-        validation_rule_string, str: validation rule provided by user.
+        validation_rule_string (str): validation rule provided by user.
+
     Returns:
-        list, the valiation rule string split by the rule delimiter
-    Raise:
-        ValueEror if the string contains a component name delimter in the beginning.
-            This would indicate that a user was trying to set component rules, but did so improperly.
+        list: the validation rule string split by the rule delimiter
     """
     # Try to catch an improperly formatted rule
     if COMPONENT_NAME_DELIMITER == validation_rule_string[0]:
         logger.error(
-            f"The provided validation rule {validation_rule_string}, looks to be formatted as a component "
-            f"based rule, but is missing the necessary formatting, "
-            f"please refer to the SchemaHub documentation for more details."
+            f"The provided validation rule {validation_rule_string}, looks to be formatted as a "
+            "component based rule, but is missing the necessary formatting, "
+            "please refer to the SchemaHub documentation for more details."
         )
 
     return validation_rule_string.split(RULE_DELIMITER)
@@ -350,70 +395,68 @@ def parse_single_set_validation_rules(validation_rule_string: str) -> list:
 
 def parse_validation_rules(validation_rules: Union[list, dict]) -> Union[list, dict]:
     """Split multiple validation rules based on :: delimiter
+
     Args:
-        validation_rules, Any[List[str], Dict]: List or Dictionary of validation rules,
-            if list, contains a string validation rule; if dictionary, key is the component the
-            rule (value) is applied to
+        validation_rules (Union[list, dict]): List or Dictionary of validation rules,
+            if list:, contains a string validation rule
+            if dict:, key is the component the rule (value) is applied to
+
     Returns:
-        validation_rules, Union[list,dict]: Parsed validation rules, component rules are output as a dictionary,
-            single sets are a list.
-    Raises:
-        Error Logged if Rule is not formatted properly
+        Union[list, dict]: Parsed validation rules, component rules are output
+          as a dictionary, single sets are a list.
     """
-
     if isinstance(validation_rules, dict):
         # Rules pulled in as a dict can be used directly
         return validation_rules
-    elif isinstance(validation_rules, list):
-        # If rules are already parsed from the JSONLD
-        if len(validation_rules) > 1 and isinstance(validation_rules[-1], str):
-            return validation_rules
-        # Parse rules set for a subset of components/manifests
-        elif COMPONENT_RULES_DELIMITER in validation_rules[0]:
-            return parse_component_validation_rules(
-                validation_rule_string=validation_rules[0]
-            )
-        # Parse rules that are set across *all* components/manifests
-        else:
-            return parse_single_set_validation_rules(
-                validation_rule_string=validation_rules[0]
-            )
-    return
+
+    # If rules are already parsed from the JSONLD
+    if len(validation_rules) > 1 and isinstance(validation_rules[-1], str):
+        return validation_rules
+    # Parse rules set for a subset of components/manifests
+    if COMPONENT_RULES_DELIMITER in validation_rules[0]:
+        return parse_component_validation_rules(
+            validation_rule_string=validation_rules[0]
+        )
+    # Parse rules that are set across *all* components/manifests
+    return parse_single_set_validation_rules(validation_rule_string=validation_rules[0])
 
 
 def extract_component_validation_rules(
-    manifest_component: str, validation_rules: dict[str, list]
-) -> list:
-    """Parse a component validation rule dictionary to pull out the rule (if any) for a given manifest
+    manifest_component: str, validation_rules_dict: dict[str, Union[list, str]]
+) -> list[Union[str, list]]:
+    """
+    Parse a component validation rule dictionary to pull out the rule (if any) for a given manifest
+
     Args:
         manifest_component, str: Component label, pulled from the manifest directly
-        validation_rules, dict[str, list[Union[list,str]]: Validation rules dictionary, where keys are the manifest component label,
-            and the value is a parsed set of validation rules.
+        validation_rules_dict, dict[str, list[Union[list,str]]: Validation rules dictionary,
+          where keys are the manifest component label, and the value is a parsed set of
+          validation rules.
     Returns:
         validation_rules, list[str]: rule for the provided manifest component if one is available,
-            if a validation rule is not specified for a given component but "all_other_components" is specified (as a key), then pull that one,
-            otherwise return an empty list.
+            if a validation rule is not specified for a given component but "all_other_components"
+            is specified (as a key), then pull that one, otherwise return an empty list.
     """
-    manifest_component_rule = validation_rules.get(manifest_component)
-    all_component_rules = validation_rules.get("all_other_components")
+    manifest_component_rule = validation_rules_dict.get(manifest_component)
+    all_component_rules = validation_rules_dict.get("all_other_components")
 
     # Capture situation where manifest_component rule is an empty string
     if manifest_component_rule is not None:
         if isinstance(manifest_component_rule, str):
             if manifest_component_rule == "":
-                validation_rules = []
+                validation_rules_list: list[Union[str, list]] = []
             else:
-                validation_rules = [manifest_component_rule]
+                validation_rules_list = [manifest_component_rule]
         elif isinstance(manifest_component_rule, list):
-            validation_rules = manifest_component_rule
+            validation_rules_list = manifest_component_rule
     elif all_component_rules:
         if isinstance(all_component_rules, str):
-            validation_rules = [all_component_rules]
+            validation_rules_list = [all_component_rules]
         elif isinstance(all_component_rules, list):
-            validation_rules = all_component_rules
+            validation_rules_list = all_component_rules
     else:
-        validation_rules = []
-    return validation_rules
+        validation_rules_list = []
+    return validation_rules_list
 
 
 def export_schema(schema: dict, file_path: str) -> None:
@@ -422,20 +465,20 @@ def export_schema(schema: dict, file_path: str) -> None:
         schema, dict: JSONLD schema
         filepath, str: path to store the schema
     """
-    with open(file_path, "w") as f:
-        json.dump(schema, f, sort_keys=True, indent=4, ensure_ascii=False)
+    with open(file_path, "w", encoding="utf-8") as json_file:
+        json.dump(schema, json_file, sort_keys=True, indent=4, ensure_ascii=False)
 
 
-def strip_context(context_value: str) -> tuple[str]:
+def strip_context(context_value: str) -> tuple[str, str]:
     """Strip contexts from str entry.
     Args:
         context_value, str: string from which to strip context from
     Returns:
         context, str: the original context
-        v, str: value separated from context
+        value, str: value separated from context
     """
     if ":" in context_value:
-        context, v = context_value.split(":")
+        context, value = context_value.split(":")
     elif "@" in context_value:
-        context, v = context_value.split("@")
-    return context, v
+        context, value = context_value.split("@")
+    return context, value
diff --git a/schematic/utils/validate_rules_utils.py b/schematic/utils/validate_rules_utils.py
index 3ee3399e6..e5192ce8a 100644
--- a/schematic/utils/validate_rules_utils.py
+++ b/schematic/utils/validate_rules_utils.py
@@ -1,16 +1,25 @@
 """validate rules utils"""
 
 import logging
-from typing import Union
+from typing import TypedDict, Optional, Literal
+from typing_extensions import assert_never
 from jsonschema import ValidationError
 
 
 logger = logging.getLogger(__name__)
 
 
-def validation_rule_info() -> (
-    dict[str, dict[str, Union[tuple[int, int], str, list[str], None]]]
-):
+class Rule(TypedDict):
+    """A validation rule"""
+
+    arguments: tuple[int, int]
+    type: str
+    complementary_rules: Optional[list[str]]
+    default_message_level: Optional[str]
+    fixed_arg: Optional[list[str]]
+
+
+def validation_rule_info() -> dict[str, Rule]:
     """
     Function to return dict that holds information about each rule
     Will be pulled into validate_single_rule, validate_manifest_rules, validate_schema_rules
@@ -27,30 +36,35 @@ def validation_rule_info() -> (
             "type": "type_validation",
             "complementary_rules": ["inRange", "IsNA"],
             "default_message_level": "error",
+            "fixed_arg": None,
         },
         "float": {
             "arguments": (1, 0),
             "type": "type_validation",
             "complementary_rules": ["inRange", "IsNA"],
             "default_message_level": "error",
+            "fixed_arg": None,
         },
         "num": {
             "arguments": (1, 0),
             "type": "type_validation",
             "complementary_rules": ["inRange", "IsNA"],
             "default_message_level": "error",
+            "fixed_arg": None,
         },
         "str": {
             "arguments": (1, 0),
             "type": "type_validation",
             "complementary_rules": None,
             "default_message_level": "error",
+            "fixed_arg": None,
         },
         "date": {
             "arguments": (1, 0),
             "type": "content_validation",
             "complementary_rules": None,
             "default_message_level": "error",
+            "fixed_arg": None,
         },
         "regex": {
             "arguments": (3, 2),
@@ -64,30 +78,42 @@ def validation_rule_info() -> (
             "type": "url_validation",
             "complementary_rules": None,
             "default_message_level": "error",
+            "fixed_arg": None,
         },
         "list": {
             "arguments": (2, 0),
             "type": "list_validation",
             "complementary_rules": ["regex"],
             "default_message_level": "error",
+            "fixed_arg": None,
         },
         "matchAtLeastOne": {
             "arguments": (3, 2),
             "type": "cross_validation",
             "complementary_rules": None,
             "default_message_level": "warning",
+            "fixed_arg": None,
         },
         "matchExactlyOne": {
             "arguments": (3, 2),
             "type": "cross_validation",
             "complementary_rules": None,
             "default_message_level": "warning",
+            "fixed_arg": None,
+        },
+        "matchNone": {
+            "arguments": (3, 2),
+            "type": "cross_validation",
+            "complementary_rules": None,
+            "default_message_level": "warning",
+            "fixed_arg": None,
         },
         "recommended": {
             "arguments": (1, 0),
             "type": "content_validation",
             "complementary_rules": None,
             "default_message_level": "warning",
+            "fixed_arg": None,
         },
         "protectAges": {
             "arguments": (1, 0),
@@ -96,28 +122,32 @@ def validation_rule_info() -> (
                 "inRange",
             ],
             "default_message_level": "warning",
+            "fixed_arg": None,
         },
         "unique": {
             "arguments": (1, 0),
             "type": "content_validation",
             "complementary_rules": None,
             "default_message_level": "error",
+            "fixed_arg": None,
         },
         "inRange": {
             "arguments": (3, 2),
             "type": "content_validation",
             "complementary_rules": ["int", "float", "num", "protectAges"],
             "default_message_level": "error",
+            "fixed_arg": None,
         },
         "IsNA": {
-            "arguments": (1, 0),
+            "arguments": (0, 0),
             "type": "content_validation",
             "complementary_rules": [
                 "int",
                 "float",
                 "num",
             ],
-            "default_message_level": "warning",
+            "default_message_level": None,
+            "fixed_arg": None,
         },
     }
 
@@ -125,7 +155,9 @@ def validation_rule_info() -> (
 def get_error(
     validation_rules: str,
     attribute_name: str,
-    error_type: str,
+    error_type: Literal[
+        "delimiter", "not_rule", "args_not_allowed", "incorrect_num_args"
+    ],
     input_filetype: str,
 ) -> list[str]:
     """
@@ -145,7 +177,7 @@ def get_error(
         error_message = error_str
         error_val = "Multiple Rules: Delimiter"
 
-    if error_type == "not_rule":
+    elif error_type == "not_rule":
         error_str = (
             f"The {input_filetype}, has an error in the validation rule "
             f"for the attribute: {attribute_name}, the provided validation rules "
@@ -156,7 +188,7 @@ def get_error(
         error_message = error_str
         error_val = "Not a Rule"
 
-    if error_type == "args_not_allowed":
+    elif error_type == "args_not_allowed":
         error_str = (
             f"The {input_filetype}, has an error in the validation rule "
             f"for the attribute: {attribute_name}, the provided validation rules "
@@ -166,7 +198,8 @@ def get_error(
         logging.error(error_str)
         error_message = error_str
         error_val = "Args not allowed."
-    if error_type == "incorrect_num_args":
+
+    elif error_type == "incorrect_num_args":
         rule_type = validation_rules.split(" ")[0]
 
         if rule_type in validation_rule_info():
@@ -189,10 +222,15 @@ def get_error(
         error_message = error_str
         error_val = "Incorrect num arguments."
 
+    else:
+        assert_never(error_type)
+
     return ["NA", error_col, error_message, error_val]
 
 
-def validate_single_rule(validation_rule: str, attribute: str, input_filetype: str):
+def validate_single_rule(
+    validation_rule: str, attribute: str, input_filetype: str
+) -> list[list[str]]:
     """
     Perform validation for a single rule to ensure it is specified
       correctly with an appropriate number of arguments
@@ -204,7 +242,7 @@ def validate_single_rule(validation_rule: str, attribute: str, input_filetype: s
     Returns:
         errors: List of errors
     """
-    errors = []
+    errors: list[list[str]] = []
     validation_types = validation_rule_info()
     validation_rule_with_args = [
         val_rule.strip() for val_rule in validation_rule.strip().split(" ")
@@ -239,9 +277,8 @@ def validate_single_rule(validation_rule: str, attribute: str, input_filetype: s
         assert len(arg_tuple) == 2
         arguments_allowed, arguments_required = arg_tuple
         # Remove any fixed args from our calc.
-        if "fixed_arg" in validation_types[rule_type]:
-            fixed_args = validation_types[rule_type]["fixed_arg"]
-            assert isinstance(fixed_args, list)
+        fixed_args = validation_types[rule_type]["fixed_arg"]
+        if fixed_args:
             num_args = (
                 len([vr for vr in validation_rule_with_args if vr not in fixed_args])
                 - 1
diff --git a/schematic/utils/validate_utils.py b/schematic/utils/validate_utils.py
index fed5b422c..76cb6f903 100644
--- a/schematic/utils/validate_utils.py
+++ b/schematic/utils/validate_utils.py
@@ -4,6 +4,7 @@
 
 import re
 from collections.abc import Mapping
+import logging
 from typing import Pattern, Union, Iterable, Any, Optional
 from numbers import Number
 from jsonschema import validate
@@ -12,6 +13,8 @@
 from schematic.utils.io_utils import load_json
 from schematic import LOADER
 
+logger = logging.getLogger(__name__)
+
 
 def validate_schema(schema: Union[Mapping, bool]) -> None:
     """Validate schema against schema.org standard"""
@@ -76,8 +79,9 @@ def parse_str_series_to_list(col: pd.Series) -> pd.Series:
         Output: ['a','b','c']
 
     """
-    col = col.apply(lambda x: [s.strip() for s in str(x).split(",")])
-
+    col = col.apply(
+        lambda x: [s.strip() for s in str(x).split(",")] if not pd.isnull(x) else pd.NA
+    )
     return col
 
 
@@ -103,3 +107,73 @@ def iterable_to_str_list(obj: Union[str, Number, Iterable]) -> list[str]:
     # If the object is iterable and not a string, convert every element
     # to string and wrap as a list
     return [str(item) for item in obj]
+
+
+def required_is_only_rule(
+    rule: str,
+    attribute: str,
+    rule_modifiers: list[str],
+    validation_expectation: dict[str, str],
+) -> bool:
+    """Need to determine if required is the only rule being set. Do this way so we dont have
+    to enforce a position for it (ie, it can only be before message and after the rule).
+    This ensures that 'required' is not treated like a real rule, in the case it is
+    accidentally combined with a rule modifier. The required rule is t
+
+    Args:
+        rule: str, the validation rule string
+        attribute: str, attribute the validation rule is set to
+        rule_modifiers: list[str], list of rule modifiers available to add to rules
+        validation_expectation: dict[str, str], currently implemented expectations.
+    Returns:
+        bool, True, if required is the only rule, false if it is not.
+    """
+    # convert rule to lowercase to ensure punctuation does not throw off determination.
+    rule = rule.lower()
+
+    # If required is not in the rule, it cant be the only rule, return False
+    if "required" not in rule:
+        return False
+
+    # If the entire rule is just 'required' then it is easily determined to be the only rule
+    if rule == "required":
+        return True
+
+    # Try to find an expectation rule in the rule, if there is one there log it and
+    # continue
+    # This function is called as part of an if that is already looking for in house rules
+    # so don't worry about looking for them.
+    rule_parts = rule.split(" ")
+    for idx, rule_part in enumerate(rule_parts):
+        if rule_part in validation_expectation:
+            return False
+
+    # identify then remove all rule modifiers, all that should be left is required in the
+    # case that someone used a standard modifier with required
+    idx_to_remove = []
+    if "required" in rule_parts:
+        for idx, rule_part in enumerate(rule_parts):
+            if rule_part in rule_modifiers:
+                idx_to_remove.append(idx)
+
+    if idx_to_remove:
+        for idx in sorted(idx_to_remove, reverse=True):
+            del rule_parts[idx]
+
+    # In this case, rule modifiers have been added to required. This is not the expected use
+    # so log a warning, but let user proceed.
+    if rule_parts == ["required"]:
+        warning_message = " ".join(
+            [
+                f"For Attribute: {attribute}, it looks like required was set as a single rule,"
+                "with modifiers attached.",
+                "Rule modifiers do not work in conjunction with the required validation rule.",
+                "Please reformat your rule.",
+            ]
+        )
+        logger.warning(warning_message)
+        return True
+
+    # Return false if no other condition has been met. In this case if the rule is not a real
+    # rule an error will be raised from the containing function.
+    return False
diff --git a/schematic/utils/viz_utils.py b/schematic/utils/viz_utils.py
index 58a904728..48dff6867 100644
--- a/schematic/utils/viz_utils.py
+++ b/schematic/utils/viz_utils.py
@@ -1,24 +1,27 @@
 """viz utils"""
 
-from typing import Optional
+from typing import Optional, Iterable, Sequence
 import graphviz  # type: ignore
 
 
-def visualize(edges, size: Optional[float] = None) -> graphviz.Digraph:
-    """_summary_
+def visualize(
+    edges: Iterable[Sequence[str]], size: Optional[float] = None
+) -> graphviz.Digraph:
+    """Creates a digraph with an edge for every edge in the edges input
 
     Args:
-        edges (_type_): _description_
-        size (Optional[float], optional): _description_. Defaults to None.
+        edges (Iterable[Sequence[str]]): Any iterable type that contains
+          indexable types with at least two strings
+        size (Optional[float], optional): Defaults to None.
 
     Returns:
-        graphviz.Digraph: _description_
+        graphviz.Digraph:
     """
     if size:
         digraph = graphviz.Digraph(graph_attr=[("size", size)])
     else:
         digraph = graphviz.Digraph()
 
-    for _item in edges:
-        digraph.edge(_item[0], _item[1])
+    for item in edges:
+        digraph.edge(item[0], item[1])
     return digraph
diff --git a/schematic/visualization/attributes_explorer.py b/schematic/visualization/attributes_explorer.py
index 420dd4f0f..9691932e7 100644
--- a/schematic/visualization/attributes_explorer.py
+++ b/schematic/visualization/attributes_explorer.py
@@ -3,7 +3,6 @@
 import logging
 import os
 from typing import Optional, no_type_check
-
 import numpy as np
 import pandas as pd
 
@@ -19,31 +18,38 @@
 class AttributesExplorer:
     """AttributesExplorer class"""
 
+    # pylint: disable=too-many-arguments
     def __init__(
         self,
         path_to_jsonld: str,
         data_model_labels: DisplayLabelType,
+        data_model_grapher: Optional[DataModelGraph] = None,
+        data_model_graph_explorer: Optional[DataModelGraphExplorer] = None,
+        parsed_data_model: Optional[dict] = None,
     ) -> None:
         self.path_to_jsonld = path_to_jsonld
 
         self.jsonld = load_json(self.path_to_jsonld)
 
-        # Instantiate Data Model Parser
-        data_model_parser = DataModelParser(
-            path_to_data_model=self.path_to_jsonld,
-        )
-
         # Parse Model
-        parsed_data_model = data_model_parser.parse_model()
+        if not parsed_data_model:
+            data_model_parser = DataModelParser(
+                path_to_data_model=self.path_to_jsonld,
+            )
+            parsed_data_model = data_model_parser.parse_model()
 
         # Instantiate DataModelGraph
-        data_model_grapher = DataModelGraph(parsed_data_model, data_model_labels)
+        if not data_model_grapher:
+            data_model_grapher = DataModelGraph(parsed_data_model, data_model_labels)
 
         # Generate graph
-        self.graph_data_model = data_model_grapher.generate_data_model_graph()
+        self.graph_data_model = data_model_grapher.graph
 
         # Instantiate Data Model Graph Explorer
-        self.dmge = DataModelGraphExplorer(self.graph_data_model)
+        if not data_model_graph_explorer:
+            self.dmge = DataModelGraphExplorer(self.graph_data_model)
+        else:
+            self.dmge = data_model_graph_explorer
 
         # Instantiate Data Model Json Schema
         self.data_model_js = DataModelJSONSchema(
diff --git a/schematic/visualization/tangled_tree.py b/schematic/visualization/tangled_tree.py
index 4be2927b1..59ba62139 100644
--- a/schematic/visualization/tangled_tree.py
+++ b/schematic/visualization/tangled_tree.py
@@ -71,7 +71,7 @@ def __init__(
         data_model_grapher = DataModelGraph(parsed_data_model, data_model_labels)
 
         # Generate graph
-        self.graph_data_model = data_model_grapher.generate_data_model_graph()
+        self.graph_data_model = data_model_grapher.graph
 
         # Instantiate Data Model Graph Explorer
         self.dmge = DataModelGraphExplorer(self.graph_data_model)
@@ -86,7 +86,11 @@ def __init__(
 
         # Initialize AttributesExplorer
         self.attributes_explorer = AttributesExplorer(
-            self.path_to_json_ld, data_model_labels
+            self.path_to_json_ld,
+            data_model_labels,
+            data_model_grapher=data_model_grapher,
+            data_model_graph_explorer=self.dmge,
+            parsed_data_model=parsed_data_model,
         )
 
         # Create output paths.
diff --git a/schematic_api/api/README.md b/schematic_api/api/README.md
index 0abddc730..7a0896761 100644
--- a/schematic_api/api/README.md
+++ b/schematic_api/api/README.md
@@ -30,7 +30,7 @@ Note: this approach only works for unix OSs users or windows user with WSL
 See steps below: 
 1) Comment out the first part of `docker-compose.yml` and focus only on building container `schematic-aws`.
 
-2) Get `service_account_creds.json` by doing `schematic init --config /path/to/config.yml`. 
+2) Get `schematic_service_account_creds.json` by following these [instructions](https://scribehow.com/shared/Enable_Google_Drive_and_Google_Sheets_APIs_for_project__yqfcJz_rQVeyTcg0KQCINA)
 
 3) Make a copy of `env.example` and rename it as `.env` and keep it in the same directory as `env.example`. By default, schematic uses port 81. If port 81 is not available, please update `USE_LISTEN_PORT` in `.env` file. 
 
@@ -54,14 +54,7 @@ By default, this command builds up two containers (`schematic` and `schematic-aw
 pip install connexion['swagger=ui']
 ```
 
-2. Please also consider following these [instructions](https://sage-schematic.readthedocs.io/en/develop/cli_reference.html) to fill in configuration file and run the following command to obtain `credentials.json` and `token.pickle`:
-```bash 
-schematic init --config ~/path/to/config.yml
-```
-
-Note: credentials.json and token.pickle have to be in the same directory as run_api.py. If you have obtained credentials.json before (but credentials.json is a different directory), please put config.yml and .synapseConfig in the same directory as run_api.py and run the above command again. 
-
-After running the command above, please follow instructions in your console and visit the URL to authorize schematic. After putting in the authorization code, token.pickle and credentials.json should be automatically downloaded in your local directory. 
+2. Due to security concerns, running `schematic init` is no longer supported. Please follow these [instructions](https://scribehow.com/shared/Enable_Google_Drive_and_Google_Sheets_APIs_for_project__yqfcJz_rQVeyTcg0KQCINA) to obtain `schematic_service_account_creds.json`. Please specify the path of `schematic_service_account_creds.json` in config.yml (see google_sheets > service_acct_creds) if you intend to use the configuration file. 
 
 ## Access Results
 Access the Swagger UI docs at this location:
@@ -88,7 +81,7 @@ Before trying out the API endpoints, please make sure that you have obtained `sc
 
 This endpoint functions similarly to the following: 
 ```bash
-schematic manifest -c ~/path/to/config.yml get -d <synapse id> -s -oauth
+schematic manifest -c ~/path/to/config.yml get -d <synapse id>
 ```
 
 Examples: 
@@ -100,7 +93,7 @@ Examples:
 
 * Step 1:  Make sure you have credentials to download the desired manifest from Synapse. The "download" button should be disabled if you don't have credentials. 
 
-* Step 2: Make sure you set asset_view to the right value. For Synapse, asset_view is the same as master_fileview in config.yml. 
+* Step 2: Make sure you set asset_view to the right value. For Synapse, asset_view is the same as master_fileview_id in config.yml. 
 
 * Step 3: Use parent id of the manifest for "dataset_id" parameter
     
@@ -108,7 +101,7 @@ Note: if the dataset_id you provided is invalid, it will generate an empty manif
 
 ### POST /model/submit
     
-* For the access_token parameter, please use the value of `auth token` in your `.synapseConfig`
+* For authorization, please use the value of `auth token` in your `.synapseConfig`
 
 * For the dataset_id parameter, please create a test folder on synapse and use its synapse ID
 
diff --git a/schematic_api/api/openapi/api.yaml b/schematic_api/api/openapi/api.yaml
index 50c0e3dc0..15a3540d1 100644
--- a/schematic_api/api/openapi/api.yaml
+++ b/schematic_api/api/openapi/api.yaml
@@ -442,6 +442,13 @@ paths:
             enum: ["display_label", "class_label"]
             default: "class_label"
           required: false
+        - in: query
+          name: file_annotations_upload
+          schema:
+            type: boolean
+            default: true
+          description: if false, do not add annotations when submitting file-based manifests. 
+          required: false
         - in: query
           name: project_scope
           schema:
diff --git a/schematic_api/api/routes.py b/schematic_api/api/routes.py
index c1b0f31c9..fbf36fbf5 100644
--- a/schematic_api/api/routes.py
+++ b/schematic_api/api/routes.py
@@ -19,6 +19,7 @@
 
 import pandas as pd
 import json
+from typing import Optional
 
 from schematic.configuration.configuration import CONFIG
 from schematic.visualization.attributes_explorer import AttributesExplorer
@@ -209,7 +210,6 @@ def save_file(file_key="csv_file"):
 
     return temp_path
 
-
 def initalize_metadata_model(schema_url, data_model_labels):
     # get path to temp data model file (csv or jsonld) as appropriate
     data_model = get_temp_model_path(schema_url)
@@ -393,6 +393,7 @@ def submit_manifest_route(
     project_scope=None,
     table_column_names=None,
     annotation_keys=None,
+    file_annotations_upload:bool=True,
 ):
     # call config_handler()
     config_handler(asset_view=asset_view)
@@ -438,7 +439,6 @@ def submit_manifest_route(
     access_token = get_access_token()
 
     manifest_id = metadata_model.submit_metadata_manifest(
-        path_to_json_ld=data_model,
         manifest_path=temp_path,
         dataset_id=dataset_id,
         validate_component=validate_component,
@@ -450,6 +450,7 @@ def submit_manifest_route(
         project_scope=project_scope,
         table_column_names=table_column_names,
         annotation_keys=annotation_keys,
+        file_annotations_upload=file_annotations_upload
     )
 
     return manifest_id
@@ -604,7 +605,7 @@ def get_viz_component_attributes_explorer(
 
     attributes_csv = AttributesExplorer(
         data_model, data_model_labels
-    ).parse_component_attributes(
+    )._parse_component_attributes(
         component, save_file=False, include_index=include_index
     )
 
@@ -769,7 +770,7 @@ def get_schema_pickle(schema_url, data_model_labels):
     data_model_grapher = DataModelGraph(parsed_data_model, data_model_labels)
 
     # Generate graph
-    graph_data_model = data_model_grapher.generate_data_model_graph()
+    graph_data_model = data_model_grapher.graph
 
     # write to local pickle file
     path = os.getcwd()
@@ -790,7 +791,7 @@ def get_subgraph_by_edge_type(schema_url, relationship, data_model_labels):
     data_model_grapher = DataModelGraph(parsed_data_model, data_model_labels)
 
     # Generate graph
-    graph_data_model = data_model_grapher.generate_data_model_graph()
+    graph_data_model = data_model_grapher.graph
 
     dmge = DataModelGraphExplorer(graph_data_model)
 
@@ -814,7 +815,7 @@ def find_class_specific_properties(schema_url, schema_class, data_model_labels):
     data_model_grapher = DataModelGraph(parsed_data_model, data_model_labels)
 
     # Generate graph
-    graph_data_model = data_model_grapher.generate_data_model_graph()
+    graph_data_model = data_model_grapher.graph
 
     dmge = DataModelGraphExplorer(graph_data_model)
 
@@ -856,7 +857,7 @@ def get_node_dependencies(
     data_model_grapher = DataModelGraph(parsed_data_model, data_model_labels)
 
     # Generate graph
-    graph_data_model = data_model_grapher.generate_data_model_graph()
+    graph_data_model = data_model_grapher.graph
 
     dmge = DataModelGraphExplorer(graph_data_model)
 
@@ -911,7 +912,7 @@ def get_node_range(
     data_model_grapher = DataModelGraph(parsed_data_model, data_model_labels)
 
     # Generate graph
-    graph_data_model = data_model_grapher.generate_data_model_graph()
+    graph_data_model = data_model_grapher.graph
 
     dmge = DataModelGraphExplorer(graph_data_model)
 
@@ -940,7 +941,7 @@ def get_if_node_required(
     data_model_grapher = DataModelGraph(parsed_data_model, data_model_labels)
 
     # Generate graph
-    graph_data_model = data_model_grapher.generate_data_model_graph()
+    graph_data_model = data_model_grapher.graph
 
     dmge = DataModelGraphExplorer(graph_data_model)
 
@@ -969,7 +970,7 @@ def get_node_validation_rules(
     data_model_grapher = DataModelGraph(parsed_data_model, data_model_labels)
 
     # Generate graph
-    graph_data_model = data_model_grapher.generate_data_model_graph()
+    graph_data_model = data_model_grapher.graph
 
     # Instantiate DataModelGraphExplorer
     dmge = DataModelGraphExplorer(graph_data_model)
@@ -1002,7 +1003,7 @@ def get_nodes_display_names(
     data_model_grapher = DataModelGraph(parsed_data_model, data_model_labels)
 
     # Generate graph
-    graph_data_model = data_model_grapher.generate_data_model_graph()
+    graph_data_model = data_model_grapher.graph
 
     # Instantiate DataModelGraphExplorer
     dmge = DataModelGraphExplorer(graph_data_model)
diff --git a/schematic_cli_examples.ipynb b/schematic_cli_examples.ipynb
index 78de2c135..c600379cf 100644
--- a/schematic_cli_examples.ipynb
+++ b/schematic_cli_examples.ipynb
@@ -19,20 +19,10 @@
     "\n",
     "```\n",
     "%%bash\n",
-    "schematic init --config ~/path/to/config.yml --auth service_account\n",
+    "schematic schema convert tests/data/example.model.csv\n",
     "```"
    ]
   },
-  {
-   "cell_type": "markdown",
-   "id": "d3f19a9e-a752-4c1d-8139-2dd1d6bf8ea9",
-   "metadata": {},
-   "source": [
-    "### Initalize schematic \n",
-    "To get service account credentials:  \n",
-    "* `schematic init --config ~/path/to/config.yml`"
-   ]
-  },
   {
    "cell_type": "markdown",
    "id": "8d95eca3-6256-49a4-a84d-8cefd94f1899",
@@ -118,9 +108,9 @@
    "id": "90dd36f9-8765-4e80-abf3-b134361e195a",
    "metadata": {},
    "source": [
-    "Submit a manifest as a table \n",
+    "Submit a manifest as a file\n",
     "\n",
-    "* `schematic model -c ~/path/to/config.yml submit -mp /path/to/Patient-Metadata-Manifest.csv -d syn23643250 -vc Patient -mrt table`\n"
+    "* `schematic model -c ~/path/to/config.yml submit -mp /path/to/Patient-Metadata-Manifest.csv -d syn23643250 -vc Patient -mrt file_only`\n"
    ]
   }
  ],


Test patch:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index a8fff3b6d..0663698e6 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -101,7 +101,7 @@ jobs:
           # add here when checked
           # poetry run mypy --install-types --non-interactive
           # add here when enforced
-          poetry run mypy --disallow-untyped-defs --install-types --non-interactive schematic/configuration/*.py schematic/exceptions.py schematic/help.py schematic/loader.py schematic/version.py schematic/visualization
+          poetry run mypy --disallow-untyped-defs --install-types --non-interactive schematic/configuration/*.py schematic/exceptions.py schematic/help.py schematic/loader.py schematic/version.py schematic/visualization schematic/utils/
 
       #----------------------------------------------
       #             linting
@@ -110,11 +110,7 @@ jobs:
         run: |
           # ran only on certain files for now
           # add here when checked
-          poetry run pylint schematic/visualization/* schematic/configuration/*.py schematic/exceptions.py schematic/help.py schematic/loader.py schematic/version.py
-          # do all utils but schema_utils.py
-          poetry run pylint schematic/utils/cli_utils.py schematic/utils/curie_utils.py schematic/utils/df_utils.py
-          poetry run pylint schematic/utils/general.py schematic/utils/google_api_utils.py schematic/utils/io_utils.py
-          poetry run pylint schematic/utils/validate_rules_utils.py schematic/utils/validate_utils.py schematic/utils/viz_utils.py
+          poetry run pylint schematic/visualization/* schematic/configuration/*.py schematic/exceptions.py schematic/help.py schematic/loader.py schematic/version.py schematic/utils/*.py schematic/schemas/*.py
 
       #----------------------------------------------
       #              run test suite
diff --git a/tests/conftest.py b/tests/conftest.py
index 6f72cf596..5e19933b3 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -81,7 +81,7 @@ def get_data_model_graph_explorer(
         )
 
         # Generate graph
-        graph_data_model = data_model_grapher.generate_data_model_graph()
+        graph_data_model = data_model_grapher.graph
 
         # Instantiate DataModelGraphExplorer
         DMGE = DataModelGraphExplorer(graph_data_model)
diff --git a/tests/data/example.model.csv b/tests/data/example.model.csv
index f49aa8d3a..bdafffa00 100644
--- a/tests/data/example.model.csv
+++ b/tests/data/example.model.csv
@@ -19,7 +19,7 @@ CRAM,,,"Genome Build, Genome FASTA",,FALSE,ValidValue,,,
 CSV/TSV,,,Genome Build,,FALSE,ValidValue,,,
 Genome Build,,"GRCh37, GRCh38, GRCm38, GRCm39",,,TRUE,DataProperty,,,
 Genome FASTA,,,,,TRUE,DataProperty,,,
-MockComponent,,,"Component, Check List, Check Regex List, Check Regex Single, Check Regex Format, Check Regex Integer, Check Num, Check Float, Check Int, Check String, Check URL,Check Match at Least, Check Match at Least values, Check Match Exactly, Check Match Exactly values, Check Recommended, Check Ages, Check Unique, Check Range, Check Date, Check NA",,FALSE,DataType,,,
+MockComponent,,,"Component, Check List, Check Regex List, Check Regex Single, Check Regex Format, Check Regex Integer, Check Num, Check Float, Check Int, Check String, Check URL,Check Match at Least, Check Match at Least values, Check Match Exactly, Check Match Exactly values, Check Match None, Check Match None values, Check Recommended, Check Ages, Check Unique, Check Range, Check Date, Check NA",,FALSE,DataType,,,
 Check List,,"ab, cd, ef, gh",,,TRUE,DataProperty,,,list strict 
 Check Regex List,,,,,TRUE,DataProperty,,,list strict::regex match [a-f]
 Check Regex Single,,,,,TRUE,DataProperty,,,regex search [a-f]
@@ -32,8 +32,10 @@ Check String,,,,,TRUE,DataProperty,,,str
 Check URL,,,,,TRUE,DataProperty,,,url
 Check Match at Least,,,,,TRUE,DataProperty,,,matchAtLeastOne Patient.PatientID set
 Check Match Exactly,,,,,TRUE,DataProperty,,,matchExactlyOne MockComponent.checkMatchExactly set
+Check Match None,,,,,TRUE,DataProperty,,,matchNone MockComponent.checkMatchNone set error
 Check Match at Least values,,,,,TRUE,DataProperty,,,matchAtLeastOne MockComponent.checkMatchatLeastvalues value
 Check Match Exactly values,,,,,TRUE,DataProperty,,,matchExactlyOne MockComponent.checkMatchExactlyvalues value
+Check Match None values,,,,,TRUE,DataProperty,,,matchNone MockComponent.checkMatchNonevalues value error
 Check Recommended,,,,,FALSE,DataProperty,,,recommended
 Check Ages,,,,,TRUE,DataProperty,,,protectAges
 Check Unique,,,,,TRUE,DataProperty,,,unique error
diff --git a/tests/data/example.model.jsonld b/tests/data/example.model.jsonld
index 9724e341d..df8c50b9a 100644
--- a/tests/data/example.model.jsonld
+++ b/tests/data/example.model.jsonld
@@ -919,6 +919,12 @@
                 {
                     "@id": "bts:CheckMatchExactlyvalues"
                 },
+                {
+                    "@id": "bts:CheckMatchNone"
+                },
+                {
+                    "@id": "bts:CheckMatchNonevalues"
+                },
                 {
                     "@id": "bts:CheckRecommended"
                 },
@@ -1221,6 +1227,44 @@
                 "matchExactlyOne MockComponent.checkMatchExactlyvalues value"
             ]
         },
+        {
+            "@id": "bts:CheckMatchNone",
+            "@type": "rdfs:Class",
+            "rdfs:comment": "TBD",
+            "rdfs:label": "CheckMatchNone",
+            "rdfs:subClassOf": [
+                {
+                    "@id": "bts:DataProperty"
+                }
+            ],
+            "schema:isPartOf": {
+                "@id": "http://schema.biothings.io"
+            },
+            "sms:displayName": "Check Match None",
+            "sms:required": "sms:true",
+            "sms:validationRules": [
+                "matchNone MockComponent.checkMatchNone set error"
+            ]
+        },
+        {
+            "@id": "bts:CheckMatchNonevalues",
+            "@type": "rdfs:Class",
+            "rdfs:comment": "TBD",
+            "rdfs:label": "CheckMatchNonevalues",
+            "rdfs:subClassOf": [
+                {
+                    "@id": "bts:DataProperty"
+                }
+            ],
+            "schema:isPartOf": {
+                "@id": "http://schema.biothings.io"
+            },
+            "sms:displayName": "Check Match None values",
+            "sms:required": "sms:true",
+            "sms:validationRules": [
+                "matchNone MockComponent.checkMatchNonevalues value error"
+            ]
+        },
         {
             "@id": "bts:CheckRecommended",
             "@type": "rdfs:Class",
diff --git a/tests/data/example_required_vr_test.model.csv b/tests/data/example_required_vr_test.model.csv
new file mode 100644
index 000000000..c7b6aecde
--- /dev/null
+++ b/tests/data/example_required_vr_test.model.csv
@@ -0,0 +1,21 @@
+Attribute,Description,Valid Values,DependsOn,Properties,Required,Parent,DependsOn Component,Source,Validation Rules
+Component,,,,,TRUE,,,,
+Patient,,,"Patient ID, Sex, Year of Birth, Diagnosis, Component",,FALSE,DataType,,,
+Patient ID,,,,,FALSE,DataProperty,,,#Patient unique warning^^#Biospecimen unique required error
+Sex,,"Female, Male, Other",,,TRUE,DataProperty,,,
+Year of Birth,,,,,FALSE,DataProperty,,,
+Diagnosis,,"Healthy, Cancer",,,TRUE,DataProperty,,,
+Cancer,,,"Cancer Type, Family History",,FALSE,ValidValue,,,
+Cancer Type,,"Breast, Colorectal, Lung, Prostate, Skin",,,TRUE,DataProperty,,,
+Family History,,"Breast, Colorectal, Lung, Prostate, Skin",,,TRUE,DataProperty,,,list strict
+Biospecimen,,,"Sample ID, Patient ID, Tissue Status, Component",,FALSE,DataType,Patient,,
+Sample ID,,,,,TRUE,DataProperty,,,
+Tissue Status,,"Healthy, Malignant",,,TRUE,DataProperty,,,
+Bulk RNA-seq Assay,,,"Filename, Sample ID, File Format, Component",,FALSE,DataType,Biospecimen,,
+Filename,,,,,TRUE,DataProperty,,,
+File Format,,"FASTQ, BAM, CRAM, CSV/TSV",,,FALSE,DataProperty,,,^^#BulkRNA-seqAssay required
+BAM,,,Genome Build,,FALSE,ValidValue,,,
+CRAM,,,"Genome Build, Genome FASTA",,FALSE,ValidValue,,,
+CSV/TSV,,,Genome Build,,FALSE,ValidValue,,,
+Genome Build,,"GRCh37, GRCh38, GRCm38, GRCm39",,,TRUE,DataProperty,,,
+Genome FASTA,,,,,TRUE,DataProperty,,,
\ No newline at end of file
diff --git a/tests/data/example_required_vr_test.model.jsonld b/tests/data/example_required_vr_test.model.jsonld
new file mode 100644
index 000000000..47821c20e
--- /dev/null
+++ b/tests/data/example_required_vr_test.model.jsonld
@@ -0,0 +1,865 @@
+{
+    "@context": {
+        "bts": "http://schema.biothings.io/",
+        "rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
+        "rdfs": "http://www.w3.org/2000/01/rdf-schema#",
+        "schema": "http://schema.org/",
+        "xsd": "http://www.w3.org/2001/XMLSchema#"
+    },
+    "@graph": [
+        {
+            "@id": "bts:Component",
+            "@type": "rdfs:Class",
+            "rdfs:comment": "TBD",
+            "rdfs:label": "Component",
+            "rdfs:subClassOf": [
+                {
+                    "@id": "bts:Thing"
+                }
+            ],
+            "schema:isPartOf": {
+                "@id": "http://schema.biothings.io"
+            },
+            "sms:displayName": "Component",
+            "sms:required": "sms:true",
+            "sms:validationRules": []
+        },
+        {
+            "@id": "bts:Patient",
+            "@type": "rdfs:Class",
+            "rdfs:comment": "TBD",
+            "rdfs:label": "Patient",
+            "rdfs:subClassOf": [
+                {
+                    "@id": "bts:DataType"
+                }
+            ],
+            "schema:isPartOf": {
+                "@id": "http://schema.biothings.io"
+            },
+            "sms:displayName": "Patient",
+            "sms:required": "sms:false",
+            "sms:requiresDependency": [
+                {
+                    "@id": "bts:PatientID"
+                },
+                {
+                    "@id": "bts:Sex"
+                },
+                {
+                    "@id": "bts:YearofBirth"
+                },
+                {
+                    "@id": "bts:Diagnosis"
+                },
+                {
+                    "@id": "bts:Component"
+                }
+            ],
+            "sms:validationRules": []
+        },
+        {
+            "@id": "bts:PatientID",
+            "@type": "rdfs:Class",
+            "rdfs:comment": "TBD",
+            "rdfs:label": "PatientID",
+            "rdfs:subClassOf": [
+                {
+                    "@id": "bts:DataProperty"
+                }
+            ],
+            "schema:isPartOf": {
+                "@id": "http://schema.biothings.io"
+            },
+            "sms:displayName": "Patient ID",
+            "sms:required": "sms:false",
+            "sms:validationRules": {
+                "Biospecimen": "unique required error",
+                "Patient": "unique warning"
+            }
+        },
+        {
+            "@id": "bts:Sex",
+            "@type": "rdfs:Class",
+            "rdfs:comment": "TBD",
+            "rdfs:label": "Sex",
+            "rdfs:subClassOf": [
+                {
+                    "@id": "bts:DataProperty"
+                }
+            ],
+            "schema:isPartOf": {
+                "@id": "http://schema.biothings.io"
+            },
+            "schema:rangeIncludes": [
+                {
+                    "@id": "bts:Female"
+                },
+                {
+                    "@id": "bts:Male"
+                },
+                {
+                    "@id": "bts:Other"
+                }
+            ],
+            "sms:displayName": "Sex",
+            "sms:required": "sms:true",
+            "sms:validationRules": []
+        },
+        {
+            "@id": "bts:YearofBirth",
+            "@type": "rdfs:Class",
+            "rdfs:comment": "TBD",
+            "rdfs:label": "YearofBirth",
+            "rdfs:subClassOf": [
+                {
+                    "@id": "bts:DataProperty"
+                }
+            ],
+            "schema:isPartOf": {
+                "@id": "http://schema.biothings.io"
+            },
+            "sms:displayName": "Year of Birth",
+            "sms:required": "sms:false",
+            "sms:validationRules": []
+        },
+        {
+            "@id": "bts:Diagnosis",
+            "@type": "rdfs:Class",
+            "rdfs:comment": "TBD",
+            "rdfs:label": "Diagnosis",
+            "rdfs:subClassOf": [
+                {
+                    "@id": "bts:DataProperty"
+                }
+            ],
+            "schema:isPartOf": {
+                "@id": "http://schema.biothings.io"
+            },
+            "schema:rangeIncludes": [
+                {
+                    "@id": "bts:Healthy"
+                },
+                {
+                    "@id": "bts:Cancer"
+                }
+            ],
+            "sms:displayName": "Diagnosis",
+            "sms:required": "sms:true",
+            "sms:validationRules": []
+        },
+        {
+            "@id": "bts:DataType",
+            "@type": "rdfs:Class",
+            "rdfs:comment": "TBD",
+            "rdfs:label": "DataType",
+            "rdfs:subClassOf": [
+                {
+                    "@id": "bts:Thing"
+                }
+            ],
+            "schema:isPartOf": {
+                "@id": "http://schema.biothings.io"
+            },
+            "sms:displayName": "DataType",
+            "sms:required": "sms:false",
+            "sms:validationRules": []
+        },
+        {
+            "@id": "bts:DataProperty",
+            "@type": "rdfs:Class",
+            "rdfs:comment": "TBD",
+            "rdfs:label": "DataProperty",
+            "rdfs:subClassOf": [
+                {
+                    "@id": "bts:Thing"
+                }
+            ],
+            "schema:isPartOf": {
+                "@id": "http://schema.biothings.io"
+            },
+            "sms:displayName": "DataProperty",
+            "sms:required": "sms:false",
+            "sms:validationRules": []
+        },
+        {
+            "@id": "bts:Female",
+            "@type": "rdfs:Class",
+            "rdfs:comment": "TBD",
+            "rdfs:label": "Female",
+            "rdfs:subClassOf": [
+                {
+                    "@id": "bts:Sex"
+                }
+            ],
+            "schema:isPartOf": {
+                "@id": "http://schema.biothings.io"
+            },
+            "sms:displayName": "Female",
+            "sms:required": "sms:false",
+            "sms:validationRules": []
+        },
+        {
+            "@id": "bts:Male",
+            "@type": "rdfs:Class",
+            "rdfs:comment": "TBD",
+            "rdfs:label": "Male",
+            "rdfs:subClassOf": [
+                {
+                    "@id": "bts:Sex"
+                }
+            ],
+            "schema:isPartOf": {
+                "@id": "http://schema.biothings.io"
+            },
+            "sms:displayName": "Male",
+            "sms:required": "sms:false",
+            "sms:validationRules": []
+        },
+        {
+            "@id": "bts:Other",
+            "@type": "rdfs:Class",
+            "rdfs:comment": "TBD",
+            "rdfs:label": "Other",
+            "rdfs:subClassOf": [
+                {
+                    "@id": "bts:Sex"
+                }
+            ],
+            "schema:isPartOf": {
+                "@id": "http://schema.biothings.io"
+            },
+            "sms:displayName": "Other",
+            "sms:required": "sms:false",
+            "sms:validationRules": []
+        },
+        {
+            "@id": "bts:Healthy",
+            "@type": "rdfs:Class",
+            "rdfs:comment": "TBD",
+            "rdfs:label": "Healthy",
+            "rdfs:subClassOf": [
+                {
+                    "@id": "bts:Diagnosis"
+                },
+                {
+                    "@id": "bts:TissueStatus"
+                }
+            ],
+            "schema:isPartOf": {
+                "@id": "http://schema.biothings.io"
+            },
+            "sms:displayName": "Healthy",
+            "sms:required": "sms:false",
+            "sms:validationRules": []
+        },
+        {
+            "@id": "bts:Cancer",
+            "@type": "rdfs:Class",
+            "rdfs:comment": "TBD",
+            "rdfs:label": "Cancer",
+            "rdfs:subClassOf": [
+                {
+                    "@id": "bts:ValidValue"
+                },
+                {
+                    "@id": "bts:Diagnosis"
+                }
+            ],
+            "schema:isPartOf": {
+                "@id": "http://schema.biothings.io"
+            },
+            "sms:displayName": "Cancer",
+            "sms:required": "sms:false",
+            "sms:requiresDependency": [
+                {
+                    "@id": "bts:CancerType"
+                },
+                {
+                    "@id": "bts:FamilyHistory"
+                }
+            ],
+            "sms:validationRules": []
+        },
+        {
+            "@id": "bts:CancerType",
+            "@type": "rdfs:Class",
+            "rdfs:comment": "TBD",
+            "rdfs:label": "CancerType",
+            "rdfs:subClassOf": [
+                {
+                    "@id": "bts:DataProperty"
+                }
+            ],
+            "schema:isPartOf": {
+                "@id": "http://schema.biothings.io"
+            },
+            "schema:rangeIncludes": [
+                {
+                    "@id": "bts:Breast"
+                },
+                {
+                    "@id": "bts:Colorectal"
+                },
+                {
+                    "@id": "bts:Lung"
+                },
+                {
+                    "@id": "bts:Prostate"
+                },
+                {
+                    "@id": "bts:Skin"
+                }
+            ],
+            "sms:displayName": "Cancer Type",
+            "sms:required": "sms:true",
+            "sms:validationRules": []
+        },
+        {
+            "@id": "bts:FamilyHistory",
+            "@type": "rdfs:Class",
+            "rdfs:comment": "TBD",
+            "rdfs:label": "FamilyHistory",
+            "rdfs:subClassOf": [
+                {
+                    "@id": "bts:DataProperty"
+                }
+            ],
+            "schema:isPartOf": {
+                "@id": "http://schema.biothings.io"
+            },
+            "schema:rangeIncludes": [
+                {
+                    "@id": "bts:Breast"
+                },
+                {
+                    "@id": "bts:Colorectal"
+                },
+                {
+                    "@id": "bts:Lung"
+                },
+                {
+                    "@id": "bts:Prostate"
+                },
+                {
+                    "@id": "bts:Skin"
+                }
+            ],
+            "sms:displayName": "Family History",
+            "sms:required": "sms:true",
+            "sms:validationRules": [
+                "list strict"
+            ]
+        },
+        {
+            "@id": "bts:ValidValue",
+            "@type": "rdfs:Class",
+            "rdfs:comment": "TBD",
+            "rdfs:label": "ValidValue",
+            "rdfs:subClassOf": [
+                {
+                    "@id": "bts:Thing"
+                }
+            ],
+            "schema:isPartOf": {
+                "@id": "http://schema.biothings.io"
+            },
+            "sms:displayName": "ValidValue",
+            "sms:required": "sms:false",
+            "sms:validationRules": []
+        },
+        {
+            "@id": "bts:Breast",
+            "@type": "rdfs:Class",
+            "rdfs:comment": "TBD",
+            "rdfs:label": "Breast",
+            "rdfs:subClassOf": [
+                {
+                    "@id": "bts:CancerType"
+                },
+                {
+                    "@id": "bts:FamilyHistory"
+                }
+            ],
+            "schema:isPartOf": {
+                "@id": "http://schema.biothings.io"
+            },
+            "sms:displayName": "Breast",
+            "sms:required": "sms:false",
+            "sms:validationRules": []
+        },
+        {
+            "@id": "bts:Colorectal",
+            "@type": "rdfs:Class",
+            "rdfs:comment": "TBD",
+            "rdfs:label": "Colorectal",
+            "rdfs:subClassOf": [
+                {
+                    "@id": "bts:CancerType"
+                },
+                {
+                    "@id": "bts:FamilyHistory"
+                }
+            ],
+            "schema:isPartOf": {
+                "@id": "http://schema.biothings.io"
+            },
+            "sms:displayName": "Colorectal",
+            "sms:required": "sms:false",
+            "sms:validationRules": []
+        },
+        {
+            "@id": "bts:Lung",
+            "@type": "rdfs:Class",
+            "rdfs:comment": "TBD",
+            "rdfs:label": "Lung",
+            "rdfs:subClassOf": [
+                {
+                    "@id": "bts:CancerType"
+                },
+                {
+                    "@id": "bts:FamilyHistory"
+                }
+            ],
+            "schema:isPartOf": {
+                "@id": "http://schema.biothings.io"
+            },
+            "sms:displayName": "Lung",
+            "sms:required": "sms:false",
+            "sms:validationRules": []
+        },
+        {
+            "@id": "bts:Prostate",
+            "@type": "rdfs:Class",
+            "rdfs:comment": "TBD",
+            "rdfs:label": "Prostate",
+            "rdfs:subClassOf": [
+                {
+                    "@id": "bts:CancerType"
+                },
+                {
+                    "@id": "bts:FamilyHistory"
+                }
+            ],
+            "schema:isPartOf": {
+                "@id": "http://schema.biothings.io"
+            },
+            "sms:displayName": "Prostate",
+            "sms:required": "sms:false",
+            "sms:validationRules": []
+        },
+        {
+            "@id": "bts:Skin",
+            "@type": "rdfs:Class",
+            "rdfs:comment": "TBD",
+            "rdfs:label": "Skin",
+            "rdfs:subClassOf": [
+                {
+                    "@id": "bts:CancerType"
+                },
+                {
+                    "@id": "bts:FamilyHistory"
+                }
+            ],
+            "schema:isPartOf": {
+                "@id": "http://schema.biothings.io"
+            },
+            "sms:displayName": "Skin",
+            "sms:required": "sms:false",
+            "sms:validationRules": []
+        },
+        {
+            "@id": "bts:Biospecimen",
+            "@type": "rdfs:Class",
+            "rdfs:comment": "TBD",
+            "rdfs:label": "Biospecimen",
+            "rdfs:subClassOf": [
+                {
+                    "@id": "bts:DataType"
+                }
+            ],
+            "schema:isPartOf": {
+                "@id": "http://schema.biothings.io"
+            },
+            "sms:displayName": "Biospecimen",
+            "sms:required": "sms:false",
+            "sms:requiresComponent": [
+                {
+                    "@id": "bts:Patient"
+                }
+            ],
+            "sms:requiresDependency": [
+                {
+                    "@id": "bts:SampleID"
+                },
+                {
+                    "@id": "bts:PatientID"
+                },
+                {
+                    "@id": "bts:TissueStatus"
+                },
+                {
+                    "@id": "bts:Component"
+                }
+            ],
+            "sms:validationRules": []
+        },
+        {
+            "@id": "bts:SampleID",
+            "@type": "rdfs:Class",
+            "rdfs:comment": "TBD",
+            "rdfs:label": "SampleID",
+            "rdfs:subClassOf": [
+                {
+                    "@id": "bts:DataProperty"
+                }
+            ],
+            "schema:isPartOf": {
+                "@id": "http://schema.biothings.io"
+            },
+            "sms:displayName": "Sample ID",
+            "sms:required": "sms:true",
+            "sms:validationRules": []
+        },
+        {
+            "@id": "bts:TissueStatus",
+            "@type": "rdfs:Class",
+            "rdfs:comment": "TBD",
+            "rdfs:label": "TissueStatus",
+            "rdfs:subClassOf": [
+                {
+                    "@id": "bts:DataProperty"
+                }
+            ],
+            "schema:isPartOf": {
+                "@id": "http://schema.biothings.io"
+            },
+            "schema:rangeIncludes": [
+                {
+                    "@id": "bts:Healthy"
+                },
+                {
+                    "@id": "bts:Malignant"
+                }
+            ],
+            "sms:displayName": "Tissue Status",
+            "sms:required": "sms:true",
+            "sms:validationRules": []
+        },
+        {
+            "@id": "bts:Malignant",
+            "@type": "rdfs:Class",
+            "rdfs:comment": "TBD",
+            "rdfs:label": "Malignant",
+            "rdfs:subClassOf": [
+                {
+                    "@id": "bts:TissueStatus"
+                }
+            ],
+            "schema:isPartOf": {
+                "@id": "http://schema.biothings.io"
+            },
+            "sms:displayName": "Malignant",
+            "sms:required": "sms:false",
+            "sms:validationRules": []
+        },
+        {
+            "@id": "bts:BulkRNA-seqAssay",
+            "@type": "rdfs:Class",
+            "rdfs:comment": "TBD",
+            "rdfs:label": "BulkRNA-seqAssay",
+            "rdfs:subClassOf": [
+                {
+                    "@id": "bts:DataType"
+                }
+            ],
+            "schema:isPartOf": {
+                "@id": "http://schema.biothings.io"
+            },
+            "sms:displayName": "Bulk RNA-seq Assay",
+            "sms:required": "sms:false",
+            "sms:requiresComponent": [
+                {
+                    "@id": "bts:Biospecimen"
+                }
+            ],
+            "sms:requiresDependency": [
+                {
+                    "@id": "bts:Filename"
+                },
+                {
+                    "@id": "bts:SampleID"
+                },
+                {
+                    "@id": "bts:FileFormat"
+                },
+                {
+                    "@id": "bts:Component"
+                }
+            ],
+            "sms:validationRules": []
+        },
+        {
+            "@id": "bts:Filename",
+            "@type": "rdfs:Class",
+            "rdfs:comment": "TBD",
+            "rdfs:label": "Filename",
+            "rdfs:subClassOf": [
+                {
+                    "@id": "bts:DataProperty"
+                }
+            ],
+            "schema:isPartOf": {
+                "@id": "http://schema.biothings.io"
+            },
+            "sms:displayName": "Filename",
+            "sms:required": "sms:true",
+            "sms:validationRules": []
+        },
+        {
+            "@id": "bts:FileFormat",
+            "@type": "rdfs:Class",
+            "rdfs:comment": "TBD",
+            "rdfs:label": "FileFormat",
+            "rdfs:subClassOf": [
+                {
+                    "@id": "bts:DataProperty"
+                }
+            ],
+            "schema:isPartOf": {
+                "@id": "http://schema.biothings.io"
+            },
+            "schema:rangeIncludes": [
+                {
+                    "@id": "bts:FASTQ"
+                },
+                {
+                    "@id": "bts:BAM"
+                },
+                {
+                    "@id": "bts:CRAM"
+                },
+                {
+                    "@id": "bts:CSV/TSV"
+                }
+            ],
+            "sms:displayName": "File Format",
+            "sms:required": "sms:false",
+            "sms:validationRules": {
+                "BulkRNA-seqAssay": "required"
+            }
+        },
+        {
+            "@id": "bts:FASTQ",
+            "@type": "rdfs:Class",
+            "rdfs:comment": "TBD",
+            "rdfs:label": "FASTQ",
+            "rdfs:subClassOf": [
+                {
+                    "@id": "bts:FileFormat"
+                }
+            ],
+            "schema:isPartOf": {
+                "@id": "http://schema.biothings.io"
+            },
+            "sms:displayName": "FASTQ",
+            "sms:required": "sms:false",
+            "sms:validationRules": []
+        },
+        {
+            "@id": "bts:BAM",
+            "@type": "rdfs:Class",
+            "rdfs:comment": "TBD",
+            "rdfs:label": "BAM",
+            "rdfs:subClassOf": [
+                {
+                    "@id": "bts:ValidValue"
+                },
+                {
+                    "@id": "bts:FileFormat"
+                }
+            ],
+            "schema:isPartOf": {
+                "@id": "http://schema.biothings.io"
+            },
+            "sms:displayName": "BAM",
+            "sms:required": "sms:false",
+            "sms:requiresDependency": [
+                {
+                    "@id": "bts:GenomeBuild"
+                }
+            ],
+            "sms:validationRules": []
+        },
+        {
+            "@id": "bts:CRAM",
+            "@type": "rdfs:Class",
+            "rdfs:comment": "TBD",
+            "rdfs:label": "CRAM",
+            "rdfs:subClassOf": [
+                {
+                    "@id": "bts:ValidValue"
+                },
+                {
+                    "@id": "bts:FileFormat"
+                }
+            ],
+            "schema:isPartOf": {
+                "@id": "http://schema.biothings.io"
+            },
+            "sms:displayName": "CRAM",
+            "sms:required": "sms:false",
+            "sms:requiresDependency": [
+                {
+                    "@id": "bts:GenomeBuild"
+                },
+                {
+                    "@id": "bts:GenomeFASTA"
+                }
+            ],
+            "sms:validationRules": []
+        },
+        {
+            "@id": "bts:CSV/TSV",
+            "@type": "rdfs:Class",
+            "rdfs:comment": "TBD",
+            "rdfs:label": "CSV/TSV",
+            "rdfs:subClassOf": [
+                {
+                    "@id": "bts:ValidValue"
+                },
+                {
+                    "@id": "bts:FileFormat"
+                }
+            ],
+            "schema:isPartOf": {
+                "@id": "http://schema.biothings.io"
+            },
+            "sms:displayName": "CSV/TSV",
+            "sms:required": "sms:false",
+            "sms:requiresDependency": [
+                {
+                    "@id": "bts:GenomeBuild"
+                }
+            ],
+            "sms:validationRules": []
+        },
+        {
+            "@id": "bts:GenomeBuild",
+            "@type": "rdfs:Class",
+            "rdfs:comment": "TBD",
+            "rdfs:label": "GenomeBuild",
+            "rdfs:subClassOf": [
+                {
+                    "@id": "bts:DataProperty"
+                }
+            ],
+            "schema:isPartOf": {
+                "@id": "http://schema.biothings.io"
+            },
+            "schema:rangeIncludes": [
+                {
+                    "@id": "bts:GRCh37"
+                },
+                {
+                    "@id": "bts:GRCh38"
+                },
+                {
+                    "@id": "bts:GRCm38"
+                },
+                {
+                    "@id": "bts:GRCm39"
+                }
+            ],
+            "sms:displayName": "Genome Build",
+            "sms:required": "sms:true",
+            "sms:validationRules": []
+        },
+        {
+            "@id": "bts:GenomeFASTA",
+            "@type": "rdfs:Class",
+            "rdfs:comment": "TBD",
+            "rdfs:label": "GenomeFASTA",
+            "rdfs:subClassOf": [
+                {
+                    "@id": "bts:DataProperty"
+                }
+            ],
+            "schema:isPartOf": {
+                "@id": "http://schema.biothings.io"
+            },
+            "sms:displayName": "Genome FASTA",
+            "sms:required": "sms:true",
+            "sms:validationRules": []
+        },
+        {
+            "@id": "bts:GRCh37",
+            "@type": "rdfs:Class",
+            "rdfs:comment": "TBD",
+            "rdfs:label": "GRCh37",
+            "rdfs:subClassOf": [
+                {
+                    "@id": "bts:GenomeBuild"
+                }
+            ],
+            "schema:isPartOf": {
+                "@id": "http://schema.biothings.io"
+            },
+            "sms:displayName": "GRCh37",
+            "sms:required": "sms:false",
+            "sms:validationRules": []
+        },
+        {
+            "@id": "bts:GRCh38",
+            "@type": "rdfs:Class",
+            "rdfs:comment": "TBD",
+            "rdfs:label": "GRCh38",
+            "rdfs:subClassOf": [
+                {
+                    "@id": "bts:GenomeBuild"
+                }
+            ],
+            "schema:isPartOf": {
+                "@id": "http://schema.biothings.io"
+            },
+            "sms:displayName": "GRCh38",
+            "sms:required": "sms:false",
+            "sms:validationRules": []
+        },
+        {
+            "@id": "bts:GRCm38",
+            "@type": "rdfs:Class",
+            "rdfs:comment": "TBD",
+            "rdfs:label": "GRCm38",
+            "rdfs:subClassOf": [
+                {
+                    "@id": "bts:GenomeBuild"
+                }
+            ],
+            "schema:isPartOf": {
+                "@id": "http://schema.biothings.io"
+            },
+            "sms:displayName": "GRCm38",
+            "sms:required": "sms:false",
+            "sms:validationRules": []
+        },
+        {
+            "@id": "bts:GRCm39",
+            "@type": "rdfs:Class",
+            "rdfs:comment": "TBD",
+            "rdfs:label": "GRCm39",
+            "rdfs:subClassOf": [
+                {
+                    "@id": "bts:GenomeBuild"
+                }
+            ],
+            "schema:isPartOf": {
+                "@id": "http://schema.biothings.io"
+            },
+            "sms:displayName": "GRCm39",
+            "sms:required": "sms:false",
+            "sms:validationRules": []
+        }
+    ],
+    "@id": "http://schema.biothings.io/#0.1"
+}
\ No newline at end of file
diff --git a/tests/data/example_test_nones.model.csv b/tests/data/example_test_nones.model.csv
new file mode 100644
index 000000000..718cee624
--- /dev/null
+++ b/tests/data/example_test_nones.model.csv
@@ -0,0 +1,45 @@
+Attribute,Validation Rules,Description,Valid Values,DependsOn,Properties,Required,Parent,DependsOn Component,Source
+Component,,,,,,TRUE,,,
+Patient,,,,"Patient ID, Sex, Year of Birth, Diagnosis, Component",,FALSE,DataType,,
+Patient ID,#Patient unique warning^^#Biospecimen unique error,,,,,TRUE,DataProperty,,
+Sex,,,"Female, Male, Other",,,TRUE,DataProperty,,
+Year of Birth,,,,,,FALSE,DataProperty,,
+Diagnosis,,,"Healthy, Cancer",,,TRUE,DataProperty,,
+Cancer,,,,"Cancer Type, Family History",,FALSE,ValidValue,,
+Cancer Type,,,"Breast, Colorectal, Lung, Prostate, Skin",,,TRUE,DataProperty,,
+Family History,list strict,,"Breast, Colorectal, Lung, Prostate, Skin",,,TRUE,DataProperty,,
+Biospecimen,,,,"Sample ID, Patient ID, Tissue Status, Component",,FALSE,DataType,Patient,
+Sample ID,,,,,,TRUE,DataProperty,,
+Tissue Status,,,"Healthy, Malignant",,,TRUE,DataProperty,,
+Bulk RNA-seq Assay,,,,"Filename, Sample ID, File Format, Component",,FALSE,DataType,Patient,
+Filename,,,,,,TRUE,DataProperty,,
+File Format,,,"FASTQ, BAM, CRAM, CSV/TSV",,,TRUE,DataProperty,,
+BAM,,,,Genome Build,,FALSE,ValidValue,,
+CRAM,,,,"Genome Build, Genome FASTA",,FALSE,ValidValue,,
+CSV/TSV,,,,Genome Build,,FALSE,ValidValue,,
+Genome Build,,,"GRCh37, GRCh38, GRCm38, GRCm39",,,TRUE,DataProperty,,
+Genome FASTA,,,,,,TRUE,DataProperty,,
+MockComponent,,,,"Component, Check List, Check Regex List, Check Regex Single, Check Regex Format, Check Regex Integer, Check Num, Check Float, Check Int, Check String, Check URL,Check Match at Least, Check Match at Least values, Check Match Exactly, Check Match Exactly values, Check Recommended, Check Ages, Check Unique, Check Range, Check Date, Check NA",,FALSE,DataType,,
+Check List,list strict ,,"ab, cd, ef, gh",,,FALSE,DataProperty,,
+Check Regex List,list strict::regex match [a-f],,,,,FALSE,DataProperty,,
+Check Regex Single,regex search [a-f],,,,,FALSE,DataProperty,,
+Check Regex Format,regex match [a-f],,,,,FALSE,DataProperty,,
+Check Regex Integer,regex search ^\d+$,,,,,FALSE,DataProperty,,
+Check Num,num,,,,,FALSE,DataProperty,,
+Check Float,float,,,,,FALSE,DataProperty,,
+Check Int,int,,,,,FALSE,DataProperty,,
+Check String,str,,,,,FALSE,DataProperty,,
+Check URL,url,,,,,FALSE,DataProperty,,
+Check Match at Least,matchAtLeastOne Patient.PatientID set,,,,,FALSE,DataProperty,,
+Check Match Exactly,matchExactlyOne MockComponent.checkMatchExactly set,,,,,FALSE,DataProperty,,
+Check Match at Least values,matchAtLeastOne MockComponent.checkMatchatLeastvalues value,,,,,FALSE,DataProperty,,
+Check Match Exactly values,matchExactlyOne MockComponent.checkMatchExactlyvalues value,,,,,FALSE,DataProperty,,
+Check Recommended,recommended,,,,,FALSE,DataProperty,,
+Check Ages,protectAges,,,,,FALSE,DataProperty,,
+Check Unique,unique error,,,,,FALSE,DataProperty,,
+Check Range,inRange 50 100 error,,,,,FALSE,DataProperty,,
+Check Date,date,,,,,FALSE,DataProperty,,
+Check NA,int::IsNA,,,,,FALSE,DataProperty,,
+MockRDB,,,,"Component, MockRDB_id, SourceManifest",,FALSE,DataType,,
+MockRDB_id,int,,,,,TRUE,DataProperty,,
+SourceManifest,,,,,,TRUE,DataProperty,,
\ No newline at end of file
diff --git a/tests/data/example_test_nones.model.jsonld b/tests/data/example_test_nones.model.jsonld
new file mode 100644
index 000000000..c47804236
--- /dev/null
+++ b/tests/data/example_test_nones.model.jsonld
@@ -0,0 +1,1473 @@
+{
+    "@context": {
+        "bts": "http://schema.biothings.io/",
+        "rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
+        "rdfs": "http://www.w3.org/2000/01/rdf-schema#",
+        "schema": "http://schema.org/",
+        "xsd": "http://www.w3.org/2001/XMLSchema#"
+    },
+    "@graph": [
+        {
+            "@id": "bts:Component",
+            "@type": "rdfs:Class",
+            "rdfs:comment": "TBD",
+            "rdfs:label": "Component",
+            "rdfs:subClassOf": [
+                {
+                    "@id": "bts:Thing"
+                }
+            ],
+            "schema:isPartOf": {
+                "@id": "http://schema.biothings.io"
+            },
+            "sms:displayName": "Component",
+            "sms:required": "sms:true",
+            "sms:validationRules": []
+        },
+        {
+            "@id": "bts:Patient",
+            "@type": "rdfs:Class",
+            "rdfs:comment": "TBD",
+            "rdfs:label": "Patient",
+            "rdfs:subClassOf": [
+                {
+                    "@id": "bts:DataType"
+                }
+            ],
+            "schema:isPartOf": {
+                "@id": "http://schema.biothings.io"
+            },
+            "sms:displayName": "Patient",
+            "sms:required": "sms:false",
+            "sms:requiresDependency": [
+                {
+                    "@id": "bts:PatientID"
+                },
+                {
+                    "@id": "bts:Sex"
+                },
+                {
+                    "@id": "bts:YearofBirth"
+                },
+                {
+                    "@id": "bts:Diagnosis"
+                },
+                {
+                    "@id": "bts:Component"
+                }
+            ],
+            "sms:validationRules": []
+        },
+        {
+            "@id": "bts:PatientID",
+            "@type": "rdfs:Class",
+            "rdfs:comment": "TBD",
+            "rdfs:label": "PatientID",
+            "rdfs:subClassOf": [
+                {
+                    "@id": "bts:DataProperty"
+                }
+            ],
+            "schema:isPartOf": {
+                "@id": "http://schema.biothings.io"
+            },
+            "sms:displayName": "Patient ID",
+            "sms:required": "sms:true",
+            "sms:validationRules": {
+                "Biospecimen": "unique error",
+                "Patient": "unique warning"
+            }
+        },
+        {
+            "@id": "bts:Sex",
+            "@type": "rdfs:Class",
+            "rdfs:comment": "TBD",
+            "rdfs:label": "Sex",
+            "rdfs:subClassOf": [
+                {
+                    "@id": "bts:DataProperty"
+                }
+            ],
+            "schema:isPartOf": {
+                "@id": "http://schema.biothings.io"
+            },
+            "schema:rangeIncludes": [
+                {
+                    "@id": "bts:Female"
+                },
+                {
+                    "@id": "bts:Male"
+                },
+                {
+                    "@id": "bts:Other"
+                }
+            ],
+            "sms:displayName": "Sex",
+            "sms:required": "sms:true",
+            "sms:validationRules": []
+        },
+        {
+            "@id": "bts:YearofBirth",
+            "@type": "rdfs:Class",
+            "rdfs:comment": "TBD",
+            "rdfs:label": "YearofBirth",
+            "rdfs:subClassOf": [
+                {
+                    "@id": "bts:DataProperty"
+                }
+            ],
+            "schema:isPartOf": {
+                "@id": "http://schema.biothings.io"
+            },
+            "sms:displayName": "Year of Birth",
+            "sms:required": "sms:false",
+            "sms:validationRules": []
+        },
+        {
+            "@id": "bts:Diagnosis",
+            "@type": "rdfs:Class",
+            "rdfs:comment": "TBD",
+            "rdfs:label": "Diagnosis",
+            "rdfs:subClassOf": [
+                {
+                    "@id": "bts:DataProperty"
+                }
+            ],
+            "schema:isPartOf": {
+                "@id": "http://schema.biothings.io"
+            },
+            "schema:rangeIncludes": [
+                {
+                    "@id": "bts:Healthy"
+                },
+                {
+                    "@id": "bts:Cancer"
+                }
+            ],
+            "sms:displayName": "Diagnosis",
+            "sms:required": "sms:true",
+            "sms:validationRules": []
+        },
+        {
+            "@id": "bts:DataType",
+            "@type": "rdfs:Class",
+            "rdfs:comment": "TBD",
+            "rdfs:label": "DataType",
+            "rdfs:subClassOf": [
+                {
+                    "@id": "bts:Thing"
+                }
+            ],
+            "schema:isPartOf": {
+                "@id": "http://schema.biothings.io"
+            },
+            "sms:displayName": "DataType",
+            "sms:required": "sms:false",
+            "sms:validationRules": []
+        },
+        {
+            "@id": "bts:DataProperty",
+            "@type": "rdfs:Class",
+            "rdfs:comment": "TBD",
+            "rdfs:label": "DataProperty",
+            "rdfs:subClassOf": [
+                {
+                    "@id": "bts:Thing"
+                }
+            ],
+            "schema:isPartOf": {
+                "@id": "http://schema.biothings.io"
+            },
+            "sms:displayName": "DataProperty",
+            "sms:required": "sms:false",
+            "sms:validationRules": []
+        },
+        {
+            "@id": "bts:Female",
+            "@type": "rdfs:Class",
+            "rdfs:comment": "TBD",
+            "rdfs:label": "Female",
+            "rdfs:subClassOf": [
+                {
+                    "@id": "bts:Sex"
+                }
+            ],
+            "schema:isPartOf": {
+                "@id": "http://schema.biothings.io"
+            },
+            "sms:displayName": "Female",
+            "sms:required": "sms:false",
+            "sms:validationRules": []
+        },
+        {
+            "@id": "bts:Male",
+            "@type": "rdfs:Class",
+            "rdfs:comment": "TBD",
+            "rdfs:label": "Male",
+            "rdfs:subClassOf": [
+                {
+                    "@id": "bts:Sex"
+                }
+            ],
+            "schema:isPartOf": {
+                "@id": "http://schema.biothings.io"
+            },
+            "sms:displayName": "Male",
+            "sms:required": "sms:false",
+            "sms:validationRules": []
+        },
+        {
+            "@id": "bts:Other",
+            "@type": "rdfs:Class",
+            "rdfs:comment": "TBD",
+            "rdfs:label": "Other",
+            "rdfs:subClassOf": [
+                {
+                    "@id": "bts:Sex"
+                }
+            ],
+            "schema:isPartOf": {
+                "@id": "http://schema.biothings.io"
+            },
+            "sms:displayName": "Other",
+            "sms:required": "sms:false",
+            "sms:validationRules": []
+        },
+        {
+            "@id": "bts:Healthy",
+            "@type": "rdfs:Class",
+            "rdfs:comment": "TBD",
+            "rdfs:label": "Healthy",
+            "rdfs:subClassOf": [
+                {
+                    "@id": "bts:Diagnosis"
+                },
+                {
+                    "@id": "bts:TissueStatus"
+                }
+            ],
+            "schema:isPartOf": {
+                "@id": "http://schema.biothings.io"
+            },
+            "sms:displayName": "Healthy",
+            "sms:required": "sms:false",
+            "sms:validationRules": []
+        },
+        {
+            "@id": "bts:Cancer",
+            "@type": "rdfs:Class",
+            "rdfs:comment": "TBD",
+            "rdfs:label": "Cancer",
+            "rdfs:subClassOf": [
+                {
+                    "@id": "bts:ValidValue"
+                },
+                {
+                    "@id": "bts:Diagnosis"
+                }
+            ],
+            "schema:isPartOf": {
+                "@id": "http://schema.biothings.io"
+            },
+            "sms:displayName": "Cancer",
+            "sms:required": "sms:false",
+            "sms:requiresDependency": [
+                {
+                    "@id": "bts:CancerType"
+                },
+                {
+                    "@id": "bts:FamilyHistory"
+                }
+            ],
+            "sms:validationRules": []
+        },
+        {
+            "@id": "bts:CancerType",
+            "@type": "rdfs:Class",
+            "rdfs:comment": "TBD",
+            "rdfs:label": "CancerType",
+            "rdfs:subClassOf": [
+                {
+                    "@id": "bts:DataProperty"
+                }
+            ],
+            "schema:isPartOf": {
+                "@id": "http://schema.biothings.io"
+            },
+            "schema:rangeIncludes": [
+                {
+                    "@id": "bts:Breast"
+                },
+                {
+                    "@id": "bts:Colorectal"
+                },
+                {
+                    "@id": "bts:Lung"
+                },
+                {
+                    "@id": "bts:Prostate"
+                },
+                {
+                    "@id": "bts:Skin"
+                }
+            ],
+            "sms:displayName": "Cancer Type",
+            "sms:required": "sms:true",
+            "sms:validationRules": []
+        },
+        {
+            "@id": "bts:FamilyHistory",
+            "@type": "rdfs:Class",
+            "rdfs:comment": "TBD",
+            "rdfs:label": "FamilyHistory",
+            "rdfs:subClassOf": [
+                {
+                    "@id": "bts:DataProperty"
+                }
+            ],
+            "schema:isPartOf": {
+                "@id": "http://schema.biothings.io"
+            },
+            "schema:rangeIncludes": [
+                {
+                    "@id": "bts:Breast"
+                },
+                {
+                    "@id": "bts:Colorectal"
+                },
+                {
+                    "@id": "bts:Lung"
+                },
+                {
+                    "@id": "bts:Prostate"
+                },
+                {
+                    "@id": "bts:Skin"
+                }
+            ],
+            "sms:displayName": "Family History",
+            "sms:required": "sms:true",
+            "sms:validationRules": [
+                "list strict"
+            ]
+        },
+        {
+            "@id": "bts:ValidValue",
+            "@type": "rdfs:Class",
+            "rdfs:comment": "TBD",
+            "rdfs:label": "ValidValue",
+            "rdfs:subClassOf": [
+                {
+                    "@id": "bts:Thing"
+                }
+            ],
+            "schema:isPartOf": {
+                "@id": "http://schema.biothings.io"
+            },
+            "sms:displayName": "ValidValue",
+            "sms:required": "sms:false",
+            "sms:validationRules": []
+        },
+        {
+            "@id": "bts:Breast",
+            "@type": "rdfs:Class",
+            "rdfs:comment": "TBD",
+            "rdfs:label": "Breast",
+            "rdfs:subClassOf": [
+                {
+                    "@id": "bts:CancerType"
+                },
+                {
+                    "@id": "bts:FamilyHistory"
+                }
+            ],
+            "schema:isPartOf": {
+                "@id": "http://schema.biothings.io"
+            },
+            "sms:displayName": "Breast",
+            "sms:required": "sms:false",
+            "sms:validationRules": []
+        },
+        {
+            "@id": "bts:Colorectal",
+            "@type": "rdfs:Class",
+            "rdfs:comment": "TBD",
+            "rdfs:label": "Colorectal",
+            "rdfs:subClassOf": [
+                {
+                    "@id": "bts:CancerType"
+                },
+                {
+                    "@id": "bts:FamilyHistory"
+                }
+            ],
+            "schema:isPartOf": {
+                "@id": "http://schema.biothings.io"
+            },
+            "sms:displayName": "Colorectal",
+            "sms:required": "sms:false",
+            "sms:validationRules": []
+        },
+        {
+            "@id": "bts:Lung",
+            "@type": "rdfs:Class",
+            "rdfs:comment": "TBD",
+            "rdfs:label": "Lung",
+            "rdfs:subClassOf": [
+                {
+                    "@id": "bts:CancerType"
+                },
+                {
+                    "@id": "bts:FamilyHistory"
+                }
+            ],
+            "schema:isPartOf": {
+                "@id": "http://schema.biothings.io"
+            },
+            "sms:displayName": "Lung",
+            "sms:required": "sms:false",
+            "sms:validationRules": []
+        },
+        {
+            "@id": "bts:Prostate",
+            "@type": "rdfs:Class",
+            "rdfs:comment": "TBD",
+            "rdfs:label": "Prostate",
+            "rdfs:subClassOf": [
+                {
+                    "@id": "bts:CancerType"
+                },
+                {
+                    "@id": "bts:FamilyHistory"
+                }
+            ],
+            "schema:isPartOf": {
+                "@id": "http://schema.biothings.io"
+            },
+            "sms:displayName": "Prostate",
+            "sms:required": "sms:false",
+            "sms:validationRules": []
+        },
+        {
+            "@id": "bts:Skin",
+            "@type": "rdfs:Class",
+            "rdfs:comment": "TBD",
+            "rdfs:label": "Skin",
+            "rdfs:subClassOf": [
+                {
+                    "@id": "bts:CancerType"
+                },
+                {
+                    "@id": "bts:FamilyHistory"
+                }
+            ],
+            "schema:isPartOf": {
+                "@id": "http://schema.biothings.io"
+            },
+            "sms:displayName": "Skin",
+            "sms:required": "sms:false",
+            "sms:validationRules": []
+        },
+        {
+            "@id": "bts:Biospecimen",
+            "@type": "rdfs:Class",
+            "rdfs:comment": "TBD",
+            "rdfs:label": "Biospecimen",
+            "rdfs:subClassOf": [
+                {
+                    "@id": "bts:DataType"
+                }
+            ],
+            "schema:isPartOf": {
+                "@id": "http://schema.biothings.io"
+            },
+            "sms:displayName": "Biospecimen",
+            "sms:required": "sms:false",
+            "sms:requiresComponent": [
+                {
+                    "@id": "bts:Patient"
+                }
+            ],
+            "sms:requiresDependency": [
+                {
+                    "@id": "bts:SampleID"
+                },
+                {
+                    "@id": "bts:PatientID"
+                },
+                {
+                    "@id": "bts:TissueStatus"
+                },
+                {
+                    "@id": "bts:Component"
+                }
+            ],
+            "sms:validationRules": []
+        },
+        {
+            "@id": "bts:SampleID",
+            "@type": "rdfs:Class",
+            "rdfs:comment": "TBD",
+            "rdfs:label": "SampleID",
+            "rdfs:subClassOf": [
+                {
+                    "@id": "bts:DataProperty"
+                }
+            ],
+            "schema:isPartOf": {
+                "@id": "http://schema.biothings.io"
+            },
+            "sms:displayName": "Sample ID",
+            "sms:required": "sms:true",
+            "sms:validationRules": []
+        },
+        {
+            "@id": "bts:TissueStatus",
+            "@type": "rdfs:Class",
+            "rdfs:comment": "TBD",
+            "rdfs:label": "TissueStatus",
+            "rdfs:subClassOf": [
+                {
+                    "@id": "bts:DataProperty"
+                }
+            ],
+            "schema:isPartOf": {
+                "@id": "http://schema.biothings.io"
+            },
+            "schema:rangeIncludes": [
+                {
+                    "@id": "bts:Healthy"
+                },
+                {
+                    "@id": "bts:Malignant"
+                }
+            ],
+            "sms:displayName": "Tissue Status",
+            "sms:required": "sms:true",
+            "sms:validationRules": []
+        },
+        {
+            "@id": "bts:Malignant",
+            "@type": "rdfs:Class",
+            "rdfs:comment": "TBD",
+            "rdfs:label": "Malignant",
+            "rdfs:subClassOf": [
+                {
+                    "@id": "bts:TissueStatus"
+                }
+            ],
+            "schema:isPartOf": {
+                "@id": "http://schema.biothings.io"
+            },
+            "sms:displayName": "Malignant",
+            "sms:required": "sms:false",
+            "sms:validationRules": []
+        },
+        {
+            "@id": "bts:BulkRNA-seqAssay",
+            "@type": "rdfs:Class",
+            "rdfs:comment": "TBD",
+            "rdfs:label": "BulkRNA-seqAssay",
+            "rdfs:subClassOf": [
+                {
+                    "@id": "bts:DataType"
+                }
+            ],
+            "schema:isPartOf": {
+                "@id": "http://schema.biothings.io"
+            },
+            "sms:displayName": "Bulk RNA-seq Assay",
+            "sms:required": "sms:false",
+            "sms:requiresComponent": [
+                {
+                    "@id": "bts:Patient"
+                }
+            ],
+            "sms:requiresDependency": [
+                {
+                    "@id": "bts:Filename"
+                },
+                {
+                    "@id": "bts:SampleID"
+                },
+                {
+                    "@id": "bts:FileFormat"
+                },
+                {
+                    "@id": "bts:Component"
+                }
+            ],
+            "sms:validationRules": []
+        },
+        {
+            "@id": "bts:Filename",
+            "@type": "rdfs:Class",
+            "rdfs:comment": "TBD",
+            "rdfs:label": "Filename",
+            "rdfs:subClassOf": [
+                {
+                    "@id": "bts:DataProperty"
+                }
+            ],
+            "schema:isPartOf": {
+                "@id": "http://schema.biothings.io"
+            },
+            "sms:displayName": "Filename",
+            "sms:required": "sms:true",
+            "sms:validationRules": []
+        },
+        {
+            "@id": "bts:FileFormat",
+            "@type": "rdfs:Class",
+            "rdfs:comment": "TBD",
+            "rdfs:label": "FileFormat",
+            "rdfs:subClassOf": [
+                {
+                    "@id": "bts:DataProperty"
+                }
+            ],
+            "schema:isPartOf": {
+                "@id": "http://schema.biothings.io"
+            },
+            "schema:rangeIncludes": [
+                {
+                    "@id": "bts:FASTQ"
+                },
+                {
+                    "@id": "bts:BAM"
+                },
+                {
+                    "@id": "bts:CRAM"
+                },
+                {
+                    "@id": "bts:CSV/TSV"
+                }
+            ],
+            "sms:displayName": "File Format",
+            "sms:required": "sms:true",
+            "sms:validationRules": []
+        },
+        {
+            "@id": "bts:FASTQ",
+            "@type": "rdfs:Class",
+            "rdfs:comment": "TBD",
+            "rdfs:label": "FASTQ",
+            "rdfs:subClassOf": [
+                {
+                    "@id": "bts:FileFormat"
+                }
+            ],
+            "schema:isPartOf": {
+                "@id": "http://schema.biothings.io"
+            },
+            "sms:displayName": "FASTQ",
+            "sms:required": "sms:false",
+            "sms:validationRules": []
+        },
+        {
+            "@id": "bts:BAM",
+            "@type": "rdfs:Class",
+            "rdfs:comment": "TBD",
+            "rdfs:label": "BAM",
+            "rdfs:subClassOf": [
+                {
+                    "@id": "bts:ValidValue"
+                },
+                {
+                    "@id": "bts:FileFormat"
+                }
+            ],
+            "schema:isPartOf": {
+                "@id": "http://schema.biothings.io"
+            },
+            "sms:displayName": "BAM",
+            "sms:required": "sms:false",
+            "sms:requiresDependency": [
+                {
+                    "@id": "bts:GenomeBuild"
+                }
+            ],
+            "sms:validationRules": []
+        },
+        {
+            "@id": "bts:CRAM",
+            "@type": "rdfs:Class",
+            "rdfs:comment": "TBD",
+            "rdfs:label": "CRAM",
+            "rdfs:subClassOf": [
+                {
+                    "@id": "bts:ValidValue"
+                },
+                {
+                    "@id": "bts:FileFormat"
+                }
+            ],
+            "schema:isPartOf": {
+                "@id": "http://schema.biothings.io"
+            },
+            "sms:displayName": "CRAM",
+            "sms:required": "sms:false",
+            "sms:requiresDependency": [
+                {
+                    "@id": "bts:GenomeBuild"
+                },
+                {
+                    "@id": "bts:GenomeFASTA"
+                }
+            ],
+            "sms:validationRules": []
+        },
+        {
+            "@id": "bts:CSV/TSV",
+            "@type": "rdfs:Class",
+            "rdfs:comment": "TBD",
+            "rdfs:label": "CSV/TSV",
+            "rdfs:subClassOf": [
+                {
+                    "@id": "bts:ValidValue"
+                },
+                {
+                    "@id": "bts:FileFormat"
+                }
+            ],
+            "schema:isPartOf": {
+                "@id": "http://schema.biothings.io"
+            },
+            "sms:displayName": "CSV/TSV",
+            "sms:required": "sms:false",
+            "sms:requiresDependency": [
+                {
+                    "@id": "bts:GenomeBuild"
+                }
+            ],
+            "sms:validationRules": []
+        },
+        {
+            "@id": "bts:GenomeBuild",
+            "@type": "rdfs:Class",
+            "rdfs:comment": "TBD",
+            "rdfs:label": "GenomeBuild",
+            "rdfs:subClassOf": [
+                {
+                    "@id": "bts:DataProperty"
+                }
+            ],
+            "schema:isPartOf": {
+                "@id": "http://schema.biothings.io"
+            },
+            "schema:rangeIncludes": [
+                {
+                    "@id": "bts:GRCh37"
+                },
+                {
+                    "@id": "bts:GRCh38"
+                },
+                {
+                    "@id": "bts:GRCm38"
+                },
+                {
+                    "@id": "bts:GRCm39"
+                }
+            ],
+            "sms:displayName": "Genome Build",
+            "sms:required": "sms:true",
+            "sms:validationRules": []
+        },
+        {
+            "@id": "bts:GenomeFASTA",
+            "@type": "rdfs:Class",
+            "rdfs:comment": "TBD",
+            "rdfs:label": "GenomeFASTA",
+            "rdfs:subClassOf": [
+                {
+                    "@id": "bts:DataProperty"
+                }
+            ],
+            "schema:isPartOf": {
+                "@id": "http://schema.biothings.io"
+            },
+            "sms:displayName": "Genome FASTA",
+            "sms:required": "sms:true",
+            "sms:validationRules": []
+        },
+        {
+            "@id": "bts:GRCh37",
+            "@type": "rdfs:Class",
+            "rdfs:comment": "TBD",
+            "rdfs:label": "GRCh37",
+            "rdfs:subClassOf": [
+                {
+                    "@id": "bts:GenomeBuild"
+                }
+            ],
+            "schema:isPartOf": {
+                "@id": "http://schema.biothings.io"
+            },
+            "sms:displayName": "GRCh37",
+            "sms:required": "sms:false",
+            "sms:validationRules": []
+        },
+        {
+            "@id": "bts:GRCh38",
+            "@type": "rdfs:Class",
+            "rdfs:comment": "TBD",
+            "rdfs:label": "GRCh38",
+            "rdfs:subClassOf": [
+                {
+                    "@id": "bts:GenomeBuild"
+                }
+            ],
+            "schema:isPartOf": {
+                "@id": "http://schema.biothings.io"
+            },
+            "sms:displayName": "GRCh38",
+            "sms:required": "sms:false",
+            "sms:validationRules": []
+        },
+        {
+            "@id": "bts:GRCm38",
+            "@type": "rdfs:Class",
+            "rdfs:comment": "TBD",
+            "rdfs:label": "GRCm38",
+            "rdfs:subClassOf": [
+                {
+                    "@id": "bts:GenomeBuild"
+                }
+            ],
+            "schema:isPartOf": {
+                "@id": "http://schema.biothings.io"
+            },
+            "sms:displayName": "GRCm38",
+            "sms:required": "sms:false",
+            "sms:validationRules": []
+        },
+        {
+            "@id": "bts:GRCm39",
+            "@type": "rdfs:Class",
+            "rdfs:comment": "TBD",
+            "rdfs:label": "GRCm39",
+            "rdfs:subClassOf": [
+                {
+                    "@id": "bts:GenomeBuild"
+                }
+            ],
+            "schema:isPartOf": {
+                "@id": "http://schema.biothings.io"
+            },
+            "sms:displayName": "GRCm39",
+            "sms:required": "sms:false",
+            "sms:validationRules": []
+        },
+        {
+            "@id": "bts:MockComponent",
+            "@type": "rdfs:Class",
+            "rdfs:comment": "TBD",
+            "rdfs:label": "MockComponent",
+            "rdfs:subClassOf": [
+                {
+                    "@id": "bts:DataType"
+                }
+            ],
+            "schema:isPartOf": {
+                "@id": "http://schema.biothings.io"
+            },
+            "sms:displayName": "MockComponent",
+            "sms:required": "sms:false",
+            "sms:requiresDependency": [
+                {
+                    "@id": "bts:Component"
+                },
+                {
+                    "@id": "bts:CheckList"
+                },
+                {
+                    "@id": "bts:CheckRegexList"
+                },
+                {
+                    "@id": "bts:CheckRegexSingle"
+                },
+                {
+                    "@id": "bts:CheckRegexFormat"
+                },
+                {
+                    "@id": "bts:CheckRegexInteger"
+                },
+                {
+                    "@id": "bts:CheckNum"
+                },
+                {
+                    "@id": "bts:CheckFloat"
+                },
+                {
+                    "@id": "bts:CheckInt"
+                },
+                {
+                    "@id": "bts:CheckString"
+                },
+                {
+                    "@id": "bts:CheckURL"
+                },
+                {
+                    "@id": "bts:CheckMatchatLeast"
+                },
+                {
+                    "@id": "bts:CheckMatchatLeastvalues"
+                },
+                {
+                    "@id": "bts:CheckMatchExactly"
+                },
+                {
+                    "@id": "bts:CheckMatchExactlyvalues"
+                },
+                {
+                    "@id": "bts:CheckRecommended"
+                },
+                {
+                    "@id": "bts:CheckAges"
+                },
+                {
+                    "@id": "bts:CheckUnique"
+                },
+                {
+                    "@id": "bts:CheckRange"
+                },
+                {
+                    "@id": "bts:CheckDate"
+                },
+                {
+                    "@id": "bts:CheckNA"
+                }
+            ],
+            "sms:validationRules": []
+        },
+        {
+            "@id": "bts:CheckList",
+            "@type": "rdfs:Class",
+            "rdfs:comment": "TBD",
+            "rdfs:label": "CheckList",
+            "rdfs:subClassOf": [
+                {
+                    "@id": "bts:DataProperty"
+                }
+            ],
+            "schema:isPartOf": {
+                "@id": "http://schema.biothings.io"
+            },
+            "schema:rangeIncludes": [
+                {
+                    "@id": "bts:Ab"
+                },
+                {
+                    "@id": "bts:Cd"
+                },
+                {
+                    "@id": "bts:Ef"
+                },
+                {
+                    "@id": "bts:Gh"
+                }
+            ],
+            "sms:displayName": "Check List",
+            "sms:required": "sms:false",
+            "sms:validationRules": [
+                "list strict"
+            ]
+        },
+        {
+            "@id": "bts:CheckRegexList",
+            "@type": "rdfs:Class",
+            "rdfs:comment": "TBD",
+            "rdfs:label": "CheckRegexList",
+            "rdfs:subClassOf": [
+                {
+                    "@id": "bts:DataProperty"
+                }
+            ],
+            "schema:isPartOf": {
+                "@id": "http://schema.biothings.io"
+            },
+            "sms:displayName": "Check Regex List",
+            "sms:required": "sms:false",
+            "sms:validationRules": [
+                "list strict",
+                "regex match [a-f]"
+            ]
+        },
+        {
+            "@id": "bts:CheckRegexSingle",
+            "@type": "rdfs:Class",
+            "rdfs:comment": "TBD",
+            "rdfs:label": "CheckRegexSingle",
+            "rdfs:subClassOf": [
+                {
+                    "@id": "bts:DataProperty"
+                }
+            ],
+            "schema:isPartOf": {
+                "@id": "http://schema.biothings.io"
+            },
+            "sms:displayName": "Check Regex Single",
+            "sms:required": "sms:false",
+            "sms:validationRules": [
+                "regex search [a-f]"
+            ]
+        },
+        {
+            "@id": "bts:CheckRegexFormat",
+            "@type": "rdfs:Class",
+            "rdfs:comment": "TBD",
+            "rdfs:label": "CheckRegexFormat",
+            "rdfs:subClassOf": [
+                {
+                    "@id": "bts:DataProperty"
+                }
+            ],
+            "schema:isPartOf": {
+                "@id": "http://schema.biothings.io"
+            },
+            "sms:displayName": "Check Regex Format",
+            "sms:required": "sms:false",
+            "sms:validationRules": [
+                "regex match [a-f]"
+            ]
+        },
+        {
+            "@id": "bts:CheckRegexInteger",
+            "@type": "rdfs:Class",
+            "rdfs:comment": "TBD",
+            "rdfs:label": "CheckRegexInteger",
+            "rdfs:subClassOf": [
+                {
+                    "@id": "bts:DataProperty"
+                }
+            ],
+            "schema:isPartOf": {
+                "@id": "http://schema.biothings.io"
+            },
+            "sms:displayName": "Check Regex Integer",
+            "sms:required": "sms:false",
+            "sms:validationRules": [
+                "regex search ^\\d+$"
+            ]
+        },
+        {
+            "@id": "bts:CheckNum",
+            "@type": "rdfs:Class",
+            "rdfs:comment": "TBD",
+            "rdfs:label": "CheckNum",
+            "rdfs:subClassOf": [
+                {
+                    "@id": "bts:DataProperty"
+                }
+            ],
+            "schema:isPartOf": {
+                "@id": "http://schema.biothings.io"
+            },
+            "sms:displayName": "Check Num",
+            "sms:required": "sms:false",
+            "sms:validationRules": [
+                "num"
+            ]
+        },
+        {
+            "@id": "bts:CheckFloat",
+            "@type": "rdfs:Class",
+            "rdfs:comment": "TBD",
+            "rdfs:label": "CheckFloat",
+            "rdfs:subClassOf": [
+                {
+                    "@id": "bts:DataProperty"
+                }
+            ],
+            "schema:isPartOf": {
+                "@id": "http://schema.biothings.io"
+            },
+            "sms:displayName": "Check Float",
+            "sms:required": "sms:false",
+            "sms:validationRules": [
+                "float"
+            ]
+        },
+        {
+            "@id": "bts:CheckInt",
+            "@type": "rdfs:Class",
+            "rdfs:comment": "TBD",
+            "rdfs:label": "CheckInt",
+            "rdfs:subClassOf": [
+                {
+                    "@id": "bts:DataProperty"
+                }
+            ],
+            "schema:isPartOf": {
+                "@id": "http://schema.biothings.io"
+            },
+            "sms:displayName": "Check Int",
+            "sms:required": "sms:false",
+            "sms:validationRules": [
+                "int"
+            ]
+        },
+        {
+            "@id": "bts:CheckString",
+            "@type": "rdfs:Class",
+            "rdfs:comment": "TBD",
+            "rdfs:label": "CheckString",
+            "rdfs:subClassOf": [
+                {
+                    "@id": "bts:DataProperty"
+                }
+            ],
+            "schema:isPartOf": {
+                "@id": "http://schema.biothings.io"
+            },
+            "sms:displayName": "Check String",
+            "sms:required": "sms:false",
+            "sms:validationRules": [
+                "str"
+            ]
+        },
+        {
+            "@id": "bts:CheckURL",
+            "@type": "rdfs:Class",
+            "rdfs:comment": "TBD",
+            "rdfs:label": "CheckURL",
+            "rdfs:subClassOf": [
+                {
+                    "@id": "bts:DataProperty"
+                }
+            ],
+            "schema:isPartOf": {
+                "@id": "http://schema.biothings.io"
+            },
+            "sms:displayName": "Check URL",
+            "sms:required": "sms:false",
+            "sms:validationRules": [
+                "url"
+            ]
+        },
+        {
+            "@id": "bts:CheckMatchatLeast",
+            "@type": "rdfs:Class",
+            "rdfs:comment": "TBD",
+            "rdfs:label": "CheckMatchatLeast",
+            "rdfs:subClassOf": [
+                {
+                    "@id": "bts:DataProperty"
+                }
+            ],
+            "schema:isPartOf": {
+                "@id": "http://schema.biothings.io"
+            },
+            "sms:displayName": "Check Match at Least",
+            "sms:required": "sms:false",
+            "sms:validationRules": [
+                "matchAtLeastOne Patient.PatientID set"
+            ]
+        },
+        {
+            "@id": "bts:CheckMatchatLeastvalues",
+            "@type": "rdfs:Class",
+            "rdfs:comment": "TBD",
+            "rdfs:label": "CheckMatchatLeastvalues",
+            "rdfs:subClassOf": [
+                {
+                    "@id": "bts:DataProperty"
+                }
+            ],
+            "schema:isPartOf": {
+                "@id": "http://schema.biothings.io"
+            },
+            "sms:displayName": "Check Match at Least values",
+            "sms:required": "sms:false",
+            "sms:validationRules": [
+                "matchAtLeastOne MockComponent.checkMatchatLeastvalues value"
+            ]
+        },
+        {
+            "@id": "bts:CheckMatchExactly",
+            "@type": "rdfs:Class",
+            "rdfs:comment": "TBD",
+            "rdfs:label": "CheckMatchExactly",
+            "rdfs:subClassOf": [
+                {
+                    "@id": "bts:DataProperty"
+                }
+            ],
+            "schema:isPartOf": {
+                "@id": "http://schema.biothings.io"
+            },
+            "sms:displayName": "Check Match Exactly",
+            "sms:required": "sms:false",
+            "sms:validationRules": [
+                "matchExactlyOne MockComponent.checkMatchExactly set"
+            ]
+        },
+        {
+            "@id": "bts:CheckMatchExactlyvalues",
+            "@type": "rdfs:Class",
+            "rdfs:comment": "TBD",
+            "rdfs:label": "CheckMatchExactlyvalues",
+            "rdfs:subClassOf": [
+                {
+                    "@id": "bts:DataProperty"
+                }
+            ],
+            "schema:isPartOf": {
+                "@id": "http://schema.biothings.io"
+            },
+            "sms:displayName": "Check Match Exactly values",
+            "sms:required": "sms:false",
+            "sms:validationRules": [
+                "matchExactlyOne MockComponent.checkMatchExactlyvalues value"
+            ]
+        },
+        {
+            "@id": "bts:CheckRecommended",
+            "@type": "rdfs:Class",
+            "rdfs:comment": "TBD",
+            "rdfs:label": "CheckRecommended",
+            "rdfs:subClassOf": [
+                {
+                    "@id": "bts:DataProperty"
+                }
+            ],
+            "schema:isPartOf": {
+                "@id": "http://schema.biothings.io"
+            },
+            "sms:displayName": "Check Recommended",
+            "sms:required": "sms:false",
+            "sms:validationRules": [
+                "recommended"
+            ]
+        },
+        {
+            "@id": "bts:CheckAges",
+            "@type": "rdfs:Class",
+            "rdfs:comment": "TBD",
+            "rdfs:label": "CheckAges",
+            "rdfs:subClassOf": [
+                {
+                    "@id": "bts:DataProperty"
+                }
+            ],
+            "schema:isPartOf": {
+                "@id": "http://schema.biothings.io"
+            },
+            "sms:displayName": "Check Ages",
+            "sms:required": "sms:false",
+            "sms:validationRules": [
+                "protectAges"
+            ]
+        },
+        {
+            "@id": "bts:CheckUnique",
+            "@type": "rdfs:Class",
+            "rdfs:comment": "TBD",
+            "rdfs:label": "CheckUnique",
+            "rdfs:subClassOf": [
+                {
+                    "@id": "bts:DataProperty"
+                }
+            ],
+            "schema:isPartOf": {
+                "@id": "http://schema.biothings.io"
+            },
+            "sms:displayName": "Check Unique",
+            "sms:required": "sms:false",
+            "sms:validationRules": [
+                "unique error"
+            ]
+        },
+        {
+            "@id": "bts:CheckRange",
+            "@type": "rdfs:Class",
+            "rdfs:comment": "TBD",
+            "rdfs:label": "CheckRange",
+            "rdfs:subClassOf": [
+                {
+                    "@id": "bts:DataProperty"
+                }
+            ],
+            "schema:isPartOf": {
+                "@id": "http://schema.biothings.io"
+            },
+            "sms:displayName": "Check Range",
+            "sms:required": "sms:false",
+            "sms:validationRules": [
+                "inRange 50 100 error"
+            ]
+        },
+        {
+            "@id": "bts:CheckDate",
+            "@type": "rdfs:Class",
+            "rdfs:comment": "TBD",
+            "rdfs:label": "CheckDate",
+            "rdfs:subClassOf": [
+                {
+                    "@id": "bts:DataProperty"
+                }
+            ],
+            "schema:isPartOf": {
+                "@id": "http://schema.biothings.io"
+            },
+            "sms:displayName": "Check Date",
+            "sms:required": "sms:false",
+            "sms:validationRules": [
+                "date"
+            ]
+        },
+        {
+            "@id": "bts:CheckNA",
+            "@type": "rdfs:Class",
+            "rdfs:comment": "TBD",
+            "rdfs:label": "CheckNA",
+            "rdfs:subClassOf": [
+                {
+                    "@id": "bts:DataProperty"
+                }
+            ],
+            "schema:isPartOf": {
+                "@id": "http://schema.biothings.io"
+            },
+            "sms:displayName": "Check NA",
+            "sms:required": "sms:false",
+            "sms:validationRules": [
+                "int",
+                "IsNA"
+            ]
+        },
+        {
+            "@id": "bts:Ab",
+            "@type": "rdfs:Class",
+            "rdfs:comment": "TBD",
+            "rdfs:label": "Ab",
+            "rdfs:subClassOf": [
+                {
+                    "@id": "bts:CheckList"
+                }
+            ],
+            "schema:isPartOf": {
+                "@id": "http://schema.biothings.io"
+            },
+            "sms:displayName": "ab",
+            "sms:required": "sms:false",
+            "sms:validationRules": []
+        },
+        {
+            "@id": "bts:Cd",
+            "@type": "rdfs:Class",
+            "rdfs:comment": "TBD",
+            "rdfs:label": "Cd",
+            "rdfs:subClassOf": [
+                {
+                    "@id": "bts:CheckList"
+                }
+            ],
+            "schema:isPartOf": {
+                "@id": "http://schema.biothings.io"
+            },
+            "sms:displayName": "cd",
+            "sms:required": "sms:false",
+            "sms:validationRules": []
+        },
+        {
+            "@id": "bts:Ef",
+            "@type": "rdfs:Class",
+            "rdfs:comment": "TBD",
+            "rdfs:label": "Ef",
+            "rdfs:subClassOf": [
+                {
+                    "@id": "bts:CheckList"
+                }
+            ],
+            "schema:isPartOf": {
+                "@id": "http://schema.biothings.io"
+            },
+            "sms:displayName": "ef",
+            "sms:required": "sms:false",
+            "sms:validationRules": []
+        },
+        {
+            "@id": "bts:Gh",
+            "@type": "rdfs:Class",
+            "rdfs:comment": "TBD",
+            "rdfs:label": "Gh",
+            "rdfs:subClassOf": [
+                {
+                    "@id": "bts:CheckList"
+                }
+            ],
+            "schema:isPartOf": {
+                "@id": "http://schema.biothings.io"
+            },
+            "sms:displayName": "gh",
+            "sms:required": "sms:false",
+            "sms:validationRules": []
+        },
+        {
+            "@id": "bts:MockRDB",
+            "@type": "rdfs:Class",
+            "rdfs:comment": "TBD",
+            "rdfs:label": "MockRDB",
+            "rdfs:subClassOf": [
+                {
+                    "@id": "bts:DataType"
+                }
+            ],
+            "schema:isPartOf": {
+                "@id": "http://schema.biothings.io"
+            },
+            "sms:displayName": "MockRDB",
+            "sms:required": "sms:false",
+            "sms:requiresDependency": [
+                {
+                    "@id": "bts:Component"
+                },
+                {
+                    "@id": "bts:MockRDBId"
+                },
+                {
+                    "@id": "bts:SourceManifest"
+                }
+            ],
+            "sms:validationRules": []
+        },
+        {
+            "@id": "bts:MockRDBId",
+            "@type": "rdfs:Class",
+            "rdfs:comment": "TBD",
+            "rdfs:label": "MockRDBId",
+            "rdfs:subClassOf": [
+                {
+                    "@id": "bts:DataProperty"
+                }
+            ],
+            "schema:isPartOf": {
+                "@id": "http://schema.biothings.io"
+            },
+            "sms:displayName": "MockRDB_id",
+            "sms:required": "sms:true",
+            "sms:validationRules": [
+                "int"
+            ]
+        },
+        {
+            "@id": "bts:SourceManifest",
+            "@type": "rdfs:Class",
+            "rdfs:comment": "TBD",
+            "rdfs:label": "SourceManifest",
+            "rdfs:subClassOf": [
+                {
+                    "@id": "bts:DataProperty"
+                }
+            ],
+            "schema:isPartOf": {
+                "@id": "http://schema.biothings.io"
+            },
+            "sms:displayName": "SourceManifest",
+            "sms:required": "sms:true",
+            "sms:validationRules": []
+        }
+    ],
+    "@id": "http://schema.biothings.io/#0.1"
+}
\ No newline at end of file
diff --git a/tests/data/mock_manifests/Biospecimen_required_vr_test_fail.manifest.csv b/tests/data/mock_manifests/Biospecimen_required_vr_test_fail.manifest.csv
new file mode 100644
index 000000000..bfacc736a
--- /dev/null
+++ b/tests/data/mock_manifests/Biospecimen_required_vr_test_fail.manifest.csv
@@ -0,0 +1,2 @@
+Sample ID,Patient ID,Tissue Status,Component
+jldkj,,Healthy,Biospecimen
\ No newline at end of file
diff --git a/tests/data/mock_manifests/Biospecimen_required_vr_test_pass.manifest.csv b/tests/data/mock_manifests/Biospecimen_required_vr_test_pass.manifest.csv
new file mode 100644
index 000000000..31557c4f6
--- /dev/null
+++ b/tests/data/mock_manifests/Biospecimen_required_vr_test_pass.manifest.csv
@@ -0,0 +1,2 @@
+Sample ID,Patient ID,Tissue Status,Component
+jldkj,lkjdkl,Healthy,Biospecimen
\ No newline at end of file
diff --git a/tests/data/mock_manifests/BulkRNAseq_component_based_required_rule_test.manifest.csv b/tests/data/mock_manifests/BulkRNAseq_component_based_required_rule_test.manifest.csv
new file mode 100644
index 000000000..d0cd7a01c
--- /dev/null
+++ b/tests/data/mock_manifests/BulkRNAseq_component_based_required_rule_test.manifest.csv
@@ -0,0 +1,4 @@
+Filename,Sample ID,File Format,Component,Genome Build,Genome FASTA
+test/file/name.png,fake_id,BAM,BulkRNA-seqAssay,GRCh37,
+test/file/name_2.png,fake_id_2,CRAM,BulkRNA-seqAssay,GRCh38,
+test/file/name_3.png,fake_id_3,,BulkRNA-seqAssay,,
\ No newline at end of file
diff --git a/tests/data/mock_manifests/Invalid_Test_Manifest.csv b/tests/data/mock_manifests/Invalid_Test_Manifest.csv
index 1cc8995ef..fcdc4d97b 100644
--- a/tests/data/mock_manifests/Invalid_Test_Manifest.csv
+++ b/tests/data/mock_manifests/Invalid_Test_Manifest.csv
@@ -1,5 +1,5 @@
-Component,Check List,Check Regex List,Check Regex Single,Check Regex Format,Check Regex Integer,Check Num,Check Float,Check Int,Check String,Check URL,Check Match at Least,Check Match at Least values,Check Match Exactly,Check Match Exactly values,Check Recommended,Check Ages,Check Unique,Check Range,Check Date,Check NA
-MockComponent,"ab,cd","ab,cd,ef",a,a,5.4,6,99.65,7,valid,https://www.google.com/,1738,1738,8085,98085,,6549,str1,70,32-984,7
-MockComponent,invalid list values,ab cd ef,q,m,0,c,99,5.63,94,http://googlef.com/,7163,51100,9965,71738,,32851,str1,30,notADate,9.5
-MockComponent,"ab,cd","ab,cd,ef",b,b,683902,6.5,62.3,2,valid,https://github.com/Sage-Bionetworks/schematic,8085,8085,1738,210065,,6550,str1,90,84-43-094,Not Applicable
-,,,,,,,,,,,,,,,,,,,
\ No newline at end of file
+Component,Check List,Check Regex List,Check Regex Single,Check Regex Format,Check Regex Integer,Check Num,Check Float,Check Int,Check String,Check URL,Check Match at Least,Check Match at Least values,Check Match Exactly,Check Match Exactly values,Check Match None,Check Match None values,Check Recommended,Check Ages,Check Unique,Check Range,Check Date,Check NA
+MockComponent,"ab,cd","ab,cd,ef",a,a,5.4,6,99.65,7,valid,https://www.google.com/,1738,1738,8085,98085,1911,334,,6549,str1,70,32-984,7
+MockComponent,invalid list values,ab cd ef,q,m,0,c,99,5.63,94,http://googlef.com/,7163,51100,9965,71738,123,717,,32851,str1,30,notADate,9.5
+MockComponent,"ab,cd","ab,cd,ef",b,b,683902,6.5,62.3,2,valid,https://github.com/Sage-Bionetworks/schematic,8085,8085,1738,210065,1427,123,,6550,str1,90,84-43-094,Not Applicable
+,,,,,,,,,,,,,,,,,,,,,
\ No newline at end of file
diff --git a/tests/data/mock_manifests/Patient_required_vr_test_pass.manifest.csv b/tests/data/mock_manifests/Patient_required_vr_test_pass.manifest.csv
new file mode 100644
index 000000000..f9c68cd58
--- /dev/null
+++ b/tests/data/mock_manifests/Patient_required_vr_test_pass.manifest.csv
@@ -0,0 +1,2 @@
+Patient ID,Sex,Year of Birth,Diagnosis,Component,Cancer Type,Family History
+,Female,,Cancer,Patient,Colorectal,"Colorectal, Breast"
\ No newline at end of file
diff --git a/tests/data/mock_manifests/Patient_test_no_entry_for_cond_required_column.manifest.csv b/tests/data/mock_manifests/Patient_test_no_entry_for_cond_required_column.manifest.csv
new file mode 100644
index 000000000..7452c9660
--- /dev/null
+++ b/tests/data/mock_manifests/Patient_test_no_entry_for_cond_required_column.manifest.csv
@@ -0,0 +1,4 @@
+Patient ID,Sex,Year of Birth,Diagnosis,Component,Cancer Type,Family History
+Kdj;lkjdl,Male,,Healthy,Patient,,
+kdie,Female,,Healthy,Patient,,
+kjdl,Male,,Healthy,Patient,,
\ No newline at end of file
diff --git a/tests/data/mock_manifests/Valid_Test_Manifest.csv b/tests/data/mock_manifests/Valid_Test_Manifest.csv
index c4b6fb01f..7a4e53671 100644
--- a/tests/data/mock_manifests/Valid_Test_Manifest.csv
+++ b/tests/data/mock_manifests/Valid_Test_Manifest.csv
@@ -1,5 +1,5 @@
-Component,Check List,Check Regex List,Check Regex Single,Check Regex Format,Check Regex Integer,Check Num,Check Float,Check Int,Check String,Check URL,Check Match at Least,Check Match at Least values,Check Match Exactly,Check Match Exactly values,Check Recommended,Check Ages,Check Unique,Check Range,Check Date,Check NA
-MockComponent,"ab,cd","a,c,f",a,a,0,6,99.65,7,valid,https://www.google.com/,1738,1738,8085,8085,,6571,str1,75,10/21/2022,Not Applicable
-MockComponent,"ab,cd","a,c,f",e,b,1234,71,58.4,3,valid,https://www.google.com/,9965,9965,9965,9965,,6571,str2,80,October 21 2022,8
-MockComponent,"ab,cd","b,d,f",b,c,683902,6.5,62.3,2,valid,https://www.google.com/,8085,8085,1738,1738,present,32849,str3,95,10/21/2022,Not Applicable
-MockComponent,"ab,cd","b,d,f",b,c,0,6.5,62.3,2,valid,https://www.google.com/,79,79,7,7,,32849,str4,55,21/10/2022,695
\ No newline at end of file
+Component,Check List,Check Regex List,Check Regex Single,Check Regex Format,Check Regex Integer,Check Num,Check Float,Check Int,Check String,Check URL,Check Match at Least,Check Match at Least values,Check Match Exactly,Check Match Exactly values,Check Match None,Check Match None values,Check Recommended,Check Ages,Check Unique,Check Range,Check Date,Check NA
+MockComponent,"ab,cd","a,c,f",a,a,0,6,99.65,7,valid,https://www.google.com/,1738,1738,8085,8085,1911,334,,6571,str1,75,10/21/2022,Not Applicable
+MockComponent,"ab,cd","a,c,f",e,b,1234,71,58.4,3,valid,https://www.google.com/,9965,9965,9965,9965,1655,717,,6571,str2,80,October 21 2022,8
+MockComponent,"ab,cd","b,d,f",b,c,683902,6.5,62.3,2,valid,https://www.google.com/,8085,8085,1738,1738,1427,206,present,32849,str3,95,10/21/2022,Not Applicable
+MockComponent,"ab,cd","b,d,f",b,c,0,6.5,62.3,2,valid,https://www.google.com/,79,79,7,7,6635,608,,32849,str4,55,21/10/2022,695
\ No newline at end of file
diff --git a/tests/data/mock_manifests/Valid_Test_Manifest_with_nones.csv b/tests/data/mock_manifests/Valid_Test_Manifest_with_nones.csv
new file mode 100644
index 000000000..091ac8c0a
--- /dev/null
+++ b/tests/data/mock_manifests/Valid_Test_Manifest_with_nones.csv
@@ -0,0 +1,5 @@
+Component,Check List,Check Regex List,Check Regex Single,Check Regex Format,Check Regex Integer,Check Num,Check Float,Check Int,Check String,Check URL,Check Match at Least,Check Match at Least values,Check Match Exactly,Check Match Exactly values,Check Recommended,Check Ages,Check Unique,Check Range,Check Date,Check NA
+MockComponent,"ab,cd","a,c,f",a,a,,6,99.65,7,valid,https://www.google.com/,1738,1738,8085,8085,,6571,str1,75,10/21/2022,Not Applicable
+MockComponent,"ab,cd","a,c,f",,b,1234,71,58.4,3,valid,https://www.google.com/,9965,9965,9965,9965,,6571,str2,80,October 21 2022,8
+MockComponent,,,b,,683902,,,,,,,,,,present,,,,,
+MockComponent,"ab,cd","b,d,f",b,c,0,6.5,62.3,2,valid,https://www.google.com/,79,79,7,7,,32849,str4,55,21/10/2022,695
\ No newline at end of file
diff --git a/tests/test_api.py b/tests/test_api.py
index e2dc2ad5f..f8ff09d4a 100644
--- a/tests/test_api.py
+++ b/tests/test_api.py
@@ -78,16 +78,13 @@ def test_manifest_json(helpers):
 
 @pytest.fixture(scope="class")
 def data_model_jsonld():
-    # data_model_jsonld ="https://raw.githubusercontent.com/Sage-Bionetworks/schematic/develop/tests/data/example.model.jsonld"
-    data_model_jsonld = "https://raw.githubusercontent.com/mialy-defelice/data_models/main/example.model.jsonld"
+    data_model_jsonld ="https://raw.githubusercontent.com/Sage-Bionetworks/schematic/develop/tests/data/example.model.jsonld"
     yield data_model_jsonld
 
 
 @pytest.fixture(scope="class")
 def benchmark_data_model_jsonld():
-    # benchmark_data_model_jsonld = "https://raw.githubusercontent.com/Sage-Bionetworks/schematic/develop/tests/data/example.single_rule.model.jsonld"
-    # Placeholder till the model is updated on develop
-    benchmark_data_model_jsonld = "https://raw.githubusercontent.com/mialy-defelice/data_models/main/example.single_rule.model.jsonld"
+    benchmark_data_model_jsonld = "https://raw.githubusercontent.com/Sage-Bionetworks/schematic/develop/tests/data/example.single_rule.model.jsonld"
     yield benchmark_data_model_jsonld
 
 
@@ -96,8 +93,7 @@ def get_MockComponent_attribute():
     Yield all of the mock conponent attributes one at a time
     TODO: pull in jsonld from fixture
     """
-    # schema_url = "https://raw.githubusercontent.com/Sage-Bionetworks/schematic/develop/tests/data/example.single_rule.model.jsonld"
-    schema_url = "https://raw.githubusercontent.com/mialy-defelice/data_models/main/example.single_rule.model.jsonld"
+    schema_url = "https://raw.githubusercontent.com/Sage-Bionetworks/schematic/develop/tests/data/example.single_rule.model.jsonld"
     data_model_parser = DataModelParser(path_to_data_model=schema_url)
     # Parse Model
     parsed_data_model = data_model_parser.parse_model()
@@ -106,10 +102,10 @@ def get_MockComponent_attribute():
     data_model_grapher = DataModelGraph(parsed_data_model)
 
     # Generate graph
-    graph_data_model = data_model_grapher.generate_data_model_graph()
+    graph_data_model = data_model_grapher.graph
 
     dmge = DataModelGraphExplorer(graph_data_model)
-    # sg = SchemaGenerator("https://raw.githubusercontent.com/Sage-Bionetworks/schematic/develop/tests/data/example.single_rule.model.jsonld")
+
     attributes = dmge.get_node_dependencies("MockComponent")
     attributes.remove("Component")
 
@@ -1090,7 +1086,7 @@ def test_submit_manifest_file_only_replace(
             elif python_version == "3.9":
                 dataset_id = "syn52656104"
 
-            specific_params = {"asset_view": "syn23643253", "dataset_id": dataset_id}
+            specific_params = {"asset_view": "syn23643253", "dataset_id": dataset_id, "project_scope":["syn54126707"]}
 
         params.update(specific_params)
 
diff --git a/tests/test_cli.py b/tests/test_cli.py
index 4631e9a9c..308f9c73f 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -1,13 +1,15 @@
 import os
 
 import pytest
+from unittest.mock import patch
 
 from click.testing import CliRunner
 
-# from schematic import init
 from schematic.schemas.commands import schema
 from schematic.manifest.commands import manifest
+from schematic.models.commands import model
 from schematic.configuration.configuration import Configuration
+from tests.conftest import Helpers
 
 
 @pytest.fixture
@@ -49,10 +51,18 @@ def test_schema_convert_cli(self, runner, helpers):
 
         output_path = helpers.get_data_path("example.model.jsonld")
 
-        label_type = 'class_label'
+        label_type = "class_label"
 
         result = runner.invoke(
-            schema, ["convert", data_model_csv_path, "--output_jsonld", output_path, "--data_model_labels", label_type]
+            schema,
+            [
+                "convert",
+                data_model_csv_path,
+                "--output_jsonld",
+                output_path,
+                "--data_model_labels",
+                label_type,
+            ],
         )
 
         assert result.exit_code == 0
@@ -139,3 +149,41 @@ def test_get_example_manifest_excel(
 
         assert result.exit_code == 0
         self.assert_expected_file(result, output_path)
+
+    @pytest.mark.parametrize("with_annotations", [True, False])
+    def test_submit_file_based_manifest(
+        self,
+        runner: CliRunner,
+        helpers: Helpers,
+        with_annotations: bool,
+        config: Configuration,
+    ) -> None:
+        manifest_path = helpers.get_data_path("mock_manifests/test_BulkRNAseq.csv")
+        config.load_config("config_example.yml")
+        config.synapse_master_fileview_id = "syn1234"
+
+        if with_annotations:
+            annotation_opt = "-fa"
+        else:
+            annotation_opt = "-no-fa"
+
+        with patch("schematic.models.metadata.MetadataModel.submit_metadata_manifest"):
+            result = runner.invoke(
+                model,
+                [
+                    "-c",
+                    config.config_path,
+                    "submit",
+                    "-mrt",
+                    "file_only",
+                    "-d",
+                    "syn12345",
+                    "-vc",
+                    "BulkRNA-seqAssay",
+                    "-mp",
+                    manifest_path,
+                    annotation_opt,
+                ],
+            )
+
+            assert result.exit_code == 0
diff --git a/tests/test_manifest.py b/tests/test_manifest.py
index cc00a2140..0525b6c6a 100644
--- a/tests/test_manifest.py
+++ b/tests/test_manifest.py
@@ -34,7 +34,7 @@ def generate_graph_data_model(helpers, path_to_data_model, data_model_labels):
     data_model_grapher = DataModelGraph(parsed_data_model, data_model_labels)
 
     # Generate graph
-    graph_data_model = data_model_grapher.generate_data_model_graph()
+    graph_data_model = data_model_grapher.graph
 
     return graph_data_model
 
diff --git a/tests/test_metadata.py b/tests/test_metadata.py
index 9bc5e5ae5..7807e1129 100644
--- a/tests/test_metadata.py
+++ b/tests/test_metadata.py
@@ -1,9 +1,14 @@
-import os
 import logging
+import os
+import shutil
+from typing import Optional, Generator
+from pathlib import Path
+from unittest.mock import patch
 
 import pytest
 
 from schematic.models.metadata import MetadataModel
+from tests.conftest import Helpers
 
 logging.basicConfig(level=logging.DEBUG)
 logger = logging.getLogger(__name__)
@@ -19,6 +24,28 @@ def metadata_model(helpers, data_model_labels):
     return metadata_model
 
 
+@pytest.fixture
+def test_bulkrnaseq(helpers: Helpers) -> Generator[Path, None, None]:
+    """create temporary copy of test_BulkRNAseq.csv
+    This fixture creates a temporary copy of the original 'test_BulkRNAseq.csv' file
+    After test, the copied file is removed.
+    Args:
+        helpers (Helpers): Helpers fixture
+
+    Yields:
+        Generator[Path, None, None]: temporary file path of the copied version test_BulkRNAseq.csv
+    """
+    # original bulkrnaseq csv
+    original_test_path = helpers.get_data_path("mock_manifests/test_BulkRNAseq.csv")
+    # Copy the original CSV file to a temporary directory
+    temp_csv_path = helpers.get_data_path("mock_manifests/test_BulkRNAseq2.csv")
+    shutil.copyfile(original_test_path, temp_csv_path)
+    yield temp_csv_path
+    # Teardown
+    if os.path.exists(temp_csv_path):
+        os.remove(temp_csv_path)
+
+
 class TestMetadataModel:
     @pytest.mark.parametrize("as_graph", [True, False], ids=["as_graph", "as_list"])
     @pytest.mark.parametrize(
@@ -93,3 +120,43 @@ def test_populate_manifest(self, helpers, return_excel, data_model_labels):
             os.remove(output_path)
         except:
             pass
+
+    @pytest.mark.parametrize("file_annotations_upload", [True, False])
+    @pytest.mark.parametrize("restrict_rules", [True, False])
+    @pytest.mark.parametrize("hide_blanks", [True, False])
+    @pytest.mark.parametrize(
+        "data_model_labels",
+        ["display_label", "class_label"],
+        ids=["data_model_labels-display_label", "data_model_labels-class_label"],
+    )
+    @pytest.mark.parametrize("validate_component", [None, "BulkRNA-seqAssay"])
+    def test_submit_metadata_manifest(
+        self,
+        test_bulkrnaseq: Path,
+        helpers: Helpers,
+        file_annotations_upload: bool,
+        restrict_rules: bool,
+        data_model_labels: str,
+        hide_blanks: bool,
+        validate_component: Optional[str],
+    ) -> None:
+        meta_data_model = metadata_model(helpers, data_model_labels)
+        with patch(
+            "schematic.models.metadata.MetadataModel.validateModelManifest",
+            return_value=([], []),
+        ):
+            with patch(
+                "schematic.store.synapse.SynapseStorage.associateMetadataWithFiles",
+                return_value="mock manifest id",
+            ):
+                mock_manifest_path = test_bulkrnaseq
+                mock_manifest_id = meta_data_model.submit_metadata_manifest(
+                    manifest_path=mock_manifest_path,
+                    validate_component=validate_component,
+                    dataset_id="mock dataset id",
+                    manifest_record_type="file_only",
+                    restrict_rules=restrict_rules,
+                    file_annotations_upload=file_annotations_upload,
+                    hide_blanks=hide_blanks,
+                )
+                assert mock_manifest_id == "mock manifest id"
diff --git a/tests/test_schemas.py b/tests/test_schemas.py
index 54b93fd84..89fe54af8 100644
--- a/tests/test_schemas.py
+++ b/tests/test_schemas.py
@@ -7,6 +7,7 @@
 import pandas as pd
 import pytest
 import random
+from typing import Optional
 
 from schematic.schemas.data_model_edges import DataModelEdges
 from schematic.schemas.data_model_nodes import DataModelNodes
@@ -18,7 +19,7 @@
     get_attribute_display_name_from_label,
     convert_bool_to_str,
     parse_validation_rules,
-    DisplayLabelType
+    DisplayLabelType,
 )
 from schematic.utils.io_utils import load_json
 
@@ -80,7 +81,9 @@ def get_data_model_parser(
 
 
 def generate_graph_data_model(
-    helpers, data_model_name: str, data_model_labels: DisplayLabelType = "class_label",
+    helpers,
+    data_model_name: str,
+    data_model_labels: DisplayLabelType = "class_label",
 ) -> nx.MultiDiGraph:
     """
     Simple helper function to generate a networkx graph data model from a CSV or JSONLD data model
@@ -99,13 +102,15 @@ def generate_graph_data_model(
     data_model_grapher = DataModelGraph(parsed_data_model, data_model_labels)
 
     # Generate graph
-    graph_data_model = data_model_grapher.generate_data_model_graph()
+    graph_data_model = data_model_grapher.graph
 
     return graph_data_model
 
 
 def generate_data_model_nodes(
-    helpers, data_model_name: str, data_model_labels: DisplayLabelType = "class_label",
+    helpers,
+    data_model_name: str,
+    data_model_labels: DisplayLabelType = "class_label",
 ) -> DataModelNodes:
     # Instantiate Parser
     data_model_parser = get_data_model_parser(
@@ -475,6 +480,100 @@ def test_find_node_range(self):
     def test_get_adjacent_nodes_by_relationship(self):
         return
 
+    @pytest.mark.parametrize(
+        "data_model",
+        ["example_required_vr_test.model.csv", "example_required_vr_test.model.jsonld"],
+        ids=["csv", "jsonld"],
+    )
+    @pytest.mark.parametrize(
+        ["manifest_component", "node_required"],
+        [
+            ["Patient", False],
+            ["Biospecimen", True],
+        ],
+        ids=["Patient_Component", "Biospecimen_Component"],
+    )
+    @pytest.mark.parametrize(
+        ["node_label", "node_display_name"],
+        [["PatientID", None], [None, "Patient ID"]],
+        ids=["use_node_label", "use_node_display_name"],
+    )
+    @pytest.mark.parametrize(
+        "provide_vrs",
+        [True, False],
+        ids=["provide_vrs_True", "provide_vrs_False"],
+    )
+    def test_get_component_node_required(
+        self,
+        helpers,
+        data_model: str,
+        manifest_component: str,
+        node_required: bool,
+        provide_vrs: bool,
+        node_label: str,
+        node_display_name: str,
+    ):
+        # Get graph explorer
+        DMGE = helpers.get_data_model_graph_explorer(path=data_model)
+
+        # Get validation rules, if indicated to do so
+        node_validation_rules = None
+        if provide_vrs:
+            node_validation_rules = DMGE.get_component_node_validation_rules(
+                manifest_component=manifest_component,
+                node_label=node_label,
+                node_display_name=node_display_name,
+            )
+
+        # Find if component node is required then compare to expectations
+        component_node_required = DMGE.get_component_node_required(
+            manifest_component=manifest_component,
+            node_validation_rules=node_validation_rules,
+            node_label=node_label,
+            node_display_name=node_display_name,
+        )
+
+        assert component_node_required == node_required
+
+    @pytest.mark.parametrize(
+        "data_model",
+        ["example_required_vr_test.model.csv", "example_required_vr_test.model.jsonld"],
+        ids=["csv", "jsonld"],
+    )
+    @pytest.mark.parametrize(
+        ["manifest_component", "node_vrs"],
+        [
+            ["Patient", "unique warning"],
+            ["Biospecimen", "unique required error"],
+        ],
+        ids=["Patient_Component", "Biospecimen_Component"],
+    )
+    @pytest.mark.parametrize(
+        ["node_label", "node_display_name"],
+        [["PatientID", None], [None, "Patient ID"]],
+        ids=["use_node_label", "use_node_display_name"],
+    )
+    def test_get_component_node_validation_rules(
+        self,
+        helpers,
+        data_model: str,
+        manifest_component: str,
+        node_vrs: str,
+        node_label: str,
+        node_display_name: str,
+    ):
+        # Get graph explorer
+        DMGE = helpers.get_data_model_graph_explorer(path=data_model)
+
+        # Get component node validation rules and compare to expectations
+        node_validation_rules = DMGE.get_component_node_validation_rules(
+            manifest_component=manifest_component,
+            node_label=node_label,
+            node_display_name=node_display_name,
+        )
+
+        assert node_validation_rules == [node_vrs]
+
     def test_get_component_requirements(self):
         return
 
@@ -532,6 +631,11 @@ def test_find_parent_classes(self):
     def test_full_schema_graph(self):
         return
 
+    @pytest.mark.parametrize(
+        "data_model",
+        ["example.model.csv", "example.model.jsonld"],
+        ids=["csv", "jsonld"],
+    )
     @pytest.mark.parametrize(
         "class_name, expected_in_schema",
         [
@@ -541,13 +645,15 @@ def test_full_schema_graph(self):
             ("InvalidComponent", False),
         ],
     )
-    def test_is_class_in_schema(self, helpers, class_name, expected_in_schema):
+    def test_is_class_in_schema(
+        self, helpers, class_name, expected_in_schema, data_model
+    ):
         """
         Test to cover checking if a given class is in a schema.
         `is_class_in_schema` should return `True` if the class is in the schema
         and `False` if it is not.
         """
-        DMGE = helpers.get_data_model_graph_explorer(path="example.model.csv")
+        DMGE = helpers.get_data_model_graph_explorer(path=data_model)
         # Check if class is in schema
         class_in_schema = DMGE.is_class_in_schema(class_name)
 
@@ -786,9 +892,12 @@ def test_run_rel_functions(self, helpers, data_model, rel_func, test_dn, test_bo
                 for ind, rule in enumerate(vrs):
                     if "::" in rule[0]:
                         assert parsed_vrs[ind] == rule[0].split("::")
-                    elif '^^' in rule[0]:
+                    elif "^^" in rule[0]:
                         component_rule_sets = rule[0].split("^^")
-                        components = [cr.split(' ')[0].replace('#', '') for cr in component_rule_sets]
+                        components = [
+                            cr.split(" ")[0].replace("#", "")
+                            for cr in component_rule_sets
+                        ]
                         assert components == [k for k in parsed_vrs[0].keys()]
                     else:
                         assert parsed_vrs[ind] == rule
@@ -1256,7 +1365,7 @@ def test_init(self, helpers, data_model):
         )
 
         # Instantiate DataModelJsonLD
-        data_model_jsonld = DataModelJsonLD(Graph=graph_data_model)
+        data_model_jsonld = DataModelJsonLD(graph=graph_data_model)
 
         # Test that __init__ is being set up properly
         assert type(data_model_jsonld.graph) == nx.MultiDiGraph
@@ -1338,7 +1447,7 @@ def test_fill_entry_template(
         )
 
         # Instantiate DataModelJsonLD
-        data_model_jsonld = DataModelJsonLD(Graph=graph_data_model)
+        data_model_jsonld = DataModelJsonLD(graph=graph_data_model)
 
         # Get empty template
         if template_type == "property":
@@ -1411,7 +1520,7 @@ def test_add_contexts_to_entries(self, helpers, data_model, template_type):
         )
 
         # Instantiate DataModelJsonLD
-        data_model_jsonld = DataModelJsonLD(Graph=graph_data_model)
+        data_model_jsonld = DataModelJsonLD(graph=graph_data_model)
 
         # Get empty template
         if template_type == "property":
@@ -1447,7 +1556,7 @@ def test_clean_template(
         )
 
         # Instantiate DataModelJsonLD
-        data_model_jsonld = DataModelJsonLD(Graph=graph_data_model)
+        data_model_jsonld = DataModelJsonLD(graph=graph_data_model)
 
         # Get empty template
         class_template = ClassTemplate()
@@ -1493,7 +1602,7 @@ def test_reorder_template_entries(self, helpers, data_model, valid_values):
         )
 
         # Instantiate DataModelJsonLD
-        data_model_jsonld = DataModelJsonLD(Graph=graph_data_model)
+        data_model_jsonld = DataModelJsonLD(graph=graph_data_model)
 
         # Get empty template
         class_template = ClassTemplate()
@@ -1532,7 +1641,7 @@ def test_generate_jsonld_object(self, helpers, data_model):
         )
 
         # Instantiate DataModelJsonLD
-        data_model_jsonld = DataModelJsonLD(Graph=graph_data_model)
+        data_model_jsonld = DataModelJsonLD(graph=graph_data_model)
         jsonld_dm = data_model_jsonld.generate_jsonld_object()
 
         assert list(jsonld_dm.keys()) == ["@context", "@graph", "@id"]
@@ -1548,6 +1657,6 @@ def test_convert_graph_to_jsonld(self, helpers, data_model):
         )
 
         # Generate JSONLD
-        jsonld_dm = convert_graph_to_jsonld(Graph=graph_data_model)
+        jsonld_dm = convert_graph_to_jsonld(graph=graph_data_model)
         assert list(jsonld_dm.keys()) == ["@context", "@graph", "@id"]
         assert len(jsonld_dm["@graph"]) > 1
diff --git a/tests/test_store.py b/tests/test_store.py
index ee24d9642..c2038db76 100644
--- a/tests/test_store.py
+++ b/tests/test_store.py
@@ -1,28 +1,33 @@
+"""Tests for store module"""
+
 from __future__ import annotations
 
 import logging
 import math
 import os
 from time import sleep
-from unittest.mock import Mock, patch
+from typing import Generator, Any
+from unittest.mock import patch
+import shutil
 
 import pandas as pd
 import pytest
-from pandas.testing import assert_frame_equal
 from synapseclient import EntityViewSchema, Folder
-from synapseclient.core.exceptions import SynapseHTTPError
 from synapseclient.entity import File
+from pandas.testing import assert_frame_equal
 
-from schematic.schemas.data_model_parser import DataModelParser
+from schematic.configuration.configuration import Configuration
 from schematic.schemas.data_model_graph import DataModelGraph, DataModelGraphExplorer
-from schematic.schemas.data_model_relationships import DataModelRelationships
+from schematic.schemas.data_model_parser import DataModelParser
+from tests.conftest import Helpers
 
-from schematic.models.metadata import MetadataModel
 from schematic.store.base import BaseStorage
 from schematic.store.synapse import (
     DatasetFileView,
     ManifestDownload,
+    SynapseStorage
 )
+from schematic.utils.general import check_synapse_cache_size
 
 logging.basicConfig(level=logging.DEBUG)
 logger = logging.getLogger(__name__)
@@ -81,6 +86,38 @@ def datasetId(synapse_store, projectId, helpers):
     yield datasetId
 
 
+@pytest.fixture
+def dmge(
+    helpers: Helpers, config: Configuration
+) -> Generator[DataModelGraphExplorer, None, None]:
+    """initiate data model explorer
+
+    Args:
+        helpers (pytest fixture): fixture
+        config (Configuration): configuration class
+
+    Yields:
+        DataModelGraphExplorer
+    """
+    # associate org FollowUp metadata with files
+    input_model_location = helpers.get_data_path(
+        os.path.basename(config.model_location)
+    )
+    data_model_parser = DataModelParser(path_to_data_model=input_model_location)
+    # Parse Model
+    parsed_data_model = data_model_parser.parse_model()
+
+    # Instantiate DataModelGraph
+    data_model_grapher = DataModelGraph(parsed_data_model)
+
+    # Generate graph
+    graph_data_model = data_model_grapher.graph
+
+    # Instantiate DataModelGraphExplorer
+    dmge = DataModelGraphExplorer(graph_data_model)
+    yield dmge
+
+
 def raise_final_error(retry_state):
     return retry_state.outcome.result()
 
@@ -92,11 +129,35 @@ def test_init(self):
 
 
 class TestSynapseStorage:
-    def test_init(self, synapse_store):
+    "Tests the SynapseStorage class"
+
+    def test_init(self, synapse_store:SynapseStorage) -> None:
+        """Tests SynapseStorage.__init__"""
         assert synapse_store.storageFileview == "syn23643253"
         assert isinstance(synapse_store.storageFileviewTable, pd.DataFrame)
-
-    def test_getFileAnnotations(self, synapse_store):
+        assert synapse_store.root_synapse_cache.endswith(".synapseCache")
+
+    def test__purge_synapse_cache(self) -> None:
+        """Tests SynapseStorage._purge_synapse_cache"""
+        synapse_store = SynapseStorage(synapse_cache_path="test_cache_dir")
+        size_before_purge = check_synapse_cache_size(synapse_store.root_synapse_cache)
+        synapse_store._purge_synapse_cache(
+            maximum_storage_allowed_cache_gb=0.000001,
+            minute_buffer=0
+        )
+        size_after_purge = check_synapse_cache_size(synapse_store.root_synapse_cache)
+        assert size_before_purge > size_after_purge
+        shutil.rmtree("test_cache_dir")
+
+    def test_login(self) -> None:
+        """Tests SynapseStorage.login"""
+        synapse_client = SynapseStorage.login()
+        assert synapse_client.cache.cache_root_dir.endswith(".synapseCache")
+        synapse_client = SynapseStorage.login("test_cache_dir")
+        assert synapse_client.cache.cache_root_dir == "test_cache_dir"
+        shutil.rmtree("test_cache_dir")
+
+    def test_getFileAnnotations(self, synapse_store:SynapseStorage) -> None:
         expected_dict = {
             "author": "bruno, milen, sujay",
             "impact": "42.9",
@@ -179,24 +240,8 @@ def test_annotation_submission(
         datasetId,
         manifest_record_type,
         config: Configuration,
+        dmge: DataModelGraphExplorer,
     ):
-        # Upload dataset annotations
-
-        # Instantiate DataModelParser
-        data_model_parser = DataModelParser(path_to_data_model=config.model_location)
-
-        # Parse Model
-        parsed_data_model = data_model_parser.parse_model()
-
-        # Instantiate DataModelGraph
-        data_model_grapher = DataModelGraph(parsed_data_model)
-
-        # Generate graph
-        graph_data_model = data_model_grapher.generate_data_model_graph()
-
-        # Instantiate DataModelGraphExplorer
-        dmge = DataModelGraphExplorer(graph_data_model)
-
         manifest_id = synapse_store.associateMetadataWithFiles(
             dmge=dmge,
             metadataManifestPath=helpers.get_data_path(manifest_path),
@@ -297,7 +342,7 @@ def test_getFilesInStorageDataset(self, synapse_store, full_path, expected):
                 [("test_file_2", "syn125")],
             ),
         ]
-        with patch("synapseutils.walk_functions._helpWalk", return_value=mock_return):
+        with patch("synapseutils.walk_functions._help_walk", return_value=mock_return):
             file_list = synapse_store.getFilesInStorageDataset(
                 datasetId="syn_mock", fileNames=None, fullpath=full_path
             )
@@ -437,7 +482,6 @@ def test_get_files_metadata_from_dataset(self, synapse_store):
                     "entityId": ["syn123", "syn456"],
                 }
 
-
 class TestDatasetFileView:
     def test_init(self, dataset_id, dataset_fileview, synapse_store):
         assert dataset_fileview.datasetId == dataset_id
@@ -525,6 +569,7 @@ def test_createTable(
         datasetId,
         table_column_names,
         annotation_keys,
+        dmge: DataModelGraphExplorer,
     ):
         table_manipulation = None
 
@@ -544,25 +589,6 @@ def test_createTable(
 
         # associate metadata with files
         manifest_path = "mock_manifests/table_manifest.csv"
-        inputModelLocaiton = helpers.get_data_path(
-            os.path.basename(config.model_location)
-        )
-
-        # Instantiate DataModelParser
-        data_model_parser = DataModelParser(path_to_data_model=inputModelLocaiton)
-
-        # Parse Model
-        parsed_data_model = data_model_parser.parse_model()
-
-        # Instantiate DataModelGraph
-        data_model_grapher = DataModelGraph(parsed_data_model)
-
-        # Generate graph
-        graph_data_model = data_model_grapher.generate_data_model_graph()
-
-        # Instantiate DataModelGraphExplorer
-        dmge = DataModelGraphExplorer(graph_data_model)
-
         # updating file view on synapse takes a long time
         manifestId = synapse_store.associateMetadataWithFiles(
             dmge=dmge,
@@ -582,7 +608,6 @@ def test_createTable(
         # assert table exists
         assert table_name in existing_tables.keys()
 
-
     @pytest.mark.parametrize(
         "table_column_names",
         ["display_label", "class_label"],
@@ -602,6 +627,7 @@ def test_replaceTable(
         datasetId,
         table_column_names,
         annotation_keys,
+        dmge: DataModelGraphExplorer,
     ):
         table_manipulation = "replace"
 
@@ -622,25 +648,6 @@ def test_replaceTable(
                 not in synapse_store.get_table_info(projectId=projectId).keys()
             )
 
-        # associate org FollowUp metadata with files
-        inputModelLocaiton = helpers.get_data_path(
-            os.path.basename(config.model_location)
-        )
-        # sg = SchemaGenerator(inputModelLocaiton)
-
-        data_model_parser = DataModelParser(path_to_data_model=inputModelLocaiton)
-        # Parse Model
-        parsed_data_model = data_model_parser.parse_model()
-
-        # Instantiate DataModelGraph
-        data_model_grapher = DataModelGraph(parsed_data_model)
-
-        # Generate graph
-        graph_data_model = data_model_grapher.generate_data_model_graph()
-
-        # Instantiate DataModelGraphExplorer
-        dmge = DataModelGraphExplorer(graph_data_model)
-
         # updating file view on synapse takes a long time
         manifestId = synapse_store.associateMetadataWithFiles(
             dmge=dmge,
@@ -693,7 +700,6 @@ def test_replaceTable(
         # delete table
         synapse_store.syn.delete(tableId)
 
-
     @pytest.mark.parametrize(
         "annotation_keys",
         ["display_label", "class_label"],
@@ -707,6 +713,7 @@ def test_upsertTable(
         projectId,
         datasetId,
         annotation_keys,
+        dmge: DataModelGraphExplorer,
     ):
         table_manipulation = "upsert"
 
@@ -727,24 +734,6 @@ def test_upsertTable(
                 not in synapse_store.get_table_info(projectId=projectId).keys()
             )
 
-        # associate org FollowUp metadata with files
-        inputModelLocaiton = helpers.get_data_path(
-            os.path.basename(config.model_location)
-        )
-
-        data_model_parser = DataModelParser(path_to_data_model=inputModelLocaiton)
-        # Parse Model
-        parsed_data_model = data_model_parser.parse_model()
-
-        # Instantiate DataModelGraph
-        data_model_grapher = DataModelGraph(parsed_data_model)
-
-        # Generate graph
-        graph_data_model = data_model_grapher.generate_data_model_graph()
-
-        # Instantiate DataModelGraphExplorer
-        dmge = DataModelGraphExplorer(graph_data_model)
-
         # updating file view on synapse takes a long time
         manifestId = synapse_store.associateMetadataWithFiles(
             dmge=dmge,
@@ -890,3 +879,367 @@ def test_entity_type_checking(self, synapse_store, entity_id, caplog):
                     "You are using entity type: folder. Please provide a file ID"
                     in record.message
                 )
+
+
+class TestManifestUpload:
+    """Test manifest upload"""
+
+    @pytest.mark.parametrize(
+        "original_manifest, files_in_dataset, expected_entity_ids, expected_filenames",
+        [
+            # there are new files in dataset folders after a manifest gets generated
+            # but the expected behavior is to add entity ID to existing "filename" column
+            (
+                {
+                    "Filename": {0: "Test sub folder/sample_file_one.txt"},
+                    "Sample ID": {0: 1},
+                    "File Format": {0: "BAM"},
+                    "Component": {0: "BulkRNA-seqAssay"},
+                    "Genome Build": {0: "GRCh37"},
+                    "Genome FASTA": {0: ""},
+                    "entityId": {0: ""},
+                    "Id": {0: "mock_id_0"},
+                },
+                [
+                    ("syn1224", "Test sub folder/sample_file_one.txt"),
+                    ("syn1225", "Test sub folder/sample_file_two.txt"),
+                ],
+                ["syn1224"],
+                ["Test sub folder/sample_file_one.txt"],
+            ),
+            # there's no new files in dataset folder after a manifest gets generated
+            (
+                {
+                    "Filename": {
+                        0: "Test sub folder/sample_file_one.txt",
+                        1: "Test sub folder/sample_file_two.txt",
+                    },
+                    "Sample ID": {0: 1, 1: 2},
+                    "File Format": {0: "BAM", 1: "BAM"},
+                    "Component": {0: "BulkRNA-seqAssay", 1: "BulkRNA-seqAssay"},
+                    "Genome Build": {0: "GRCh37", 1: "GRCh37"},
+                    "Genome FASTA": {0: "", 1: ""},
+                    "entityId": {0: "syn1224", 1: "syn1225"},
+                    "Id": {0: "mock_id_0", 1: "mock_id_1"},
+                },
+                [
+                    ("syn1224", "Test sub folder/sample_file_one.txt"),
+                    ("syn1225", "Test sub folder/sample_file_two.txt"),
+                ],
+                ["syn1224", "syn1225"],
+                [
+                    "Test sub folder/sample_file_one.txt",
+                    "Test sub folder/sample_file_two.txt",
+                ],
+            ),
+        ],
+    )
+    def test_add_annotations_to_entities_files(
+        self,
+        synapse_store: SynapseStorage,
+        dmge: DataModelGraphExplorer,
+        original_manifest: dict[str, Any],
+        files_in_dataset: str,
+        expected_filenames: list[str],
+        expected_entity_ids: list[str],
+    ) -> None:
+        """test adding annotations to entities files
+
+        Args:
+            helpers (fixture): a pytest fixture
+            synapse_store (SynapseStorage): mock synapse store
+            dmge (DataModelGraphExplorer): data model grpah explorer object
+            original_manifest (Dictionary): the dataframe of manifest that you want to submit
+            files_in_dataset (str): mock entityid and file name returned by getFilesInStorageDataset function
+            expected_filenames (list(str)): expected list of file names
+            expected_entity_ids (list(str)): expected list of entity ids
+        """
+        with patch(
+            "schematic.store.synapse.SynapseStorage.getFilesInStorageDataset",
+            return_value=files_in_dataset,
+        ):
+            manifest_df = pd.DataFrame(original_manifest)
+
+            new_df = synapse_store.add_annotations_to_entities_files(
+                dmge,
+                manifest_df,
+                manifest_record_type="entity",
+                datasetId="mock id",
+                hideBlanks=True,
+            )
+            file_names_lst = new_df["Filename"].tolist()
+            entity_ids_lst = new_df["entityId"].tolist()
+
+            # test entityId and Id columns get added
+            assert "entityId" in new_df.columns
+            assert "Id" in new_df.columns
+            assert file_names_lst == expected_filenames
+            assert entity_ids_lst == expected_entity_ids
+
+    @pytest.mark.parametrize(
+        "mock_manifest_file_path",
+        [
+            "mock_manifests/test_mock_manifest.csv",
+            "mock_manifests/test_mock_manifest_censored.csv",
+        ],
+    )
+    def test_upload_manifest_file(
+        self,
+        helpers: Helpers,
+        synapse_store: SynapseStorage,
+        mock_manifest_file_path: str,
+    ) -> None:
+        """test upload manifest file function
+
+        Args:
+            helpers (fixture): a pytest fixture
+            synapse_store (SynapseStorage): mock synapse store
+            dmge (DataModelGraphExplorer): data model grpah explorer object
+        """
+        test_df = pd.DataFrame(
+            {
+                "Filename": {
+                    0: "Test sub folder/sample_file_one.txt",
+                    1: "Test sub folder/sample_file_three.txt",
+                    2: "Test sub folder/sample_file_two.txt",
+                },
+                "Sample ID": {0: 1, 1: 2, 2: 3},
+                "File Format": {0: "BAM", 1: "BAM", 2: "BAM"},
+                "Component": {
+                    0: "BulkRNA-seqAssay",
+                    1: "BulkRNA-seqAssay",
+                    2: "BulkRNA-seqAssay",
+                },
+                "Genome Build": {0: "GRCh37", 1: "GRCh37", 2: "GRCh37"},
+                "Genome FASTA": {0: "", 1: "", 2: ""},
+                "Id": {0: "mock1", 1: "mock2", 2: "mock3"},
+                "entityId": {0: "syn1224", 1: "syn1225", 2: "syn1226"},
+            }
+        )
+        with patch("synapseclient.Synapse.store") as syn_store_mock, patch(
+            "synapseutils.copy_functions.changeFileMetaData"
+        ):
+            syn_store_mock.return_value.id = "mock manifest id"
+            mock_file_path = helpers.get_data_path(mock_manifest_file_path)
+            mock_manifest_synapse_file_id = synapse_store.upload_manifest_file(
+                manifest=test_df,
+                metadataManifestPath=mock_file_path,
+                datasetId="mock dataset id",
+                restrict_manifest=True,
+            )
+            assert mock_manifest_synapse_file_id == "mock manifest id"
+
+    @pytest.mark.parametrize("file_annotations_upload", [True, False])
+    @pytest.mark.parametrize("hide_blanks", [True, False])
+    @pytest.mark.parametrize("restrict", [True, False])
+    @pytest.mark.parametrize("manifest_record_type", ["entity", "table", "both"])
+    def test_upload_manifest_as_csv(
+        self,
+        helpers: Helpers,
+        dmge: DataModelGraphExplorer,
+        synapse_store: SynapseStorage,
+        file_annotations_upload: bool,
+        manifest_record_type: str,
+        hide_blanks: bool,
+        restrict: bool,
+    ) -> None:
+        with (
+            patch(
+                "schematic.store.synapse.SynapseStorage.add_annotations_to_entities_files"
+            ) as add_anno_mock,
+            patch(
+                "schematic.store.synapse.SynapseStorage.upload_manifest_file",
+                return_value="mock manifest id",
+            ) as upload_manifest_mock,
+            patch(
+                "schematic.store.synapse.SynapseStorage.format_manifest_annotations"
+            ) as format_manifest_anno_mock,
+            patch.object(synapse_store.syn, "set_annotations"),
+        ):
+            manifest_path = helpers.get_data_path("mock_manifests/test_BulkRNAseq.csv")
+            manifest_df = helpers.get_data_frame(manifest_path)
+            synapse_store.upload_manifest_as_csv(
+                dmge,
+                manifest=manifest_df,
+                metadataManifestPath=manifest_path,
+                datasetId="mock synapse id",
+                restrict=restrict,
+                manifest_record_type=manifest_record_type,
+                file_annotations_upload=file_annotations_upload,
+                hideBlanks=hide_blanks,
+                component_name="BulkRNA-seqAssay",
+                annotation_keys="class_label",
+            )
+            if file_annotations_upload:
+                add_anno_mock.assert_called_once()
+            else:
+                add_anno_mock.assert_not_called()
+
+            upload_manifest_mock.assert_called_once()
+            format_manifest_anno_mock.assert_called_once()
+
+    @pytest.mark.parametrize("file_annotations_upload", [True, False])
+    @pytest.mark.parametrize("hide_blanks", [True, False])
+    @pytest.mark.parametrize("restrict", [True, False])
+    @pytest.mark.parametrize("manifest_record_type", ["entity", "table", "both"])
+    def test_upload_manifest_as_table(
+        self,
+        helpers: Helpers,
+        synapse_store: SynapseStorage,
+        dmge: DataModelGraphExplorer,
+        file_annotations_upload: bool,
+        hide_blanks: bool,
+        restrict: bool,
+        manifest_record_type: str,
+    ) -> None:
+        mock_df = pd.DataFrame()
+        with (
+            patch(
+                "schematic.store.synapse.SynapseStorage.uploadDB",
+                return_value=["mock_table_id", mock_df, "mock_table_manifest"],
+            ) as update_db_mock,
+            patch(
+                "schematic.store.synapse.SynapseStorage.add_annotations_to_entities_files"
+            ) as add_anno_mock,
+            patch(
+                "schematic.store.synapse.SynapseStorage.upload_manifest_file",
+                return_value="mock manifest id",
+            ),
+            patch.object(synapse_store.syn, "set_annotations") as set_anno_mock,
+            patch(
+                "schematic.store.synapse.SynapseStorage.format_manifest_annotations"
+            ) as format_manifest_anno_mock,
+        ):
+            manifest_path = helpers.get_data_path("mock_manifests/test_BulkRNAseq.csv")
+            manifest_df = helpers.get_data_frame(manifest_path)
+            synapse_store.upload_manifest_as_table(
+                dmge,
+                manifest=manifest_df,
+                metadataManifestPath=manifest_path,
+                datasetId="mock synapse id",
+                table_name="new table name",
+                component_name="BulkRNA-seqAssay",
+                restrict=restrict,
+                manifest_record_type=manifest_record_type,
+                hideBlanks=hide_blanks,
+                table_manipulation="replace",
+                table_column_names="class_label",
+                annotation_keys="class_label",
+                file_annotations_upload=file_annotations_upload,
+            )
+            if file_annotations_upload:
+                add_anno_mock.assert_called_once()
+            else:
+                add_anno_mock.assert_not_called()
+            # need to set annotations for both table and files
+            assert format_manifest_anno_mock.call_count == 2
+            assert set_anno_mock.call_count == 2
+            assert update_db_mock.call_count == 2
+
+    @pytest.mark.parametrize("file_annotations_upload", [True, False])
+    @pytest.mark.parametrize("hide_blanks", [True, False])
+    @pytest.mark.parametrize("restrict", [True, False])
+    @pytest.mark.parametrize("manifest_record_type", ["entity", "table", "both"])
+    def test_upload_manifest_combo(
+        self,
+        helpers: Helpers,
+        synapse_store: SynapseStorage,
+        dmge: DataModelGraphExplorer,
+        file_annotations_upload: bool,
+        hide_blanks: bool,
+        restrict: bool,
+        manifest_record_type: str,
+    ) -> None:
+        mock_df = pd.DataFrame()
+        manifest_path = helpers.get_data_path("mock_manifests/test_BulkRNAseq.csv")
+        manifest_df = helpers.get_data_frame(manifest_path)
+        with (
+            patch(
+                "schematic.store.synapse.SynapseStorage.uploadDB",
+                return_value=["mock_table_id", mock_df, "mock_table_manifest"],
+            ) as update_db_mock,
+            patch(
+                "schematic.store.synapse.SynapseStorage.add_annotations_to_entities_files"
+            ) as add_anno_mock,
+            patch(
+                "schematic.store.synapse.SynapseStorage.upload_manifest_file",
+                return_value="mock manifest id",
+            ),
+            patch.object(synapse_store.syn, "set_annotations") as set_anno_mock,
+            patch(
+                "schematic.store.synapse.SynapseStorage.format_manifest_annotations"
+            ) as format_manifest_anno_mock,
+        ):
+            synapse_store.upload_manifest_combo(
+                dmge,
+                manifest=manifest_df,
+                metadataManifestPath=manifest_path,
+                datasetId="mock synapse id",
+                table_name="new table name",
+                component_name="BulkRNA-seqAssay",
+                restrict=restrict,
+                manifest_record_type=manifest_record_type,
+                hideBlanks=hide_blanks,
+                table_manipulation="replace",
+                table_column_names="class_label",
+                annotation_keys="class_label",
+                file_annotations_upload=file_annotations_upload,
+            )
+
+            if file_annotations_upload:
+                add_anno_mock.assert_called_once()
+            else:
+                add_anno_mock.assert_not_called()
+            # need to set annotations for both table and files
+            assert format_manifest_anno_mock.call_count == 2
+            assert set_anno_mock.call_count == 2
+            assert update_db_mock.call_count == 2
+
+    @pytest.mark.parametrize(
+        "manifest_record_type,expected",
+        [
+            ("file_only", "mock_id_csv"),
+            ("table_and_file", "mock_id_table"),
+            ("file_and_entities", "mock_id_csv"),
+            ("table_file_and_entities", "mock_id_entities"),
+        ],
+    )
+    @pytest.mark.parametrize("restrict_rules", [True, False])
+    @pytest.mark.parametrize("hide_blanks", [True, False])
+    @pytest.mark.parametrize("file_annotations_upload", [True, False])
+    def test_associate_metadata_with_files(
+        self,
+        helpers: Helpers,
+        restrict_rules: bool,
+        hide_blanks: bool,
+        synapse_store: SynapseStorage,
+        manifest_record_type: str,
+        expected: str,
+        file_annotations_upload: bool,
+        dmge: DataModelGraphExplorer,
+    ) -> None:
+        with (
+            patch(
+                "schematic.store.synapse.SynapseStorage.upload_manifest_as_csv",
+                return_value="mock_id_csv",
+            ),
+            patch(
+                "schematic.store.synapse.SynapseStorage.upload_manifest_as_table",
+                return_value="mock_id_table",
+            ),
+            patch(
+                "schematic.store.synapse.SynapseStorage.upload_manifest_combo",
+                return_value="mock_id_entities",
+            ),
+        ):
+            manifest_path = "mock_manifests/test_BulkRNAseq.csv"
+            manifest_id = synapse_store.associateMetadataWithFiles(
+                dmge=dmge,
+                metadataManifestPath=helpers.get_data_path(manifest_path),
+                datasetId="mock_dataset_id",
+                hideBlanks=hide_blanks,
+                restrict_manifest=restrict_rules,
+                manifest_record_type=manifest_record_type,
+                file_annotations_upload=file_annotations_upload,
+            )
+        assert manifest_id == expected
diff --git a/tests/test_utils.py b/tests/test_utils.py
index 3b99377f2..525778185 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -7,6 +7,9 @@
 import time
 from datetime import datetime
 from unittest import mock
+from pathlib import Path
+from typing import Union, Generator
+from _pytest.fixtures import FixtureRequest
 
 import numpy as np
 import pandas as pd
@@ -70,6 +73,27 @@
 
 IN_GITHUB_ACTIONS = os.getenv("GITHUB_ACTIONS")
 
+RULE_MODIFIERS = ["error", "warning", "strict", "like", "set", "value"]
+VALIDATION_EXPECTATION = {
+    "int": "expect_column_values_to_be_in_type_list",
+    "float": "expect_column_values_to_be_in_type_list",
+    "str": "expect_column_values_to_be_of_type",
+    "num": "expect_column_values_to_be_in_type_list",
+    "date": "expect_column_values_to_be_dateutil_parseable",
+    "recommended": "expect_column_values_to_not_be_null",
+    "protectAges": "expect_column_values_to_be_between",
+    "unique": "expect_column_values_to_be_unique",
+    "inRange": "expect_column_values_to_be_between",
+    "IsNA": "expect_column_values_to_match_regex_list",
+    # To be implemented rules with possible expectations
+    # "list": "expect_column_values_to_not_match_regex_list",
+    # "regex": "expect_column_values_to_match_regex",
+    # "url": "expect_column_values_to_be_valid_urls",
+    # "matchAtLeastOne": "expect_foreign_keys_in_column_a_to_exist_in_column_b",
+    # "matchExactlyOne": "expect_foreign_keys_in_column_a_to_exist_in_column_b",
+    # "matchNone": "expect_compound_columns_to_be_unique",
+}
+
 MULTI_RULE_DICT = {
     "multi_rule": {
         "starting_rule": "unique::list::num",
@@ -147,8 +171,7 @@
     "duplicated_component": {
         "validation_rules": ["#Patient unique^^#Patient int"],
         "parsed_rules": "raises_exception",
-        }
-
+    },
 }
 
 TEST_DN_DICT = {
@@ -159,22 +182,55 @@
     "bio_things": {"class": "BioThings", "property": "bioThings"},
 }
 
+test_disk_storage = [
+    (2, 4000, 16000),
+    (1000, 4000, 16000),
+    (2000000, 1900000, 2000000),
+    (1073741825, 1073741824, 1181116006.4),
+]
+
+
+# create temporary files with various size based on request
+@pytest.fixture()
+def create_temp_query_file(
+    tmp_path: Path, request: FixtureRequest
+) -> Generator[tuple[Path, Path, Path], None, None]:
+    """create temporary files of various size based on request parameter.
+
+    Args:
+        tmp_path (Path): temporary file path
+        request (any): a request for a fixture from a test
+
+    Yields:
+        Generator[Tuple[Path, Path, Path]]: return path of mock synapse cache directory, mock table query folder and csv
+    """
+    # define location of mock synapse cache
+    mock_synapse_cache_dir = tmp_path / ".synapseCache/"
+    mock_synapse_cache_dir.mkdir()
+    mock_sub_folder = mock_synapse_cache_dir / "123"
+    mock_sub_folder.mkdir()
+    mock_table_query_folder = mock_sub_folder / "456"
+    mock_table_query_folder.mkdir()
+
+    # create mock table query csv
+    mock_synapse_table_query_csv = (
+        mock_table_query_folder / "mock_synapse_table_query.csv"
+    )
+    with open(mock_synapse_table_query_csv, "wb") as f:
+        f.write(b"\0" * request.param)
+    yield mock_synapse_cache_dir, mock_table_query_folder, mock_synapse_table_query_csv
+
 
 class TestGeneral:
-    def test_clear_synapse_cache(self, tmp_path):
+    @pytest.mark.parametrize("create_temp_query_file", [3, 1000], indirect=True)
+    def test_clear_synapse_cache(self, create_temp_query_file) -> None:
         # define location of mock synapse cache
-        mock_synapse_cache_dir = tmp_path / ".synapseCache/"
-        mock_synapse_cache_dir.mkdir()
-        mock_sub_folder = mock_synapse_cache_dir / "123"
-        mock_sub_folder.mkdir()
-        mock_table_query_folder = mock_sub_folder / "456"
-        mock_table_query_folder.mkdir()
-
-        # create mock table query csv and a mock cache map
-        mock_synapse_table_query_csv = (
-            mock_table_query_folder / "mock_synapse_table_query.csv"
-        )
-        mock_synapse_table_query_csv.write_text("mock table query content")
+        (
+            mock_synapse_cache_dir,
+            mock_table_query_folder,
+            mock_synapse_table_query_csv,
+        ) = create_temp_query_file
+        # create a mock cache map
         mock_cache_map = mock_table_query_folder / ".cacheMap"
         mock_cache_map.write_text(
             f"{mock_synapse_table_query_csv}: '2022-06-13T19:24:27.000Z'"
@@ -222,22 +278,25 @@ def test_calculate_datetime_raise_error(self):
 
     # this test might fail for windows machine
     @pytest.mark.not_windows
-    def test_check_synapse_cache_size(self, tmp_path):
-        mock_synapse_cache_dir = tmp_path / ".synapseCache"
-        mock_synapse_cache_dir.mkdir()
-
-        mock_synapse_table_query_csv = (
-            mock_synapse_cache_dir / "mock_synapse_table_query.csv"
-        )
-        mock_synapse_table_query_csv.write_text("example file for calculating cache")
-
-        file_size = check_synapse_cache_size(mock_synapse_cache_dir)
+    @pytest.mark.parametrize(
+        "create_temp_query_file,local_disk_size,gh_disk_size",
+        test_disk_storage,
+        indirect=["create_temp_query_file"],
+    )
+    def test_check_synapse_cache_size(
+        self,
+        create_temp_query_file,
+        local_disk_size: int,
+        gh_disk_size: Union[int, float],
+    ) -> None:
+        mock_synapse_cache_dir, _, _ = create_temp_query_file
+        disk_size = check_synapse_cache_size(mock_synapse_cache_dir)
 
         # For some reasons, when running in github action, the size of file changes.
         if IN_GITHUB_ACTIONS:
-            assert file_size == 8000
+            assert disk_size == gh_disk_size
         else:
-            assert file_size == 4000
+            assert disk_size == local_disk_size
 
     def test_find_duplicates(self):
         mock_list = ["foo", "bar", "foo"]
@@ -775,18 +834,20 @@ def test_parse_single_set_validation_rules(self, test_individual_rule_set):
     @pytest.mark.parametrize(
         "component_names",
         [
-            ["duplicated_component", ['Patient', 'Biospecimen', 'Patient']],
-            ["individual_component", ['Patient', 'Biospecimen']],
-            ["no_component", []]
+            ["duplicated_component", ["Patient", "Biospecimen", "Patient"]],
+            ["individual_component", ["Patient", "Biospecimen"]],
+            ["no_component", []],
         ],
         ids=["duplicated_component", "individual_component", "no_component"],
     )
     def test_check_for_duplicate_components(self, component_names):
         """Test that we are properly identifying duplicates in a list.
-            Exception should only be triggered when the duplicate component list is passed.
+        Exception should only be triggered when the duplicate component list is passed.
         """
         try:
-            check_for_duplicate_components(component_names=component_names[1], validation_rule_string='dummy_str')
+            check_for_duplicate_components(
+                component_names=component_names[1], validation_rule_string="dummy_str"
+            )
         except:
             assert component_names[0] == "duplicated_component"
 
@@ -812,7 +873,7 @@ def test_parse_validation_rules(self, test_rule_name):
             )
             assert expected_parsed_rules == parsed_validation_rules
         except:
-            assert test_rule_name in ["str_rule", "duplicated_component"] 
+            assert test_rule_name in ["str_rule", "duplicated_component"]
 
     @pytest.mark.parametrize(
         "test_rule_name",
@@ -836,6 +897,7 @@ def test_extract_component_validation_rules(self, test_rule_name):
                         component
                     ]
                 )
+
     @pytest.mark.parametrize(
         "test_dn",
         list(TEST_DN_DICT.keys()),
@@ -950,8 +1012,7 @@ def test_get_label_from_display_name(self, test_dn: str, data_model_labels: str)
 
 class TestValidateUtils:
     def test_validate_schema(self, helpers):
-        """
-        """
+        """ """
 
         # Get data model path
         data_model_path = helpers.get_data_path("example.model.jsonld")
@@ -999,6 +1060,50 @@ def test_validate_property_schema(self, helpers):
 
         assert error is None
 
+    @pytest.mark.parametrize(
+        "rule",
+        [
+            "required warning",
+            "strict warning set required",
+            "required strict warning set",
+            "unique warning required",
+            "unique required warning",
+            "list strict",
+            "required",
+        ],
+        ids=[
+            "required_with_modifier",
+            "required_last_with_multiple_modifiers",
+            "required_first_with_multiple_modifiers",
+            "rule_with_modifier_and_required_last",
+            "rule_with_modifier_and_required_middle",
+            "no_required",
+            "only_required",
+        ],
+    )
+    def test_required_is_only_rule(self, rule: str) -> None:
+        """Verify that function required_is_only_rule is working as expected.
+        Args:
+            rule: str, various strings that we expect to behave a certain way during parsing.
+        """
+
+        output = validate_utils.required_is_only_rule(
+            rule=rule,
+            attribute="Patient ID",
+            rule_modifiers=RULE_MODIFIERS,
+            validation_expectation=VALIDATION_EXPECTATION,
+        )
+
+        if rule in [
+            "required warning",
+            "strict warning set required",
+            "required strict warning set",
+            "required",
+        ]:
+            assert output == True
+        else:
+            assert output == False
+
 
 class TestCsvUtils:
     def test_csv_to_schemaorg(self, helpers, tmp_path):
@@ -1020,10 +1125,10 @@ def test_csv_to_schemaorg(self, helpers, tmp_path):
         data_model_grapher = DataModelGraph(parsed_data_model)
 
         # Generate graph
-        graph_data_model = data_model_grapher.generate_data_model_graph()
+        graph_data_model = data_model_grapher.graph
 
         # Convert graph to JSONLD
-        jsonld_data_model = convert_graph_to_jsonld(Graph=graph_data_model)
+        jsonld_data_model = convert_graph_to_jsonld(graph=graph_data_model)
 
         # saving updated schema.org schema
         actual_jsonld_path = tmp_path / "example.from_csv.model.jsonld"
diff --git a/tests/test_validation.py b/tests/test_validation.py
index 8e028b6fe..f8b9a5746 100644
--- a/tests/test_validation.py
+++ b/tests/test_validation.py
@@ -27,16 +27,13 @@ def DMGE(helpers):
     dmge = helpers.get_data_model_graph_explorer(path="example.model.jsonld")
     yield dmge
 
-
-@pytest.fixture
-def metadataModel(helpers):
+def get_metadataModel(helpers, model_name:str):
     metadataModel = MetadataModel(
-        inputMModelLocation=helpers.get_data_path("example.model.jsonld"),
+        inputMModelLocation=helpers.get_data_path(model_name),
         inputMModelLocationType="local",
         data_model_labels="class_label",
     )
-
-    yield metadataModel
+    return metadataModel
 
 
 def get_rule_combinations():
@@ -55,65 +52,143 @@ class TestManifestValidation:
     if os.path.exists("great_expectations/expectations/Manifest_test_suite.json"):
         os.remove("great_expectations/expectations/Manifest_test_suite.json")
 
-    def test_valid_manifest(self, helpers, metadataModel):
-        manifestPath = helpers.get_data_path("mock_manifests/Valid_Test_Manifest.csv")
-        rootNode = "MockComponent"
-
-        errors, warnings = metadataModel.validateModelManifest(
-            manifestPath=manifestPath,
-            rootNode=rootNode,
-            project_scope=["syn23643250"],
-        )
-
-        assert errors == []
-        assert warnings == []
+    @pytest.mark.parametrize(
+        ("model_name", "manifest_name", "root_node"),
+        [
+            ("example.model.csv","mock_manifests/Valid_Test_Manifest.csv", "MockComponent"),
+            ("example.model.csv", "mock_manifests/Patient_test_no_entry_for_cond_required_column.manifest.csv", "Patient"),
+            ("example_test_nones.model.csv","mock_manifests/Valid_Test_Manifest_with_nones.csv", "MockComponent"),
+        ],
+        ids=["example_model", "example_with_no_entry_for_cond_required_columns", "example_with_nones"],
+    )
+    @pytest.mark.parametrize(
+        "project_scope",
+        ["syn54126707", "syn55250368", "syn55271234"],
+        ids=["project_scope_with_manifests", "project_scope_without_manifests", "project_scope_with_empty_manifest"],
+    )
+    def test_valid_manifest(self, helpers, model_name:str, manifest_name:str,
+            root_node:str, project_scope:str, dmge:DataModelGraph):
+        """ Run the valid manifest in various situations, some of which will generate errors or warnings,
+            if there are "issues" with target manifests on manifests. Since there are so many parameters, limit
+            the combinations that are being run to the ones that are relevant.
+        Args:
+            project_scope: scope to limit cross manifest validation
+                project_scope_with_manifests, is a scope that contains all the necessary manifests with the proper
+                entries, for the Valid_Test_Manifest.csv to pass validation
+                project_scope_without_manifests, is a scope that does not contain any manifests. For each cross manifest
+                    validation run, there will be a warning raised that validation will pass and its assumed the
+                    manifest is the first one being submitted.
+                project_scope_with_empty_manifest, is a scope that contains a single empty MockComponent manifest,
+                    depending on messaging level, a warning or error will be raised to alert users that the
+                    target manifest is empty.
+            model_name, str: csv model used for validation, located in tests/data
+            manifest_name, str: valid manifest being validated
+            root_node, str:
+                Component name for the manifest being validated
 
-    def test_invalid_manifest(self, helpers, dmge, metadataModel):
+        """
+        manifest_path = helpers.get_data_path(manifest_name)
+
+        warning_rule_sets_1 = [
+                ('Check Match at Least', 'matchAtLeastOne Patient.PatientID set'),
+                ('Check Match at Least values', 'matchAtLeastOne MockComponent.checkMatchatLeastvalues value'),
+                ('Check Match Exactly', 'matchExactlyOne MockComponent.checkMatchExactly set'),
+                ('Check Match Exactly values', 'matchExactlyOne MockComponent.checkMatchExactlyvalues value'),
+            ]
+        warning_rule_sets_2 = warning_rule_sets_1[1:]
+        error_rule_sets = [
+                ('Check Match None', 'matchNone MockComponent.checkMatchNone set error'),
+                ('Check Match None values', 'matchNone MockComponent.checkMatchNonevalues value error'),
+            ]
+
+        # For the standard project scope, models and manifest should pass without warnings or errors
+        if project_scope == "syn54126707":
+            metadataModel = get_metadataModel(helpers, model_name)
+            errors, warnings = metadataModel.validateModelManifest(
+                manifestPath=manifest_path,
+                rootNode=root_node,
+                project_scope=[project_scope],
+            )
+            assert (errors, warnings) == ([], [])
+
+        # When submitting the first manifest for cross manifest validation (MockComponent), check that proper warning
+        # (to alert users that no validation will be run), is raised. The manifest is still valid to submit.
+        if (project_scope == "syn55250368" and root_node=="MockComponent" and
+            model_name in ["example.model.csv", "example_test_nones.model.csv"]):
+            metadataModel = get_metadataModel(helpers, model_name)
+            errors, warnings = metadataModel.validateModelManifest(
+                manifestPath=manifest_path,
+                rootNode=root_node,
+                project_scope=[project_scope],
+            )
+            
+            for attribute_name, val_rule in warning_rule_sets_1:
+                assert GenerateError.generate_no_cross_warning(
+                    dmge=dmge,
+                    attribute_name=attribute_name,
+                    val_rule=val_rule)[0] in warnings
+            assert errors == []
+        
+        # When submitting a manifest to a project that contains a manifest without data, ensure that the proper
+        # warnings/errors are raised.
+        elif project_scope == "syn55271234" and root_node=="MockComponent" and model_name == "example.model.csv":
+            metadataModel = get_metadataModel(helpers, model_name)
+            errors, warnings = metadataModel.validateModelManifest(
+                manifestPath=manifest_path,
+                rootNode=root_node,
+                project_scope=[project_scope],
+            )
+            for attribute_name, val_rule in warning_rule_sets_2:
+                assert GenerateError.generate_no_value_in_manifest_error(
+                    dmge=dmge,
+                    attribute_name=attribute_name,
+                    val_rule=val_rule)[1][0] in warnings
+            
+            for attribute_name, val_rule in error_rule_sets:
+                assert GenerateError.generate_no_value_in_manifest_error(
+                    dmge=dmge,
+                    attribute_name=attribute_name,
+                    val_rule=val_rule)[0][0] in errors
+
+
+    def test_invalid_manifest(self, helpers, dmge):
+        metadataModel = get_metadataModel(helpers, model_name="example.model.jsonld")
+        
         manifestPath = helpers.get_data_path("mock_manifests/Invalid_Test_Manifest.csv")
         rootNode = "MockComponent"
 
         errors, warnings = metadataModel.validateModelManifest(
             manifestPath=manifestPath,
             rootNode=rootNode,
-            project_scope=["syn23643250"],
+            project_scope=["syn54126707"],
         )
 
         # Check errors
-        assert (
-            GenerateError.generate_type_error(
+        assert GenerateError.generate_type_error(
                 val_rule="num",
                 row_num="3",
                 attribute_name="Check Num",
                 invalid_entry="c",
                 dmge=dmge,
-            )[0]
-            in errors
-        )
+            )[0] in errors
 
-        assert (
-            GenerateError.generate_type_error(
+        assert GenerateError.generate_type_error(
                 val_rule="int",
                 row_num="3",
                 attribute_name="Check Int",
                 invalid_entry="5.63",
                 dmge=dmge,
-            )[0]
-            in errors
-        )
-
-        assert (
-            GenerateError.generate_type_error(
+            )[0] in errors
+    
+        assert GenerateError.generate_type_error(
                 val_rule="str",
                 row_num="3",
                 attribute_name="Check String",
                 invalid_entry="94",
                 dmge=dmge,
-            )[0]
-            in errors
-        )
+            )[0] in errors
 
-        assert (
-            GenerateError.generate_list_error(
+        assert GenerateError.generate_list_error(
                 val_rule="list strict",
                 list_string="invalid list values",
                 row_num="3",
@@ -121,12 +196,9 @@ def test_invalid_manifest(self, helpers, dmge, metadataModel):
                 list_error="not_comma_delimited",
                 invalid_entry="invalid list values",
                 dmge=dmge,
-            )[0]
-            in errors
-        )
+            )[0] in errors
 
-        assert (
-            GenerateError.generate_list_error(
+        assert GenerateError.generate_list_error(
                 val_rule="list strict",
                 list_string="ab cd ef",
                 row_num="3",
@@ -134,12 +206,9 @@ def test_invalid_manifest(self, helpers, dmge, metadataModel):
                 list_error="not_comma_delimited",
                 invalid_entry="ab cd ef",
                 dmge=dmge,
-            )[0]
-            in errors
-        )
+            )[0] in errors
 
-        assert (
-            GenerateError.generate_regex_error(
+        assert GenerateError.generate_regex_error(
                 val_rule="regex",
                 reg_expression="[a-f]",
                 row_num="3",
@@ -147,12 +216,9 @@ def test_invalid_manifest(self, helpers, dmge, metadataModel):
                 module_to_call="match",
                 invalid_entry="m",
                 dmge=dmge,
-            )[0]
-            in errors
-        )
+            )[0] in errors
 
-        assert (
-            GenerateError.generate_regex_error(
+        assert GenerateError.generate_regex_error(
                 val_rule="regex",
                 reg_expression="[a-f]",
                 row_num="3",
@@ -160,12 +226,9 @@ def test_invalid_manifest(self, helpers, dmge, metadataModel):
                 module_to_call="search",
                 invalid_entry="q",
                 dmge=dmge,
-            )[0]
-            in errors
-        )
+            )[0] in errors
 
-        assert (
-            GenerateError.generate_regex_error(
+        assert GenerateError.generate_regex_error(
                 val_rule="regex",
                 reg_expression="^\d+$",
                 row_num="2",
@@ -173,12 +236,9 @@ def test_invalid_manifest(self, helpers, dmge, metadataModel):
                 module_to_call="search",
                 invalid_entry="5.4",
                 dmge=dmge,
-            )[0]
-            in errors
-        )
+            )[0] in errors
 
-        assert (
-            GenerateError.generate_url_error(
+        assert GenerateError.generate_url_error(
                 val_rule="url",
                 url="http://googlef.com/",
                 url_error="invalid_url",
@@ -187,102 +247,102 @@ def test_invalid_manifest(self, helpers, dmge, metadataModel):
                 argument=None,
                 invalid_entry="http://googlef.com/",
                 dmge=dmge,
-            )[0]
-            in errors
-        )
+            )[0] in errors
 
         date_err = GenerateError.generate_content_error(
             val_rule="date",
             attribute_name="Check Date",
             dmge=dmge,
             row_num=["2", "3", "4"],
-            error_val=["84-43-094", "32-984", "notADate"],
+            invalid_entry=["84-43-094", "32-984", "notADate"],
         )[0]
         error_in_list = [date_err[2] in error for error in errors]
         assert any(error_in_list)
 
-        assert (
-            GenerateError.generate_content_error(
+        assert GenerateError.generate_content_error(
                 val_rule="unique error",
                 attribute_name="Check Unique",
                 dmge=dmge,
                 row_num=["2", "3", "4"],
-                error_val=["str1"],
-            )[0]
-            in errors
-        )
+                invalid_entry=["str1"],
+            )[0] in errors
 
-        assert (
-            GenerateError.generate_content_error(
+        assert GenerateError.generate_content_error(
                 val_rule="inRange 50 100 error",
                 attribute_name="Check Range",
                 dmge=dmge,
                 row_num=["3"],
-                error_val=["30"],
+                invalid_entry=["30"],
+            )[0] in errors
+
+        assert (
+            GenerateError.generate_cross_warning(
+                val_rule="matchNone error",
+                row_num=["3"],
+                attribute_name="Check Match None",
+                manifest_id=["syn54126950"],
+                invalid_entry=["123"],
+                dmge=dmge,
             )[0]
-            in errors
+             in errors
         )
 
-        # check warnings
         assert (
-            GenerateError.generate_content_error(
+            GenerateError.generate_cross_warning(
+                val_rule="matchNone value error",
+                row_num=["4"],
+                attribute_name="Check Match None values",
+                invalid_entry=["123"],
+                dmge=dmge,
+            )[0]
+             in errors
+        )
+
+        # check warnings
+        assert GenerateError.generate_content_error(
                 val_rule="recommended",
                 attribute_name="Check Recommended",
                 dmge=dmge,
-            )[1]
-            in warnings
-        )
+            )[1] in warnings
 
-        assert (
-            GenerateError.generate_content_error(
+        assert GenerateError.generate_content_error(
                 val_rule="protectAges",
                 attribute_name="Check Ages",
                 dmge=dmge,
                 row_num=["2", "3"],
-                error_val=["6549", "32851"],
-            )[1]
-            in warnings
-        )
+                invalid_entry=["6549", "32851"],
+            )[1] in warnings
 
-        assert (
-            GenerateError.generate_cross_warning(
+        assert GenerateError.generate_cross_warning(
                 val_rule="matchAtLeastOne",
                 row_num=["3"],
                 attribute_name="Check Match at Least",
                 invalid_entry=["7163"],
-                missing_manifest_ID=["syn27600110", "syn29381803"],
+                manifest_id=["syn54126997", "syn54127001"],
                 dmge=dmge,
-            )[1]
-            in warnings
-        )
+            )[1] in warnings
 
-        assert (
-            GenerateError.generate_cross_warning(
+        assert GenerateError.generate_cross_warning(
                 val_rule="matchAtLeastOne MockComponent.checkMatchatLeastvalues value",
                 row_num=["3"],
                 attribute_name="Check Match at Least values",
                 invalid_entry=["51100"],
                 dmge=dmge,
-            )[1]
-            in warnings
-        )
+            )[1] in warnings       
 
-        assert (
+        assert \
             GenerateError.generate_cross_warning(
                 val_rule="matchExactlyOne",
                 attribute_name="Check Match Exactly",
-                matching_manifests=["syn29862078", "syn27648165"],
+                matching_manifests=["syn54126950", "syn54127008"],
                 dmge=dmge,
-            )[1]
-            in warnings
+            )[1] in warnings \
             or GenerateError.generate_cross_warning(
                 val_rule="matchExactlyOne",
                 attribute_name="Check Match Exactly",
-                matching_manifests=["syn29862066", "syn27648165"],
+                matching_manifests=["syn54127702", "syn54127008"],
                 dmge=dmge,
-            )[1]
-            in warnings
-        )
+            )[1] in warnings
 
         cross_warning = GenerateError.generate_cross_warning(
             val_rule="matchExactlyOne MockComponent.checkMatchExactlyvalues MockComponent.checkMatchExactlyvalues value",
@@ -291,11 +351,13 @@ def test_invalid_manifest(self, helpers, dmge, metadataModel):
             invalid_entry=["71738", "98085", "210065"],
             dmge=dmge,
         )[1]
+
         warning_in_list = [cross_warning[1] in warning for warning in warnings]
         assert any(warning_in_list)
 
 
-    def test_in_house_validation(self, helpers, dmge, metadataModel):
+    def test_in_house_validation(self, helpers, dmge):
+        metadataModel = get_metadataModel(helpers, model_name="example.model.jsonld")
         manifestPath = helpers.get_data_path("mock_manifests/Invalid_Test_Manifest.csv")
         rootNode = "MockComponent"
 
@@ -303,56 +365,43 @@ def test_in_house_validation(self, helpers, dmge, metadataModel):
             manifestPath=manifestPath,
             rootNode=rootNode,
             restrict_rules=True,
-            project_scope=["syn23643250"],
+            project_scope=["syn54126707"],
         )
 
         # Check errors
-        assert (
-            GenerateError.generate_type_error(
+        assert GenerateError.generate_type_error(
                 val_rule="num",
                 row_num="3",
                 attribute_name="Check Num",
                 invalid_entry="c",
                 dmge=dmge,
-            )[0]
-            in errors
-        )
+            )[0] in errors
 
-        assert (
-            GenerateError.generate_type_error(
+        assert GenerateError.generate_type_error(
                 val_rule="int",
                 row_num="3",
                 attribute_name="Check Int",
                 invalid_entry="5.63",
                 dmge=dmge,
-            )[0]
-            in errors
-        )
+            )[0] in errors
 
-        assert (
-            GenerateError.generate_type_error(
+        assert GenerateError.generate_type_error(
                 val_rule="str",
                 row_num="3",
                 attribute_name="Check String",
                 invalid_entry="94",
                 dmge=dmge,
-            )[0]
-            in errors
-        )
+            )[0] in errors
 
-        assert (
-            GenerateError.generate_type_error(
+        assert GenerateError.generate_type_error(
                 val_rule="int",
                 row_num="3",
                 attribute_name="Check NA",
                 invalid_entry="9.5",
                 dmge=dmge,
-            )[0]
-            in errors
-        )
+            )[0] in errors
 
-        assert (
-            GenerateError.generate_list_error(
+        assert GenerateError.generate_list_error(
                 val_rule="list strict",
                 list_string="invalid list values",
                 row_num="3",
@@ -360,12 +409,9 @@ def test_in_house_validation(self, helpers, dmge, metadataModel):
                 list_error="not_comma_delimited",
                 invalid_entry="invalid list values",
                 dmge=dmge,
-            )[0]
-            in errors
-        )
+            )[0] in errors
 
-        assert (
-            GenerateError.generate_list_error(
+        assert GenerateError.generate_list_error(
                 val_rule="list strict",
                 list_string="ab cd ef",
                 row_num="3",
@@ -373,12 +419,9 @@ def test_in_house_validation(self, helpers, dmge, metadataModel):
                 list_error="not_comma_delimited",
                 invalid_entry="ab cd ef",
                 dmge=dmge,
-            )[0]
-            in errors
-        )
+            )[0] in errors
 
-        assert (
-            GenerateError.generate_regex_error(
+        assert GenerateError.generate_regex_error(
                 val_rule="regex",
                 reg_expression="[a-f]",
                 row_num="3",
@@ -386,12 +429,9 @@ def test_in_house_validation(self, helpers, dmge, metadataModel):
                 module_to_call="search",
                 invalid_entry="q",
                 dmge=dmge,
-            )[0]
-            in errors
-        )
+            )[0] in errors
 
-        assert (
-            GenerateError.generate_regex_error(
+        assert GenerateError.generate_regex_error(
                 val_rule="regex",
                 reg_expression="[a-f]",
                 row_num="3",
@@ -399,12 +439,9 @@ def test_in_house_validation(self, helpers, dmge, metadataModel):
                 module_to_call="match",
                 invalid_entry="m",
                 dmge=dmge,
-            )[0]
-            in errors
-        )
+            )[0] in errors
 
-        assert (
-            GenerateError.generate_url_error(
+        assert GenerateError.generate_url_error(
                 val_rule="url",
                 url="http://googlef.com/",
                 url_error="invalid_url",
@@ -413,61 +450,204 @@ def test_in_house_validation(self, helpers, dmge, metadataModel):
                 argument=None,
                 invalid_entry="http://googlef.com/",
                 dmge=dmge,
+            )[0] in errors
+
+        assert (
+            GenerateError.generate_cross_warning(
+                val_rule="matchNone error",
+                row_num=["3"],
+                attribute_name="Check Match None",
+                manifest_id=["syn54126950"],
+                invalid_entry=["123"],
+                dmge=dmge,
             )[0]
-            in errors
+             in errors
         )
 
-        # Check Warnings
         assert (
             GenerateError.generate_cross_warning(
+                val_rule="matchNone value error",
+                row_num=["4"],
+                attribute_name="Check Match None values",
+                invalid_entry=["123"],
+                dmge=dmge,
+            )[0]
+             in errors
+        )
+
+        # Check Warnings
+        assert GenerateError.generate_cross_warning(
                 val_rule="matchAtLeastOne",
                 row_num=["3"],
                 attribute_name="Check Match at Least",
                 invalid_entry=["7163"],
-                missing_manifest_ID=["syn27600110", "syn29381803"],
+                manifest_id=["syn54126997", "syn54127001"],
                 dmge=dmge,
-            )[1]
-            in warnings
-        )
+            )[1] in warnings
 
-        assert (
-            GenerateError.generate_cross_warning(
+        assert GenerateError.generate_cross_warning(
                 val_rule="matchAtLeastOne MockComponent.checkMatchatLeastvalues value",
                 row_num=["3"],
                 attribute_name="Check Match at Least values",
                 invalid_entry=["51100"],
                 dmge=dmge,
-            )[1]
-            in warnings
-        )
+            )[1] in warnings
 
-        assert (
+        assert \
             GenerateError.generate_cross_warning(
                 val_rule="matchExactlyOne",
                 attribute_name="Check Match Exactly",
-                matching_manifests=["syn29862078", "syn27648165"],
+                matching_manifests=["syn54126950", "syn54127008"],
                 dmge=dmge,
-            )[1]
-            in warnings
+            )[1] in warnings \
             or GenerateError.generate_cross_warning(
                 val_rule="matchExactlyOne",
                 attribute_name="Check Match Exactly",
-                matching_manifests=["syn29862066", "syn27648165"],
+                matching_manifests=["syn54127702", "syn54127008"],
                 dmge=dmge,
-            )[1]
-            in warnings
-        )
+            )[1] in warnings
 
-        assert (
-            GenerateError.generate_cross_warning(
+        assert GenerateError.generate_cross_warning(
                 val_rule="matchExactlyOne MockComponent.checkMatchExactlyvalues MockComponent.checkMatchExactlyvalues value",
                 row_num=["2", "3", "4"],
                 attribute_name="Check Match Exactly values",
                 invalid_entry=["71738", "98085", "210065"],
                 dmge=dmge,
-            )[1]
-            in warnings
-        )
+            )[1] in warnings
+
+    @pytest.mark.parametrize(
+        "model_name",
+        [
+            "example.model.csv",
+            "example_required_vr_test.model.csv",
+        ],
+        ids=["example_model", "example_with_requirements_from_vr"],
+    )
+
+    @pytest.mark.parametrize(
+        ["manifest_name", "root_node",],
+        [
+            ("mock_manifests/Biospecimen_required_vr_test_fail.manifest.csv", "Biospecimen"),
+            ("mock_manifests/Biospecimen_required_vr_test_pass.manifest.csv", "Biospecimen"),
+            ("mock_manifests/Patient_required_vr_test_pass.manifest.csv", "Patient"),
+            ("mock_manifests/Patient_test_no_entry_for_cond_required_column.manifest.csv", "Patient"),
+            ("mock_manifests/BulkRNAseq_component_based_required_rule_test.manifest.csv", "BulkRNA-seqAssay"),
+        ],
+        ids=["biospeciment_required_vr_empty", "biospecimen_required_filled", "patient_not_required_empty", "patient_conditionally_required_not_filled", "bulk_rna_seq_component_based_rule_test"],
+    )
+    def test_required_validation_rule(self, helpers, model_name:str, manifest_name:str, root_node:str, dmge:DataModelGraphExplorer) -> None:
+        """
+        Args:
+            model_name, str: model to run test validation against
+                Model Difference:
+                    - example.model.csv:
+                        PatientID attribute:
+                            Required=True
+                        FileFormat attribute:
+                            Required = True, Genome Build/ Genome Fasta are conditionally required
+
+                    - example_required_vr_test.model.csv,
+                        PatientID attribute
+                            Required=False,
+                            validation rule: #Patient unique warning^^#Biospecimen unique required error
+                                meaning PatientID is required for the Biospecimen manifest (but not Patient)
+                        FileFormat attribute:
+                            Required = False
+                            validation rule: ^^#BulkRNA-seqAssay list required
+                                meaning for BulkRNA=seqAssay (only) FileFormat is conditionally required.
+                                Genome Build/ Genome Fasta are conditionally required
+            manifest_name, str: manfiest to run validation with
+                What Each Manifest is Testing:
+                    -Biospecimen_required_vr_test_fail: PatentID is required for Biospecimen in each model (through different routes) and not provided
+                    -Biospecimen_required_vr_test_pass: PatentID is required for Biospecimen in each model (through different routes) and provided
+                    -Patient_required_vr_test_pass: PatentID not provided, will fail for example model and pass for vr test model (where it is not required)
+                    -Patient_test_no_entry_for_cond_required_column: Tests conditionally required value not required if preceeding condition not met (helps test an edge case)
+                    -BulkRNAseq_component_based_required_rule_test: FileFormat, this manifest checks conditional requirements, provided for some rows and not for others
+
+            root_node, str: component for the given manifest
+            dmge, DataModelGraphExplorer Object
+        """
+
+        manifest_path = helpers.get_data_path(manifest_name)
+        metadataModel = get_metadataModel(helpers, model_name)
+
+        errors, warnings = metadataModel.validateModelManifest(
+            manifestPath=manifest_path,
+            rootNode=root_node,
+        )
+
+        error_and_warning_free_manifests = ["Biospecimen_required_vr_test_pass", "Patient_test_no_entry_for_cond_required_column", ""]
+
+        # For each model, these manifest should pass, bc either the value is being passed as requierd, or its not currently required
+        for manifest in error_and_warning_free_manifests:
+            if manifest_name in manifest:
+                assert errors == []
+                assert warnings == []
+
+        messages = {"patient_id_empty_warning": {
+                        "row_num":"2",
+                        "attribute_name":"Patient ID",
+                        "error_message":"'' should be non-empty",
+                        "invalid_entry":""},
+                    "bulk_rnaseq_cbr_error_1":{
+                        "row_num":"3",
+                        "attribute_name":"Genome FASTA",
+                        "error_message":"'' should be non-empty",
+                        "invalid_entry":""},
+                    "bulk_rnaseq_cbr_error_2":{
+                        "row_num":"4",
+                        "attribute_name":"File Format",
+                        "error_message":"'' is not one of ['CSV/TSV', 'CRAM', 'FASTQ', 'BAM']",
+                        "invalid_entry":""},
+            }
+
+        # This manifest should fail in the example_model bc the manifest Required=False, and in the example_with_requirements_from_vr
+        # bc the requirments are set to false in the validation rule
+        if (("Biospecimen_required_vr_test_fail" in manifest_name) or
+             ("Patient_required_vr_test_pass" in manifest_name and model_name == "example.model.csv")
+             ):
+            message_key = "patient_id_empty_warning"
+            assert GenerateError.generate_schema_error(
+                        row_num=messages[message_key]["row_num"],
+                        attribute_name=messages[message_key]["attribute_name"],
+                        error_message=messages[message_key]["error_message"],
+                        invalid_entry=messages[message_key]["invalid_entry"],
+                        dmge=dmge,
+                    )[0] in errors
+            assert warnings == []
+
+        if "Patient_required_vr_test_pass" in manifest_name and model_name == "example_required_vr_test.model.csv":
+            assert errors == []
+            assert warnings == []
+
+        if "BulkRNAseq_component_based_required_rule_test" in manifest_name:
+            message_key = "bulk_rnaseq_cbr_error_1"
+            assert GenerateError.generate_schema_error(
+                    row_num=messages[message_key]["row_num"],
+                    attribute_name=messages[message_key]["attribute_name"],
+                    error_message=messages[message_key]["error_message"],
+                    invalid_entry=messages[message_key]["invalid_entry"],
+                    dmge=dmge,
+                )[0] in errors
+
+            message_key = "bulk_rnaseq_cbr_error_2"
+            expected_error = GenerateError.generate_schema_error(
+                    row_num=messages[message_key]["row_num"],
+                    attribute_name=messages[message_key]["attribute_name"],
+                    error_message=messages[message_key]["error_message"],
+                    invalid_entry=messages[message_key]["invalid_entry"],
+                    dmge=dmge,
+                )[0]
+
+            # since the valid value order isnt set in error reporting, check a portion of the expected output
+            # Check the error row is expected
+            assert expected_error[1] in errors[1]
+            # Check that one of the values for the expected valid values is present
+            # Extract a valid value
+            valid_value = expected_error[2].split(',')[-1].split(']')[0].strip(' ').strip("\'")
+            assert  valid_value in errors[1][2]
+            assert warnings==[]
+
 
     @pytest.mark.parametrize(
         "manifest_path",
@@ -528,7 +708,7 @@ def test_component_validations(self, helpers, manifest_path, dmge):
     )
     @pytest.mark.parametrize("base_rule, second_rule", get_rule_combinations())
     def test_rule_combinations(
-        self, helpers, dmge, base_rule, second_rule, metadataModel
+        self, helpers, dmge, base_rule, second_rule,
     ):
         """
         TODO: Describe what this test is doing.
@@ -538,6 +718,8 @@ def test_rule_combinations(
         rule_regex = re.compile(base_rule + ".*")
         rootNode = "MockComponent"
 
+        metadataModel = get_metadataModel(helpers, model_name="example.model.jsonld")
+
         manifestPath = helpers.get_data_path("mock_manifests/Rule_Combo_Manifest.csv")
         manifest = helpers.get_data_frame(manifestPath)
 
diff --git a/tests/test_validator.py b/tests/test_validator.py
index 37ae008f6..6f5bd95f0 100644
--- a/tests/test_validator.py
+++ b/tests/test_validator.py
@@ -31,7 +31,7 @@ def graph_data_model_func(helpers, data_model_name):
     data_model_grapher = DataModelGraph(parsed_data_model)
 
     # Generate graph
-    graph_data_model = data_model_grapher.generate_data_model_graph()
+    graph_data_model = data_model_grapher.graph
 
     return graph_data_model