Repository: 1Password/connect-sdk-python (language: python)

Issue:
SDK to support async operations
### Summary
When downloading a bunch of items to initialize our app, we would like to benefit from async io operation to perform multiple bulk downloads. Current implementation force us to download these sequentially or having to resort to using threading which create a large overhead.

### Use cases
Initializing an app on runtime to download all needed passwords

### Proposed solution
Implement the internal request builder using built-in async.io or any other async library

### Is there a workaround to accomplish this today?
yes, running using threads

### References & Prior Work
no sure


Discussion / hints:

Fix patch:
diff --git a/README.md b/README.md
index a1834ad..8c0b468 100644
--- a/README.md
+++ b/README.md
@@ -29,7 +29,7 @@ Check the [Python Connect SDK Example](example/README.md) to see an example of i
 
    ```sh
    export OP_CONNECT_HOST=<your-connect-host> && \
-       export OP_CONNECT_TOKEN=<your-connect-token>
+   export OP_CONNECT_TOKEN=<your-connect-token>
    ```
 
 3. Use the SDK:
diff --git a/USAGE.md b/USAGE.md
index d53fb58..902b71b 100644
--- a/USAGE.md
+++ b/USAGE.md
@@ -21,6 +21,12 @@ connect_client_from_env: Client = new_client_from_environment()
 connect_client_from_token: Client = new_client(
     "{1Password_Connect_Host}",
     "{1Password_Connect_API_Token}")
+
+# creates async client
+connect_async_client: Client = new_client(
+    "{1Password_Connect_Host}",
+    "{1Password_Connect_API_Token}",
+    True)
 ```
 
 ## Environment Variables
@@ -32,6 +38,10 @@ connect_client_from_token: Client = new_client(
   - `http://localhost:8080` if the Connect server is running in Docker on the same host.
   - `http(s)://<ip>:8080` or `http(s)://<hostname>:8080` if the Connect server is running on another host.
 - **OP_VAULT** - The default vault to fetch items from if not specified.
+- **OP_CONNECT_CLIENT_ASYNC** - Whether to use async client or not. Possible values are:
+     - True - to use async client
+     - False - to use synchronous client (this is used by default)
+
 
 ## Working with Vaults
 
@@ -136,3 +146,24 @@ CONFIG = Config()
 
 values_object = onepasswordconnectsdk.load(connect_client, CONFIG)
 ```
+
+## Async client
+
+All the examples above can work using an async client.
+```python
+import asyncio
+
+# initialize async client by passing `is_async = True`
+async_client: Client = new_client(
+    "{1Password_Connect_Host}",
+    "{1Password_Connect_API_Token}",
+    True)
+
+async def main():
+    vaults = await async_client.get_vaults()
+    item = await async_client.get_item("{item_id}", "{vault_id}")
+    # do something with vaults and item
+    await async_client.session.aclose()  # close the client gracefully when you are done
+
+asyncio.run(main())
+```
\ No newline at end of file
diff --git a/poetry.lock b/poetry.lock
index 60cfaeb..6ccac8c 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -1,27 +1,33 @@
-# This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand.
+# This file is automatically @generated by Poetry 1.4.2 and should not be changed by hand.
 
 [[package]]
-name = "attrs"
-version = "22.2.0"
-description = "Classes Without Boilerplate"
+name = "anyio"
+version = "3.7.1"
+description = "High level compatibility layer for multiple asynchronous event loop implementations"
+category = "main"
 optional = false
-python-versions = ">=3.6"
+python-versions = ">=3.7"
 files = [
-    {file = "attrs-22.2.0-py3-none-any.whl", hash = "sha256:29e95c7f6778868dbd49170f98f8818f78f3dc5e0e37c0b1f474e3561b240836"},
-    {file = "attrs-22.2.0.tar.gz", hash = "sha256:c9227bfc2f01993c03f68db37d1d15c9690188323c067c641f1a35ca58185f99"},
+    {file = "anyio-3.7.1-py3-none-any.whl", hash = "sha256:91dee416e570e92c64041bd18b900d1d6fa78dff7048769ce5ac5ddad004fbb5"},
+    {file = "anyio-3.7.1.tar.gz", hash = "sha256:44a3c9aba0f5defa43261a8b3efb97891f2bd7d804e0e1f56419befa1adfc780"},
 ]
 
+[package.dependencies]
+exceptiongroup = {version = "*", markers = "python_version < \"3.11\""}
+idna = ">=2.8"
+sniffio = ">=1.1"
+typing-extensions = {version = "*", markers = "python_version < \"3.8\""}
+
 [package.extras]
-cov = ["attrs[tests]", "coverage-enable-subprocess", "coverage[toml] (>=5.3)"]
-dev = ["attrs[docs,tests]"]
-docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope.interface"]
-tests = ["attrs[tests-no-zope]", "zope.interface"]
-tests-no-zope = ["cloudpickle", "cloudpickle", "hypothesis", "hypothesis", "mypy (>=0.971,<0.990)", "mypy (>=0.971,<0.990)", "pympler", "pympler", "pytest (>=4.3.0)", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-mypy-plugins", "pytest-xdist[psutil]", "pytest-xdist[psutil]"]
+doc = ["Sphinx", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme (>=1.2.2)", "sphinxcontrib-jquery"]
+test = ["anyio[trio]", "coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "mock (>=4)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"]
+trio = ["trio (<0.22)"]
 
 [[package]]
 name = "certifi"
 version = "2023.7.22"
 description = "Python package for providing Mozilla's CA Bundle."
+category = "main"
 optional = false
 python-versions = ">=3.6"
 files = [
@@ -29,24 +35,11 @@ files = [
     {file = "certifi-2023.7.22.tar.gz", hash = "sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082"},
 ]
 
-[[package]]
-name = "charset-normalizer"
-version = "2.1.1"
-description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
-optional = false
-python-versions = ">=3.6.0"
-files = [
-    {file = "charset-normalizer-2.1.1.tar.gz", hash = "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845"},
-    {file = "charset_normalizer-2.1.1-py3-none-any.whl", hash = "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f"},
-]
-
-[package.extras]
-unicode-backport = ["unicodedata2"]
-
 [[package]]
 name = "colorama"
 version = "0.4.6"
 description = "Cross-platform colored terminal text."
+category = "dev"
 optional = false
 python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
 files = [
@@ -56,62 +49,72 @@ files = [
 
 [[package]]
 name = "coverage"
-version = "7.0.1"
+version = "7.2.7"
 description = "Code coverage measurement for Python"
+category = "dev"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "coverage-7.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b3695c4f4750bca943b3e1f74ad4be8d29e4aeab927d50772c41359107bd5d5c"},
-    {file = "coverage-7.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fa6a5a224b7f4cfb226f4fc55a57e8537fcc096f42219128c2c74c0e7d0953e1"},
-    {file = "coverage-7.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:74f70cd92669394eaf8d7756d1b195c8032cf7bbbdfce3bc489d4e15b3b8cf73"},
-    {file = "coverage-7.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b66bb21a23680dee0be66557dc6b02a3152ddb55edf9f6723fa4a93368f7158d"},
-    {file = "coverage-7.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d87717959d4d0ee9db08a0f1d80d21eb585aafe30f9b0a54ecf779a69cb015f6"},
-    {file = "coverage-7.0.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:854f22fa361d1ff914c7efa347398374cc7d567bdafa48ac3aa22334650dfba2"},
-    {file = "coverage-7.0.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:1e414dc32ee5c3f36544ea466b6f52f28a7af788653744b8570d0bf12ff34bc0"},
-    {file = "coverage-7.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6c5ad996c6fa4d8ed669cfa1e8551348729d008a2caf81489ab9ea67cfbc7498"},
-    {file = "coverage-7.0.1-cp310-cp310-win32.whl", hash = "sha256:691571f31ace1837838b7e421d3a09a8c00b4aac32efacb4fc9bd0a5c647d25a"},
-    {file = "coverage-7.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:89caf4425fe88889e2973a8e9a3f6f5f9bbe5dd411d7d521e86428c08a873a4a"},
-    {file = "coverage-7.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:63d56165a7c76265468d7e0c5548215a5ba515fc2cba5232d17df97bffa10f6c"},
-    {file = "coverage-7.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4f943a3b2bc520102dd3e0bb465e1286e12c9a54f58accd71b9e65324d9c7c01"},
-    {file = "coverage-7.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:830525361249dc4cd013652b0efad645a385707a5ae49350c894b67d23fbb07c"},
-    {file = "coverage-7.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd1b9c5adc066db699ccf7fa839189a649afcdd9e02cb5dc9d24e67e7922737d"},
-    {file = "coverage-7.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e00c14720b8b3b6c23b487e70bd406abafc976ddc50490f645166f111c419c39"},
-    {file = "coverage-7.0.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:6d55d840e1b8c0002fce66443e124e8581f30f9ead2e54fbf6709fb593181f2c"},
-    {file = "coverage-7.0.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:66b18c3cf8bbab0cce0d7b9e4262dc830e93588986865a8c78ab2ae324b3ed56"},
-    {file = "coverage-7.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:12a5aa77783d49e05439fbe6e6b427484f8a0f9f456b46a51d8aac022cfd024d"},
-    {file = "coverage-7.0.1-cp311-cp311-win32.whl", hash = "sha256:b77015d1cb8fe941be1222a5a8b4e3fbca88180cfa7e2d4a4e58aeabadef0ab7"},
-    {file = "coverage-7.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:fb992c47cb1e5bd6a01e97182400bcc2ba2077080a17fcd7be23aaa6e572e390"},
-    {file = "coverage-7.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e78e9dcbf4f3853d3ae18a8f9272111242531535ec9e1009fa8ec4a2b74557dc"},
-    {file = "coverage-7.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e60bef2e2416f15fdc05772bf87db06c6a6f9870d1db08fdd019fbec98ae24a9"},
-    {file = "coverage-7.0.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9823e4789ab70f3ec88724bba1a203f2856331986cd893dedbe3e23a6cfc1e4e"},
-    {file = "coverage-7.0.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9158f8fb06747ac17bd237930c4372336edc85b6e13bdc778e60f9d685c3ca37"},
-    {file = "coverage-7.0.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:486ee81fa694b4b796fc5617e376326a088f7b9729c74d9defa211813f3861e4"},
-    {file = "coverage-7.0.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:1285648428a6101b5f41a18991c84f1c3959cee359e51b8375c5882fc364a13f"},
-    {file = "coverage-7.0.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2c44fcfb3781b41409d0f060a4ed748537557de9362a8a9282182fafb7a76ab4"},
-    {file = "coverage-7.0.1-cp37-cp37m-win32.whl", hash = "sha256:d6814854c02cbcd9c873c0f3286a02e3ac1250625cca822ca6bc1018c5b19f1c"},
-    {file = "coverage-7.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:f66460f17c9319ea4f91c165d46840314f0a7c004720b20be58594d162a441d8"},
-    {file = "coverage-7.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9b373c9345c584bb4b5f5b8840df7f4ab48c4cbb7934b58d52c57020d911b856"},
-    {file = "coverage-7.0.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d3022c3007d3267a880b5adcf18c2a9bf1fc64469b394a804886b401959b8742"},
-    {file = "coverage-7.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:92651580bd46519067e36493acb394ea0607b55b45bd81dd4e26379ed1871f55"},
-    {file = "coverage-7.0.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3cfc595d2af13856505631be072835c59f1acf30028d1c860b435c5fc9c15b69"},
-    {file = "coverage-7.0.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b4b3a4d9915b2be879aff6299c0a6129f3d08a775d5a061f503cf79571f73e4"},
-    {file = "coverage-7.0.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b6f22bb64cc39bcb883e5910f99a27b200fdc14cdd79df8696fa96b0005c9444"},
-    {file = "coverage-7.0.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:72d1507f152abacea81f65fee38e4ef3ac3c02ff8bc16f21d935fd3a8a4ad910"},
-    {file = "coverage-7.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:0a79137fc99815fff6a852c233628e735ec15903cfd16da0f229d9c4d45926ab"},
-    {file = "coverage-7.0.1-cp38-cp38-win32.whl", hash = "sha256:b3763e7fcade2ff6c8e62340af9277f54336920489ceb6a8cd6cc96da52fcc62"},
-    {file = "coverage-7.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:09f6b5a8415b6b3e136d5fec62b552972187265cb705097bf030eb9d4ffb9b60"},
-    {file = "coverage-7.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:978258fec36c154b5e250d356c59af7d4c3ba02bef4b99cda90b6029441d797d"},
-    {file = "coverage-7.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:19ec666533f0f70a0993f88b8273057b96c07b9d26457b41863ccd021a043b9a"},
-    {file = "coverage-7.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cfded268092a84605f1cc19e5c737f9ce630a8900a3589e9289622db161967e9"},
-    {file = "coverage-7.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07bcfb1d8ac94af886b54e18a88b393f6a73d5959bb31e46644a02453c36e475"},
-    {file = "coverage-7.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:397b4a923cc7566bbc7ae2dfd0ba5a039b61d19c740f1373791f2ebd11caea59"},
-    {file = "coverage-7.0.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:aec2d1515d9d39ff270059fd3afbb3b44e6ec5758af73caf18991807138c7118"},
-    {file = "coverage-7.0.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:c20cfebcc149a4c212f6491a5f9ff56f41829cd4f607b5be71bb2d530ef243b1"},
-    {file = "coverage-7.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:fd556ff16a57a070ce4f31c635953cc44e25244f91a0378c6e9bdfd40fdb249f"},
-    {file = "coverage-7.0.1-cp39-cp39-win32.whl", hash = "sha256:b9ea158775c7c2d3e54530a92da79496fb3fb577c876eec761c23e028f1e216c"},
-    {file = "coverage-7.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:d1991f1dd95eba69d2cd7708ff6c2bbd2426160ffc73c2b81f617a053ebcb1a8"},
-    {file = "coverage-7.0.1-pp37.pp38.pp39-none-any.whl", hash = "sha256:3dd4ee135e08037f458425b8842d24a95a0961831a33f89685ff86b77d378f89"},
-    {file = "coverage-7.0.1.tar.gz", hash = "sha256:a4a574a19eeb67575a5328a5760bbbb737faa685616586a9f9da4281f940109c"},
+    {file = "coverage-7.2.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d39b5b4f2a66ccae8b7263ac3c8170994b65266797fb96cbbfd3fb5b23921db8"},
+    {file = "coverage-7.2.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6d040ef7c9859bb11dfeb056ff5b3872436e3b5e401817d87a31e1750b9ae2fb"},
+    {file = "coverage-7.2.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba90a9563ba44a72fda2e85302c3abc71c5589cea608ca16c22b9804262aaeb6"},
+    {file = "coverage-7.2.7-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7d9405291c6928619403db1d10bd07888888ec1abcbd9748fdaa971d7d661b2"},
+    {file = "coverage-7.2.7-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31563e97dae5598556600466ad9beea39fb04e0229e61c12eaa206e0aa202063"},
+    {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ebba1cd308ef115925421d3e6a586e655ca5a77b5bf41e02eb0e4562a111f2d1"},
+    {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:cb017fd1b2603ef59e374ba2063f593abe0fc45f2ad9abdde5b4d83bd922a353"},
+    {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d62a5c7dad11015c66fbb9d881bc4caa5b12f16292f857842d9d1871595f4495"},
+    {file = "coverage-7.2.7-cp310-cp310-win32.whl", hash = "sha256:ee57190f24fba796e36bb6d3aa8a8783c643d8fa9760c89f7a98ab5455fbf818"},
+    {file = "coverage-7.2.7-cp310-cp310-win_amd64.whl", hash = "sha256:f75f7168ab25dd93110c8a8117a22450c19976afbc44234cbf71481094c1b850"},
+    {file = "coverage-7.2.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:06a9a2be0b5b576c3f18f1a241f0473575c4a26021b52b2a85263a00f034d51f"},
+    {file = "coverage-7.2.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5baa06420f837184130752b7c5ea0808762083bf3487b5038d68b012e5937dbe"},
+    {file = "coverage-7.2.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdec9e8cbf13a5bf63290fc6013d216a4c7232efb51548594ca3631a7f13c3a3"},
+    {file = "coverage-7.2.7-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:52edc1a60c0d34afa421c9c37078817b2e67a392cab17d97283b64c5833f427f"},
+    {file = "coverage-7.2.7-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63426706118b7f5cf6bb6c895dc215d8a418d5952544042c8a2d9fe87fcf09cb"},
+    {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:afb17f84d56068a7c29f5fa37bfd38d5aba69e3304af08ee94da8ed5b0865833"},
+    {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:48c19d2159d433ccc99e729ceae7d5293fbffa0bdb94952d3579983d1c8c9d97"},
+    {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0e1f928eaf5469c11e886fe0885ad2bf1ec606434e79842a879277895a50942a"},
+    {file = "coverage-7.2.7-cp311-cp311-win32.whl", hash = "sha256:33d6d3ea29d5b3a1a632b3c4e4f4ecae24ef170b0b9ee493883f2df10039959a"},
+    {file = "coverage-7.2.7-cp311-cp311-win_amd64.whl", hash = "sha256:5b7540161790b2f28143191f5f8ec02fb132660ff175b7747b95dcb77ac26562"},
+    {file = "coverage-7.2.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f2f67fe12b22cd130d34d0ef79206061bfb5eda52feb6ce0dba0644e20a03cf4"},
+    {file = "coverage-7.2.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a342242fe22407f3c17f4b499276a02b01e80f861f1682ad1d95b04018e0c0d4"},
+    {file = "coverage-7.2.7-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:171717c7cb6b453aebac9a2ef603699da237f341b38eebfee9be75d27dc38e01"},
+    {file = "coverage-7.2.7-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49969a9f7ffa086d973d91cec8d2e31080436ef0fb4a359cae927e742abfaaa6"},
+    {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b46517c02ccd08092f4fa99f24c3b83d8f92f739b4657b0f146246a0ca6a831d"},
+    {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:a3d33a6b3eae87ceaefa91ffdc130b5e8536182cd6dfdbfc1aa56b46ff8c86de"},
+    {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:976b9c42fb2a43ebf304fa7d4a310e5f16cc99992f33eced91ef6f908bd8f33d"},
+    {file = "coverage-7.2.7-cp312-cp312-win32.whl", hash = "sha256:8de8bb0e5ad103888d65abef8bca41ab93721647590a3f740100cd65c3b00511"},
+    {file = "coverage-7.2.7-cp312-cp312-win_amd64.whl", hash = "sha256:9e31cb64d7de6b6f09702bb27c02d1904b3aebfca610c12772452c4e6c21a0d3"},
+    {file = "coverage-7.2.7-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:58c2ccc2f00ecb51253cbe5d8d7122a34590fac9646a960d1430d5b15321d95f"},
+    {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d22656368f0e6189e24722214ed8d66b8022db19d182927b9a248a2a8a2f67eb"},
+    {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a895fcc7b15c3fc72beb43cdcbdf0ddb7d2ebc959edac9cef390b0d14f39f8a9"},
+    {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e84606b74eb7de6ff581a7915e2dab7a28a0517fbe1c9239eb227e1354064dcd"},
+    {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:0a5f9e1dbd7fbe30196578ca36f3fba75376fb99888c395c5880b355e2875f8a"},
+    {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:419bfd2caae268623dd469eff96d510a920c90928b60f2073d79f8fe2bbc5959"},
+    {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2aee274c46590717f38ae5e4650988d1af340fe06167546cc32fe2f58ed05b02"},
+    {file = "coverage-7.2.7-cp37-cp37m-win32.whl", hash = "sha256:61b9a528fb348373c433e8966535074b802c7a5d7f23c4f421e6c6e2f1697a6f"},
+    {file = "coverage-7.2.7-cp37-cp37m-win_amd64.whl", hash = "sha256:b1c546aca0ca4d028901d825015dc8e4d56aac4b541877690eb76490f1dc8ed0"},
+    {file = "coverage-7.2.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:54b896376ab563bd38453cecb813c295cf347cf5906e8b41d340b0321a5433e5"},
+    {file = "coverage-7.2.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3d376df58cc111dc8e21e3b6e24606b5bb5dee6024f46a5abca99124b2229ef5"},
+    {file = "coverage-7.2.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e330fc79bd7207e46c7d7fd2bb4af2963f5f635703925543a70b99574b0fea9"},
+    {file = "coverage-7.2.7-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e9d683426464e4a252bf70c3498756055016f99ddaec3774bf368e76bbe02b6"},
+    {file = "coverage-7.2.7-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d13c64ee2d33eccf7437961b6ea7ad8673e2be040b4f7fd4fd4d4d28d9ccb1e"},
+    {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b7aa5f8a41217360e600da646004f878250a0d6738bcdc11a0a39928d7dc2050"},
+    {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8fa03bce9bfbeeef9f3b160a8bed39a221d82308b4152b27d82d8daa7041fee5"},
+    {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:245167dd26180ab4c91d5e1496a30be4cd721a5cf2abf52974f965f10f11419f"},
+    {file = "coverage-7.2.7-cp38-cp38-win32.whl", hash = "sha256:d2c2db7fd82e9b72937969bceac4d6ca89660db0a0967614ce2481e81a0b771e"},
+    {file = "coverage-7.2.7-cp38-cp38-win_amd64.whl", hash = "sha256:2e07b54284e381531c87f785f613b833569c14ecacdcb85d56b25c4622c16c3c"},
+    {file = "coverage-7.2.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:537891ae8ce59ef63d0123f7ac9e2ae0fc8b72c7ccbe5296fec45fd68967b6c9"},
+    {file = "coverage-7.2.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:06fb182e69f33f6cd1d39a6c597294cff3143554b64b9825d1dc69d18cc2fff2"},
+    {file = "coverage-7.2.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:201e7389591af40950a6480bd9edfa8ed04346ff80002cec1a66cac4549c1ad7"},
+    {file = "coverage-7.2.7-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f6951407391b639504e3b3be51b7ba5f3528adbf1a8ac3302b687ecababf929e"},
+    {file = "coverage-7.2.7-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f48351d66575f535669306aa7d6d6f71bc43372473b54a832222803eb956fd1"},
+    {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b29019c76039dc3c0fd815c41392a044ce555d9bcdd38b0fb60fb4cd8e475ba9"},
+    {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:81c13a1fc7468c40f13420732805a4c38a105d89848b7c10af65a90beff25250"},
+    {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:975d70ab7e3c80a3fe86001d8751f6778905ec723f5b110aed1e450da9d4b7f2"},
+    {file = "coverage-7.2.7-cp39-cp39-win32.whl", hash = "sha256:7ee7d9d4822c8acc74a5e26c50604dff824710bc8de424904c0982e25c39c6cb"},
+    {file = "coverage-7.2.7-cp39-cp39-win_amd64.whl", hash = "sha256:eb393e5ebc85245347950143969b241d08b52b88a3dc39479822e073a1a8eb27"},
+    {file = "coverage-7.2.7-pp37.pp38.pp39-none-any.whl", hash = "sha256:b7b4c971f05e6ae490fef852c218b0e79d4e52f79ef0c8475566584a8fb3e01d"},
+    {file = "coverage-7.2.7.tar.gz", hash = "sha256:924d94291ca674905fe9481f12294eb11f2d3d3fd1adb20314ba89e94f44ed59"},
 ]
 
 [package.dependencies]
@@ -122,22 +125,85 @@ toml = ["tomli"]
 
 [[package]]
 name = "exceptiongroup"
-version = "1.1.0"
+version = "1.1.3"
 description = "Backport of PEP 654 (exception groups)"
+category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "exceptiongroup-1.1.0-py3-none-any.whl", hash = "sha256:327cbda3da756e2de031a3107b81ab7b3770a602c4d16ca618298c526f4bec1e"},
-    {file = "exceptiongroup-1.1.0.tar.gz", hash = "sha256:bcb67d800a4497e1b404c2dd44fca47d3b7a5e5433dbab67f96c1a685cdfdf23"},
+    {file = "exceptiongroup-1.1.3-py3-none-any.whl", hash = "sha256:343280667a4585d195ca1cf9cef84a4e178c4b6cf2274caef9859782b567d5e3"},
+    {file = "exceptiongroup-1.1.3.tar.gz", hash = "sha256:097acd85d473d75af5bb98e41b61ff7fe35efe6675e4f9370ec6ec5126d160e9"},
 ]
 
 [package.extras]
 test = ["pytest (>=6)"]
 
+[[package]]
+name = "h11"
+version = "0.14.0"
+description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"},
+    {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"},
+]
+
+[package.dependencies]
+typing-extensions = {version = "*", markers = "python_version < \"3.8\""}
+
+[[package]]
+name = "httpcore"
+version = "0.16.3"
+description = "A minimal low-level HTTP client."
+category = "main"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "httpcore-0.16.3-py3-none-any.whl", hash = "sha256:da1fb708784a938aa084bde4feb8317056c55037247c787bd7e19eb2c2949dc0"},
+    {file = "httpcore-0.16.3.tar.gz", hash = "sha256:c5d6f04e2fc530f39e0c077e6a30caa53f1451096120f1f38b954afd0b17c0cb"},
+]
+
+[package.dependencies]
+anyio = ">=3.0,<5.0"
+certifi = "*"
+h11 = ">=0.13,<0.15"
+sniffio = ">=1.0.0,<2.0.0"
+
+[package.extras]
+http2 = ["h2 (>=3,<5)"]
+socks = ["socksio (>=1.0.0,<2.0.0)"]
+
+[[package]]
+name = "httpx"
+version = "0.23.3"
+description = "The next generation HTTP client."
+category = "main"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "httpx-0.23.3-py3-none-any.whl", hash = "sha256:a211fcce9b1254ea24f0cd6af9869b3d29aba40154e947d2a07bb499b3e310d6"},
+    {file = "httpx-0.23.3.tar.gz", hash = "sha256:9818458eb565bb54898ccb9b8b251a28785dd4a55afbc23d0eb410754fe7d0f9"},
+]
+
+[package.dependencies]
+certifi = "*"
+httpcore = ">=0.15.0,<0.17.0"
+rfc3986 = {version = ">=1.3,<2", extras = ["idna2008"]}
+sniffio = "*"
+
+[package.extras]
+brotli = ["brotli", "brotlicffi"]
+cli = ["click (>=8.0.0,<9.0.0)", "pygments (>=2.0.0,<3.0.0)", "rich (>=10,<13)"]
+http2 = ["h2 (>=3,<5)"]
+socks = ["socksio (>=1.0.0,<2.0.0)"]
+
 [[package]]
 name = "idna"
 version = "3.4"
 description = "Internationalized Domain Names in Applications (IDNA)"
+category = "main"
 optional = false
 python-versions = ">=3.5"
 files = [
@@ -147,13 +213,14 @@ files = [
 
 [[package]]
 name = "importlib-metadata"
-version = "5.2.0"
+version = "6.7.0"
 description = "Read metadata from Python packages"
+category = "dev"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "importlib_metadata-5.2.0-py3-none-any.whl", hash = "sha256:0eafa39ba42bf225fc00e67f701d71f85aead9f878569caf13c3724f704b970f"},
-    {file = "importlib_metadata-5.2.0.tar.gz", hash = "sha256:404d48d62bba0b7a77ff9d405efd91501bef2e67ff4ace0bed40a0cf28c3c7cd"},
+    {file = "importlib_metadata-6.7.0-py3-none-any.whl", hash = "sha256:cb52082e659e97afc5dac71e79de97d8681de3aa07ff18578330904a9d18e5b5"},
+    {file = "importlib_metadata-6.7.0.tar.gz", hash = "sha256:1aaf550d4f73e5d6783e7acb77aec43d49da8017410afae93822cc9cca98c4d4"},
 ]
 
 [package.dependencies]
@@ -163,39 +230,42 @@ zipp = ">=0.5"
 [package.extras]
 docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
 perf = ["ipython"]
-testing = ["flake8 (<5)", "flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)"]
+testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)", "pytest-ruff"]
 
 [[package]]
 name = "iniconfig"
-version = "1.1.1"
-description = "iniconfig: brain-dead simple config-ini parsing"
+version = "2.0.0"
+description = "brain-dead simple config-ini parsing"
+category = "dev"
 optional = false
-python-versions = "*"
+python-versions = ">=3.7"
 files = [
-    {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"},
-    {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"},
+    {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"},
+    {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"},
 ]
 
 [[package]]
 name = "packaging"
-version = "22.0"
+version = "23.1"
 description = "Core utilities for Python packages"
+category = "dev"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "packaging-22.0-py3-none-any.whl", hash = "sha256:957e2148ba0e1a3b282772e791ef1d8083648bc131c8ab0c1feba110ce1146c3"},
-    {file = "packaging-22.0.tar.gz", hash = "sha256:2198ec20bd4c017b8f9717e00f0c8714076fc2fd93816750ab48e2c41de2cfd3"},
+    {file = "packaging-23.1-py3-none-any.whl", hash = "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61"},
+    {file = "packaging-23.1.tar.gz", hash = "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f"},
 ]
 
 [[package]]
 name = "pluggy"
-version = "1.0.0"
+version = "1.2.0"
 description = "plugin and hook calling mechanisms for python"
+category = "dev"
 optional = false
-python-versions = ">=3.6"
+python-versions = ">=3.7"
 files = [
-    {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"},
-    {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"},
+    {file = "pluggy-1.2.0-py3-none-any.whl", hash = "sha256:c2fd55a7d7a3863cba1a013e4e2414658b1d07b6bc57b3919e0c63c9abb99849"},
+    {file = "pluggy-1.2.0.tar.gz", hash = "sha256:d12f0c4b579b15f5e054301bb226ee85eeeba08ffec228092f8defbaa3a4c4b3"},
 ]
 
 [package.dependencies]
@@ -207,17 +277,17 @@ testing = ["pytest", "pytest-benchmark"]
 
 [[package]]
 name = "pytest"
-version = "7.2.0"
+version = "7.4.0"
 description = "pytest: simple powerful testing with Python"
+category = "dev"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "pytest-7.2.0-py3-none-any.whl", hash = "sha256:892f933d339f068883b6fd5a459f03d85bfcb355e4981e146d2c7616c21fef71"},
-    {file = "pytest-7.2.0.tar.gz", hash = "sha256:c4014eb40e10f11f355ad4e3c2fb2c6c6d1919c73f3b5a433de4708202cade59"},
+    {file = "pytest-7.4.0-py3-none-any.whl", hash = "sha256:78bf16451a2eb8c7a2ea98e32dc119fd2aa758f1d5d66dbf0a59d69a3969df32"},
+    {file = "pytest-7.4.0.tar.gz", hash = "sha256:b4bf8c45bd59934ed84001ad51e11b4ee40d40a1229d2c79f9c592b0a3f6bd8a"},
 ]
 
 [package.dependencies]
-attrs = ">=19.2.0"
 colorama = {version = "*", markers = "sys_platform == \"win32\""}
 exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""}
 importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""}
@@ -227,17 +297,38 @@ pluggy = ">=0.12,<2.0"
 tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""}
 
 [package.extras]
-testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"]
+testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"]
+
+[[package]]
+name = "pytest-asyncio"
+version = "0.20.3"
+description = "Pytest support for asyncio"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "pytest-asyncio-0.20.3.tar.gz", hash = "sha256:83cbf01169ce3e8eb71c6c278ccb0574d1a7a3bb8eaaf5e50e0ad342afb33b36"},
+    {file = "pytest_asyncio-0.20.3-py3-none-any.whl", hash = "sha256:f129998b209d04fcc65c96fc85c11e5316738358909a8399e93be553d7656442"},
+]
+
+[package.dependencies]
+pytest = ">=6.1.0"
+typing-extensions = {version = ">=3.7.2", markers = "python_version < \"3.8\""}
+
+[package.extras]
+docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"]
+testing = ["coverage (>=6.2)", "flaky (>=3.5.0)", "hypothesis (>=5.7.1)", "mypy (>=0.931)", "pytest-trio (>=0.7.0)"]
 
 [[package]]
 name = "pytest-cov"
-version = "4.0.0"
+version = "4.1.0"
 description = "Pytest plugin for measuring coverage."
+category = "dev"
 optional = false
-python-versions = ">=3.6"
+python-versions = ">=3.7"
 files = [
-    {file = "pytest-cov-4.0.0.tar.gz", hash = "sha256:996b79efde6433cdbd0088872dbc5fb3ed7fe1578b68cdbba634f14bb8dd0470"},
-    {file = "pytest_cov-4.0.0-py3-none-any.whl", hash = "sha256:2feb1b751d66a8bd934e5edfa2e961d11309dc37b73b0eabe73b5945fee20f6b"},
+    {file = "pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6"},
+    {file = "pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"},
 ]
 
 [package.dependencies]
@@ -251,6 +342,7 @@ testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtuale
 name = "python-dateutil"
 version = "2.8.2"
 description = "Extensions to the standard Python datetime module"
+category = "main"
 optional = false
 python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
 files = [
@@ -262,30 +354,43 @@ files = [
 six = ">=1.5"
 
 [[package]]
-name = "requests"
-version = "2.31.0"
-description = "Python HTTP for Humans."
+name = "respx"
+version = "0.20.2"
+description = "A utility for mocking out the Python HTTPX and HTTP Core libraries."
+category = "dev"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"},
-    {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"},
+    {file = "respx-0.20.2-py2.py3-none-any.whl", hash = "sha256:ab8e1cf6da28a5b2dd883ea617f8130f77f676736e6e9e4a25817ad116a172c9"},
+    {file = "respx-0.20.2.tar.gz", hash = "sha256:07cf4108b1c88b82010f67d3c831dae33a375c7b436e54d87737c7f9f99be643"},
+]
+
+[package.dependencies]
+httpx = ">=0.21.0"
+
+[[package]]
+name = "rfc3986"
+version = "1.5.0"
+description = "Validating URI References per RFC 3986"
+category = "main"
+optional = false
+python-versions = "*"
+files = [
+    {file = "rfc3986-1.5.0-py2.py3-none-any.whl", hash = "sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97"},
+    {file = "rfc3986-1.5.0.tar.gz", hash = "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835"},
 ]
 
 [package.dependencies]
-certifi = ">=2017.4.17"
-charset-normalizer = ">=2,<4"
-idna = ">=2.5,<4"
-urllib3 = ">=1.21.1,<3"
+idna = {version = "*", optional = true, markers = "extra == \"idna2008\""}
 
 [package.extras]
-socks = ["PySocks (>=1.5.6,!=1.5.7)"]
-use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
+idna2008 = ["idna"]
 
 [[package]]
 name = "six"
 version = "1.16.0"
 description = "Python 2 and 3 compatibility utilities"
+category = "main"
 optional = false
 python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
 files = [
@@ -293,10 +398,23 @@ files = [
     {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
 ]
 
+[[package]]
+name = "sniffio"
+version = "1.3.0"
+description = "Sniff out which async library your code is running under"
+category = "main"
+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"},
+]
+
 [[package]]
 name = "tomli"
 version = "2.0.1"
 description = "A lil' TOML parser"
+category = "dev"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -306,47 +424,33 @@ files = [
 
 [[package]]
 name = "typing-extensions"
-version = "4.4.0"
+version = "4.7.1"
 description = "Backported and Experimental Type Hints for Python 3.7+"
+category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "typing_extensions-4.4.0-py3-none-any.whl", hash = "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e"},
-    {file = "typing_extensions-4.4.0.tar.gz", hash = "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa"},
+    {file = "typing_extensions-4.7.1-py3-none-any.whl", hash = "sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36"},
+    {file = "typing_extensions-4.7.1.tar.gz", hash = "sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2"},
 ]
 
-[[package]]
-name = "urllib3"
-version = "1.26.13"
-description = "HTTP library with thread-safe connection pooling, file post, and more."
-optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*"
-files = [
-    {file = "urllib3-1.26.13-py2.py3-none-any.whl", hash = "sha256:47cc05d99aaa09c9e72ed5809b60e7ba354e64b59c9c173ac3018642d8bb41fc"},
-    {file = "urllib3-1.26.13.tar.gz", hash = "sha256:c083dd0dce68dbfbe1129d5271cb90f9447dea7d52097c6e0126120c521ddea8"},
-]
-
-[package.extras]
-brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"]
-secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"]
-socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]
-
 [[package]]
 name = "zipp"
-version = "3.11.0"
+version = "3.15.0"
 description = "Backport of pathlib-compatible object wrapper for zip files"
+category = "dev"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "zipp-3.11.0-py3-none-any.whl", hash = "sha256:83a28fcb75844b5c0cdaf5aa4003c2d728c77e05f5aeabe8e95e56727005fbaa"},
-    {file = "zipp-3.11.0.tar.gz", hash = "sha256:a7a22e05929290a67401440b39690ae6563279bced5f314609d9d03798f56766"},
+    {file = "zipp-3.15.0-py3-none-any.whl", hash = "sha256:48904fc76a60e542af151aded95726c1a5c34ed43ab4134b597665c86d7ad556"},
+    {file = "zipp-3.15.0.tar.gz", hash = "sha256:112929ad649da941c23de50f356a2b5570c954b65150642bccdd66bf194d224b"},
 ]
 
 [package.extras]
-docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)"]
-testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"]
+docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
+testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"]
 
 [metadata]
 lock-version = "2.0"
 python-versions = "^3.7"
-content-hash = "cac4a5815b13269da64f86e0b0481742124eba1200928ffb3fdd605c12d5c3c8"
+content-hash = "5c06df5db167647617c8fb7afc9da451dcec9d10e24df37e6024124cccd6da60"
diff --git a/pyproject.toml b/pyproject.toml
index 7f2595e..4b9d1c6 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -13,12 +13,14 @@ repository = "https://github.com/1Password/connect-sdk-python"
 
 [tool.poetry.dependencies]
 python = "^3.7"
-requests = "^2.24.0"
 python-dateutil = "^2.8.1"
+httpx = "^0.23.3"
 
-[tool.poetry.dev-dependencies]
+[tool.poetry.group.dev.dependencies]
 pytest = "^7.2.0"
+pytest-asyncio = "^0.20.3"
 pytest-cov = "^4.0.0"
+respx = "^0.20.1"
 
 [build-system]
 requires = ["poetry>=0.12"]
diff --git a/src/onepasswordconnectsdk/async_client.py b/src/onepasswordconnectsdk/async_client.py
new file mode 100644
index 0000000..415924c
--- /dev/null
+++ b/src/onepasswordconnectsdk/async_client.py
@@ -0,0 +1,373 @@
+"""Python AsyncClient for connecting to 1Password Connect"""
+import httpx
+from httpx import HTTPError
+import json
+import os
+
+from onepasswordconnectsdk.serializer import Serializer
+from onepasswordconnectsdk.utils import build_headers, is_valid_uuid, PathBuilder
+from onepasswordconnectsdk.errors import (
+    FailedToRetrieveItemException,
+    FailedToRetrieveVaultException,
+)
+from onepasswordconnectsdk.models import Item, ItemVault
+
+
+class AsyncClient:
+    """Python Async Client Class"""
+
+    def __init__(self, url: str, token: str):
+        """Initialize async client"""
+        self.url = url
+        self.token = token
+        self.session = self.create_session(url, token)
+        self.serializer = Serializer()
+
+    def create_session(self, url: str, token: str):
+        return httpx.AsyncClient(base_url=url, headers=self.build_headers(token))
+
+    def build_headers(self, token: str):
+        return build_headers(token)
+
+    async def __aexit__(self):
+        await self.session.aclose()
+
+    async def get_file(self, file_id: str, item_id: str, vault_id: str):
+        url = PathBuilder().vaults(vault_id).items(item_id).files(file_id).build()
+        response = await self.build_request("GET", url)
+        try:
+            response.raise_for_status()
+        except HTTPError:
+            raise FailedToRetrieveItemException(
+                f"Unable to retrieve item. Received {response.status_code}\
+                                     for {url} with message: {response.json().get('message')}"
+            )
+        return self.serializer.deserialize(response.content, "File")
+
+    async def get_files(self, item_id: str, vault_id: str):
+        url = PathBuilder().vaults(vault_id).items(item_id).files().build()
+        response = await self.build_request("GET", url)
+        try:
+            response.raise_for_status()
+        except HTTPError:
+            raise FailedToRetrieveItemException(
+                f"Unable to retrieve item. Received {response.status_code}\
+                             for {url} with message: {response.json().get('message')}"
+            )
+        return self.serializer.deserialize(response.content, "list[File]")
+
+    async def get_file_content(self, file_id: str, item_id: str, vault_id: str, content_path: str = None):
+        url = content_path
+        if content_path is None:
+            url = PathBuilder().vaults(vault_id).items(item_id).files(file_id).content().build()
+        response = await self.build_request("GET", url)
+        try:
+            response.raise_for_status()
+        except HTTPError:
+            raise FailedToRetrieveItemException(
+                f"Unable to retrieve items. Received {response.status_code} \
+                     for {url} with message: {response.json().get('message')}"
+            )
+        return response.content
+
+    async def download_file(self, file_id: str, item_id: str, vault_id: str, path: str):
+        file_object = await self.get_file(file_id, item_id, vault_id)
+        filename = file_object.name or "1password_item_file.txt"
+        content = await self.get_file_content(file_id, item_id, vault_id, file_object.content_path)
+        global_path = os.path.join(path, filename)
+
+        file = open(global_path, "wb")
+        file.write(content)
+        file.close()
+
+    async def get_item(self, item: str, vault: str):
+        """Get a specific item
+
+        Args:
+            item (str): the id or title of the item to be fetched
+            vault (str): the id or name of the vault in which to get the item from
+
+        Raises:
+            FailedToRetrieveItemException: Thrown when a HTTP error is returned
+            from the 1Password Connect API
+
+        Returns:
+            Item object: The found item
+        """
+
+        vault_id = vault
+        if not is_valid_uuid(vault):
+            vault = await self.get_vault_by_title(vault)
+            vault_id = vault.id
+
+        if is_valid_uuid(item):
+            return await self.get_item_by_id(item, vault_id)
+        else:
+            return await self.get_item_by_title(item, vault_id)
+
+    async def get_item_by_id(self, item_id: str, vault_id: str):
+        """Get a specific item by uuid
+
+        Args:
+            item_id (str): The id of the item to be fetched
+            vault_id (str): The id of the vault in which to get the item from
+
+        Raises:
+            FailedToRetrieveItemException: Thrown when a HTTP error is returned
+            from the 1Password Connect API
+
+        Returns:
+            Item object: The found item
+        """
+        url = PathBuilder().vaults(vault_id).items(item_id).build()
+        response = await self.build_request("GET", url)
+        try:
+            response.raise_for_status()
+        except HTTPError:
+            raise FailedToRetrieveItemException(
+                f"Unable to retrieve item. Received {response.status_code}\
+                     for {url} with message: {response.json().get('message')}"
+            )
+        return self.serializer.deserialize(response.content, "Item")
+
+    async def get_item_by_title(self, title: str, vault_id: str):
+        """Get a specific item by title
+
+        Args:
+            title (str): The title of the item to be fetched
+            vault_id (str): The id of the vault in which to get the item from
+
+        Raises:
+            FailedToRetrieveItemException: Thrown when a HTTP error is returned
+            from the 1Password Connect API
+
+        Returns:
+            Item object: The found item
+        """
+        filter_query = f'title eq "{title}"'
+        url = PathBuilder().vaults(vault_id).items().query("filter", filter_query).build()
+        response = await self.build_request("GET", url)
+        try:
+            response.raise_for_status()
+        except HTTPError:
+            raise FailedToRetrieveItemException(
+                f"Unable to retrieve items. Received {response.status_code} \
+                     for {url} with message: {response.json().get('message')}"
+            )
+
+        if len(response.json()) != 1:
+            raise FailedToRetrieveItemException(
+                f"Found {len(response.json())} items in vault {vault_id} with \
+                    title {title}"
+            )
+
+        item_summary = self.serializer.deserialize(response.content, "list[SummaryItem]")[0]
+        return await self.get_item_by_id(item_summary.id, vault_id)
+
+    async def get_items(self, vault_id: str, filter_query: str = None):
+        """Returns a list of item summaries for the specified vault
+
+        Args:
+            vault_id (str): The id of the vault in which to get the items from
+
+        Raises:
+            FailedToRetrieveItemException: Thrown when a HTTP error is returned
+            from the 1Password Connect API
+
+        Returns:
+            List[SummaryItem]: A list of summarized items
+        """
+        if filter_query is None:
+            url = PathBuilder().vaults(vault_id).items().build()
+        else:
+            url = PathBuilder().vaults(vault_id).items().query("filter", filter_query).build()
+        response = await self.build_request("GET", url)
+        try:
+            response.raise_for_status()
+        except HTTPError:
+            raise FailedToRetrieveItemException(
+                f"Unable to retrieve items. Received {response.status_code} \
+                     for {url} with message: {response.json().get('message')}"
+            )
+
+        return self.serializer.deserialize(response.content, "list[SummaryItem]")
+
+    async def delete_item(self, item_id: str, vault_id: str):
+        """Deletes a specified item from a specified vault
+
+        Args:
+            item_id (str): The id of the item in which to delete the item from
+            vault_id (str): The id of the vault in which to delete the item
+            from
+
+        Raises:
+            FailedToRetrieveItemException: Thrown when a HTTP error is returned
+            from the 1Password Connect API
+        """
+        url = PathBuilder().vaults(vault_id).items(item_id).build()
+        response = await self.build_request("DELETE", url)
+        try:
+            response.raise_for_status()
+        except HTTPError:
+            raise FailedToRetrieveItemException(
+                f"Unable to delete item. Received {response.status_code}\
+                     for {url} with message: {response.json().get('message')}"
+            )
+
+    async def create_item(self, vault_id: str, item: Item):
+        """Creates an item at the specified vault
+
+        Args:
+            vault_id (str): The id of the vault in which add the item to
+            item (Item): The item to create
+
+        Raises:
+            FailedToRetrieveItemException: Thrown when a HTTP error is returned
+            from the 1Password Connect API
+
+        Returns:
+            Item: The created item
+        """
+
+        url = PathBuilder().vaults(vault_id).items().build()
+        response = await self.build_request("POST", url, item)
+        try:
+            response.raise_for_status()
+        except HTTPError:
+            raise FailedToRetrieveItemException(
+                f"Unable to post item. Received {response.status_code}\
+                    for {url} with message: {response.json().get('message')}"
+            )
+        return self.serializer.deserialize(response.content, "Item")
+
+    async def update_item(self, item_uuid: str, vault_id: str, item: Item):
+        """Update the specified item at the specified vault.
+
+        Args:
+            item_uuid (str): The id of the item in which to update
+            vault_id (str): The id of the vault in which to update the item
+            item (Item): The updated item
+
+        Raises:
+            FailedToRetrieveItemException: Thrown when a HTTP error is returned
+            from the 1Password Connect API
+
+        Returns:
+            Item: The updated item
+        """
+        url = PathBuilder().vaults(vault_id).items(item_uuid).build()
+        item.id = item_uuid
+        item.vault = ItemVault(id=vault_id)
+
+        response = await self.build_request("PUT", url, item)
+        try:
+            response.raise_for_status()
+        except HTTPError:
+            raise FailedToRetrieveItemException(
+                f"Unable to post item. Received {response.status_code}\
+                    for {url} with message: {response.json().get('message')}"
+            )
+        return self.serializer.deserialize(response.content, "Item")
+
+    async def get_vault(self, vault_id: str):
+        """Returns the vault with the given vault_id
+
+        Args:
+            vault_id (str): The id of the vault in which to fetch
+
+        Raises:
+            FailedToRetrieveVaultException: Thrown when a HTTP error is
+            returned from the 1Password Connect API
+
+        Returns:
+            Vault: The specified vault
+        """
+        url = PathBuilder().vaults(vault_id).build()
+        response = await self.build_request("GET", url)
+        try:
+            response.raise_for_status()
+        except HTTPError:
+            raise FailedToRetrieveVaultException(
+                f"Unable to retrieve vault. Received {response.status_code} \
+                     for {url} with message {response.json().get('message')}"
+            )
+
+        return self.serializer.deserialize(response.content, "Vault")
+
+    async def get_vault_by_title(self, name: str):
+        """Returns the vault with the given name
+
+        Args:
+            name (str): The name of the vault in which to fetch
+
+        Raises:
+            FailedToRetrieveVaultException: Thrown when a HTTP error is
+            returned from the 1Password Connect API
+
+        Returns:
+            Vault: The specified vault
+        """
+        filter_query = f'name eq "{name}"'
+        url = PathBuilder().vaults().query("filter", filter_query).build()
+        response = await self.build_request("GET", url)
+        try:
+            response.raise_for_status()
+        except HTTPError:
+            raise FailedToRetrieveVaultException(
+                f"Unable to retrieve vaults. Received {response.status_code} \
+                     for {url} with message {response.json().get('message')}"
+            )
+
+        if len(response.json()) != 1:
+            raise FailedToRetrieveItemException(
+                f"Found {len(response.json())} vaults with \
+                    name {name}"
+            )
+
+        return self.serializer.deserialize(response.content, "list[Vault]")[0]
+
+    async def get_vaults(self):
+        """Returns all vaults for service account set in client
+
+        Raises:
+            FailedToRetrieveVaultException: Thrown when a HTTP error is
+            returned from the 1Password Connect API
+
+        Returns:
+            List[Vault]: All vaults for the service account in use
+        """
+        url = PathBuilder().vaults().build()
+        response = await self.build_request("GET", url)
+        try:
+            response.raise_for_status()
+        except HTTPError:
+            raise FailedToRetrieveVaultException(
+                f"Unable to retrieve vaults. Received {response.status_code} \
+                     for {url} with message {response.json().get('message')}"
+            )
+
+        return self.serializer.deserialize(response.content, "list[Vault]")
+
+    def build_request(self, method: str, path: str, body=None):
+        """Builds a http request
+        Parameters:
+        method (str): The rest method to be used
+        path (str): The request path
+        body (str): The request body
+
+        Returns:
+        Response object: The request response
+        """
+
+        if body:
+            serialized_body = json.dumps(self.serializer.sanitize_for_serialization(body))
+            response = self.session.request(method, path, data=serialized_body)
+        else:
+            response = self.session.request(method, path)
+        return response
+
+    def deserialize(self, response, response_type):
+        return self.serializer.deserialize(response, response_type)
+
+    def sanitize_for_serialization(self, obj):
+        return self.serializer.sanitize_for_serialization(obj)
diff --git a/src/onepasswordconnectsdk/client.py b/src/onepasswordconnectsdk/client.py
index 694da33..4202ccf 100644
--- a/src/onepasswordconnectsdk/client.py
+++ b/src/onepasswordconnectsdk/client.py
@@ -1,58 +1,46 @@
 """Python Client for connecting to 1Password Connect"""
-from dateutil.parser import parse
+import httpx
+from httpx import HTTPError
 import json
 import os
-import re
-import requests
-import datetime
-from requests.exceptions import HTTPError
-import onepasswordconnectsdk
+
+from onepasswordconnectsdk.async_client import AsyncClient
+from onepasswordconnectsdk.serializer import Serializer
+from onepasswordconnectsdk.utils import build_headers, is_valid_uuid, PathBuilder
+from onepasswordconnectsdk.errors import (
+    FailedToRetrieveItemException,
+    FailedToRetrieveVaultException,
+    EnvironmentHostNotSetException,
+    EnvironmentTokenNotSetException,
+)
 from onepasswordconnectsdk.models import Item, ItemVault
 from onepasswordconnectsdk.models.constants import CONNECT_HOST_ENV_VARIABLE
 
 ENV_SERVICE_ACCOUNT_JWT_VARIABLE = "OP_CONNECT_TOKEN"
-UUIDLength = 26
+ENV_IS_ASYNC_CLIENT = "OP_CONNECT_CLIENT_ASYNC"
 
 
 class Client:
-    PRIMITIVE_TYPES = (float, bool, bytes, str, int)
-    NATIVE_TYPES_MAPPING = {
-        "int": int,
-        "float": float,
-        "str": str,
-        "bool": bool,
-        "date": datetime.date,
-        "datetime": datetime.datetime,
-        "object": object,
-    }
-
     """Python Client Class"""
 
     def __init__(self, url: str, token: str):
         """Initialize client"""
         self.url = url
         self.token = token
-        self.session = self.create_session()
+        self.session = self.create_session(url, token)
+        self.serializer = Serializer()
 
-    def create_session(self):
-        session = requests.Session()
-        session.headers.update(self.build_headers())
-        return session
+    def create_session(self, url: str, token: str):
+        return httpx.Client(base_url=url, headers=self.build_headers(token))
 
-    def build_headers(self):
-        """Builds the headers needed to make a request to the server
+    def build_headers(self, token: str):
+        return build_headers(token)
 
-        Returns:
-            dict: The 1Password Connect API request headers
-        """
-
-        headers = {}
-        headers["Authorization"] = f"Bearer {self.token}"
-        headers["Content-Type"] = "application/json"
-        return headers
+    def __del__(self):
+        self.session.close()
 
     def get_file(self, file_id: str, item_id: str, vault_id: str):
-        url = f"/v1/vaults/{vault_id}/items/{item_id}/files/{file_id}"
+        url = PathBuilder().vaults(vault_id).items(item_id).files(file_id).build()
         response = self.build_request("GET", url)
         try:
             response.raise_for_status()
@@ -61,11 +49,10 @@ def get_file(self, file_id: str, item_id: str, vault_id: str):
                 f"Unable to retrieve item. Received {response.status_code}\
                                      for {url} with message: {response.json().get('message')}"
             )
-        return self.deserialize(response.content, "File")
+        return self.serializer.deserialize(response.content, "File")
 
     def get_files(self, item_id: str, vault_id: str):
-        url = f"/v1/vaults/{vault_id}/items/{item_id}/files"
-
+        url = PathBuilder().vaults(vault_id).items(item_id).files().build()
         response = self.build_request("GET", url)
         try:
             response.raise_for_status()
@@ -74,11 +61,12 @@ def get_files(self, item_id: str, vault_id: str):
                 f"Unable to retrieve item. Received {response.status_code}\
                              for {url} with message: {response.json().get('message')}"
             )
-        return self.deserialize(response.content, "list[File]")
+        return self.serializer.deserialize(response.content, "list[File]")
 
     def get_file_content(self, file_id: str, item_id: str, vault_id: str, content_path: str = None):
-        url = content_path if content_path is not None else f"/v1/vaults/{vault_id}/items/{item_id}/files/{file_id}/content"
-
+        url = content_path
+        if content_path is None:
+            url = PathBuilder().vaults(vault_id).items(item_id).files(file_id).content().build()
         response = self.build_request("GET", url)
         try:
             response.raise_for_status()
@@ -115,10 +103,10 @@ def get_item(self, item: str, vault: str):
         """
 
         vault_id = vault
-        if not self._is_valid_UUID(vault):
+        if not is_valid_uuid(vault):
             vault_id = self.get_vault_by_title(vault).id
 
-        if self._is_valid_UUID(item):
+        if is_valid_uuid(item):
             return self.get_item_by_id(item, vault_id)
         else:
             return self.get_item_by_title(item, vault_id)
@@ -137,8 +125,7 @@ def get_item_by_id(self, item_id: str, vault_id: str):
         Returns:
             Item object: The found item
         """
-        url = f"/v1/vaults/{vault_id}/items/{item_id}"
-
+        url = PathBuilder().vaults(vault_id).items(item_id).build()
         response = self.build_request("GET", url)
         try:
             response.raise_for_status()
@@ -147,7 +134,7 @@ def get_item_by_id(self, item_id: str, vault_id: str):
                 f"Unable to retrieve item. Received {response.status_code}\
                      for {url} with message: {response.json().get('message')}"
             )
-        return self.deserialize(response.content, "Item")
+        return self.serializer.deserialize(response.content, "Item")
 
     def get_item_by_title(self, title: str, vault_id: str):
         """Get a specific item by title
@@ -164,8 +151,7 @@ def get_item_by_title(self, title: str, vault_id: str):
             Item object: The found item
         """
         filter_query = f'title eq "{title}"'
-        url = f"/v1/vaults/{vault_id}/items?filter={filter_query}"
-
+        url = PathBuilder().vaults(vault_id).items().query("filter", filter_query).build()
         response = self.build_request("GET", url)
         try:
             response.raise_for_status()
@@ -181,15 +167,15 @@ def get_item_by_title(self, title: str, vault_id: str):
                     title {title}"
             )
 
-        item_summary = self.deserialize(response.content, "list[SummaryItem]")[0]
+        item_summary = self.serializer.deserialize(response.content, "list[SummaryItem]")[0]
         return self.get_item_by_id(item_summary.id, vault_id)
 
-    def get_items(self, vault_id: str, filter_query=None):
+    def get_items(self, vault_id: str, filter_query: str = None):
         """Returns a list of item summaries for the specified vault
 
         Args:
             vault_id (str): The id of the vault in which to get the items from
-            filter_query (str): A optional query statement. `title eq foo.bar`
+            filter_query (str): A optional query statement. `title eq "Example Item"`
 
         Raises:
             FailedToRetrieveItemException: Thrown when a HTTP error is returned
@@ -199,9 +185,9 @@ def get_items(self, vault_id: str, filter_query=None):
             List[SummaryItem]: A list of summarized items
         """
         if filter_query is None:
-            url = f"/v1/vaults/{vault_id}/items"
+            url = PathBuilder().vaults(vault_id).items().build()
         else:
-            url = f"/v1/vaults/{vault_id}/items?filter={filter_query}"
+            url = PathBuilder().vaults(vault_id).items().query("filter", filter_query).build()
 
         response = self.build_request("GET", url)
         try:
@@ -212,7 +198,7 @@ def get_items(self, vault_id: str, filter_query=None):
                      for {url} with message: {response.json().get('message')}"
             )
 
-        return self.deserialize(response.content, "list[SummaryItem]")
+        return self.serializer.deserialize(response.content, "list[SummaryItem]")
 
     def delete_item(self, item_id: str, vault_id: str):
         """Deletes a specified item from a specified vault
@@ -220,14 +206,12 @@ def delete_item(self, item_id: str, vault_id: str):
         Args:
             item_id (str): The id of the item in which to delete the item from
             vault_id (str): The id of the vault in which to delete the item
-            from
 
         Raises:
             FailedToRetrieveItemException: Thrown when a HTTP error is returned
             from the 1Password Connect API
         """
-        url = f"/v1/vaults/{vault_id}/items/{item_id}"
-
+        url = PathBuilder().vaults(vault_id).items(item_id).build()
         response = self.build_request("DELETE", url)
         try:
             response.raise_for_status()
@@ -252,9 +236,8 @@ def create_item(self, vault_id: str, item: Item):
             Item: The created item
         """
 
-        url = f"/v1/vaults/{vault_id}/items"
-
-        response: requests.Response = self.build_request("POST", url, item)
+        url = PathBuilder().vaults(vault_id).items().build()
+        response = self.build_request("POST", url, item)
         try:
             response.raise_for_status()
         except HTTPError:
@@ -262,7 +245,7 @@ def create_item(self, vault_id: str, item: Item):
                 f"Unable to post item. Received {response.status_code}\
                     for {url} with message: {response.json().get('message')}"
             )
-        return self.deserialize(response.content, "Item")
+        return self.serializer.deserialize(response.content, "Item")
 
     def update_item(self, item_uuid: str, vault_id: str, item: Item):
         """Update the specified item at the specified vault.
@@ -279,11 +262,11 @@ def update_item(self, item_uuid: str, vault_id: str, item: Item):
         Returns:
             Item: The updated item
         """
-        url = f"/v1/vaults/{vault_id}/items/{item_uuid}"
+        url = PathBuilder().vaults(vault_id).items(item_uuid).build()
         item.id = item_uuid
         item.vault = ItemVault(id=vault_id)
 
-        response: requests.Response = self.build_request("PUT", url, item)
+        response = self.build_request("PUT", url, item)
         try:
             response.raise_for_status()
         except HTTPError:
@@ -291,7 +274,7 @@ def update_item(self, item_uuid: str, vault_id: str, item: Item):
                 f"Unable to post item. Received {response.status_code}\
                     for {url} with message: {response.json().get('message')}"
             )
-        return self.deserialize(response.content, "Item")
+        return self.serializer.deserialize(response.content, "Item")
 
     def get_vault(self, vault_id: str):
         """Returns the vault with the given vault_id
@@ -306,7 +289,7 @@ def get_vault(self, vault_id: str):
         Returns:
             Vault: The specified vault
         """
-        url = f"/v1/vaults/{vault_id}"
+        url = PathBuilder().vaults(vault_id).build()
         response = self.build_request("GET", url)
         try:
             response.raise_for_status()
@@ -316,7 +299,7 @@ def get_vault(self, vault_id: str):
                      for {url} with message {response.json().get('message')}"
             )
 
-        return self.deserialize(response.content, "Vault")
+        return self.serializer.deserialize(response.content, "Vault")
 
     def get_vault_by_title(self, name: str):
         """Returns the vault with the given name
@@ -332,8 +315,7 @@ def get_vault_by_title(self, name: str):
             Vault: The specified vault
         """
         filter_query = f'name eq "{name}"'
-        url = f"/v1/vaults?filter={filter_query}"
-
+        url = PathBuilder().vaults().query("filter", filter_query).build()
         response = self.build_request("GET", url)
         try:
             response.raise_for_status()
@@ -349,7 +331,7 @@ def get_vault_by_title(self, name: str):
                     name {name}"
             )
 
-        return self.deserialize(response.content, "list[Vault]")[0]
+        return self.serializer.deserialize(response.content, "list[Vault]")[0]
 
     def get_vaults(self):
         """Returns all vaults for service account set in client
@@ -361,9 +343,8 @@ def get_vaults(self):
         Returns:
             List[Vault]: All vaults for the service account in use
         """
-        url = "/v1/vaults"
+        url = PathBuilder().vaults().build()
         response = self.build_request("GET", url)
-
         try:
             response.raise_for_status()
         except HTTPError:
@@ -372,7 +353,7 @@ def get_vaults(self):
                      for {url} with message {response.json().get('message')}"
             )
 
-        return self.deserialize(response.content, "list[Vault]")
+        return self.serializer.deserialize(response.content, "list[Vault]")
 
     def build_request(self, method: str, path: str, body=None):
         """Builds a http request
@@ -384,227 +365,34 @@ def build_request(self, method: str, path: str, body=None):
         Returns:
         Response object: The request response
         """
-        url = f"{self.url}{path}"
 
         if body:
-            serialized_body = json.dumps(self.sanitize_for_serialization(body))
-            response = self.session.request(method, url, data=serialized_body)
+            serialized_body = json.dumps(self.serializer.sanitize_for_serialization(body))
+            response = self.session.request(method, path, data=serialized_body)
         else:
-            response = self.session.request(method, url)
+            response = self.session.request(method, path)
         return response
 
     def deserialize(self, response, response_type):
-        """Deserializes response into an object.
-
-        :param response: RESTResponse object to be deserialized.
-        :param response_type: class literal for
-            deserialized object, or string of class name.
-
-        :return: deserialized object.
-        """
-        # fetch data from response object
-        try:
-            data = json.loads(response)
-        except ValueError:
-            data = response
-
-        return self.__deserialize(data, response_type)
+        return self.serializer.deserialize(response, response_type)
 
     def sanitize_for_serialization(self, obj):
-        """Builds a JSON POST object.
-
-        If obj is None, return None.
-        If obj is str, int, long, float, bool, return directly.
-        If obj is datetime.datetime, datetime.date convert to string
-        in iso8601 format.
-        If obj is list, sanitize each element in the list.
-        If obj is dict, return the dict.
-        If obj is OpenAPI model, return the properties dict.
-
-        :param obj: The data to serialize.
-        :return: The serialized form of data.
-        """
-        if obj is None:
-            return None
-        elif isinstance(obj, self.PRIMITIVE_TYPES):
-            return obj
-        elif isinstance(obj, list):
-            return [self.sanitize_for_serialization(sub_obj) for sub_obj in obj]  # noqa: E501
-        elif isinstance(obj, tuple):
-            return tuple(self.sanitize_for_serialization(sub_obj) for sub_obj in obj)  # noqa: E501
-        elif isinstance(obj, (datetime.datetime, datetime.date)):
-            return obj.isoformat()
-
-        if isinstance(obj, dict):
-            obj_dict = obj
-        else:
-            # Convert model obj to dict except
-            # attributes `openapi_types`, `attribute_map`
-            # and attributes which value is not None.
-            # Convert attribute name to json key in
-            # model definition for request.
-            obj_dict = {
-                obj.attribute_map[attr]: getattr(obj, attr)
-                for attr in obj.openapi_types.keys()
-                if getattr(obj, attr) is not None
-            }
-
-        return {
-            key: self.sanitize_for_serialization(val)
-            for key, val in obj_dict.items()
-        }
-
-    def __deserialize(self, data, klass):
-        """Deserializes dict, list, str into an object.
-
-        :param data: dict, list or str.
-        :param klass: class literal, or string of class name.
-
-        :return: object.
-        """
-        if data is None:
-            return None
-
-        if type(klass) == str:
-            if klass.startswith("list["):
-                sub_kls = re.match(r"list\[(.*)\]", klass).group(1)
-                return [self.__deserialize(sub_data, sub_kls) for sub_data in data]  # noqa: E501
-
-            if klass.startswith("dict("):
-                sub_kls = re.match(r"dict\(([^,]*), (.*)\)", klass).group(2)
-                return {
-                    k: self.__deserialize(v, sub_kls) for k, v in data.items()  # noqa: E501
-                }
-
-            # convert str to class
-            if klass in self.NATIVE_TYPES_MAPPING:
-                klass = self.NATIVE_TYPES_MAPPING[klass]
-            else:
-                klass = getattr(onepasswordconnectsdk.models, klass)
-
-        if klass in self.PRIMITIVE_TYPES:
-            return self.__deserialize_primitive(data, klass)
-        elif klass == object:
-            return self.__deserialize_object(data)
-        elif klass == datetime.date:
-            return self.__deserialize_date(data)
-        elif klass == datetime.datetime:
-            return self.__deserialize_datetime(data)
-        else:
-            return self.__deserialize_model(data, klass)
-
-    def __deserialize_primitive(self, data, klass):
-        """Deserializes string to primitive type.
-
-        :param data: str.
-        :param klass: class literal.
-
-        :return: int, long, float, str, bool.
-        """
-        try:
-            return klass(data)
-        except UnicodeEncodeError:
-            return str(data)
-        except TypeError:
-            return data
+        return self.serializer.sanitize_for_serialization(obj)
 
-    def __deserialize_object(self, value):
-        """Return an original value.
 
-        :return: object.
-        """
-        return value
-
-    def __deserialize_date(self, string):
-        """Deserializes string to date.
-
-        :param string: str.
-        :return: date.
-        """
-        try:
-            return parse(string).date()
-        except ImportError:
-            return string
-        except ValueError:
-            raise FailedToDeserializeException(
-                f'Failed to parse `{0}`\
-                 as date object".format(string)'
-            )
-
-    def __deserialize_datetime(self, string):
-        """Deserializes string to datetime.
-
-        The string should be in iso8601 datetime format.
-
-        :param string: str.
-        :return: datetime.
-        """
-        try:
-            return parse(string)
-        except ImportError:
-            return string
-        except ValueError:
-            raise FailedToDeserializeException(
-                f'Failed to parse `{0}`\
-                 as date object".format(string)'
-            )
-
-    def __deserialize_model(self, data, klass):
-        """Deserializes list or dict to model.
-
-        :param data: dict, list.
-        :param klass: class literal.
-        :return: model object.
-        """
-        has_discriminator = False
-        if (
-                hasattr(klass, "get_real_child_model")
-                and klass.discriminator_value_class_map
-        ):
-            has_discriminator = True
-
-        if not klass.openapi_types and has_discriminator is False:
-            return data
-
-        kwargs = {}
-        if (
-                data is not None
-                and klass.openapi_types is not None
-                and isinstance(data, (list, dict))
-        ):
-            for attr, attr_type in klass.openapi_types.items():
-                if klass.attribute_map[attr] in data:
-                    value = data[klass.attribute_map[attr]]
-                    kwargs[attr] = self.__deserialize(value, attr_type)
-
-        instance = klass(**kwargs)
-
-        if has_discriminator:
-            klass_name = instance.get_real_child_model(data)
-            if klass_name:
-                instance = self.__deserialize(data, klass_name)
-        return instance
-
-    def _is_valid_UUID(self, uuid):
-        if len(uuid) is not UUIDLength:
-            return False
-        for c in uuid:
-            valid = (c >= 'a' and c <= 'z') or (c >= '0' and c <= '9')
-            if valid is False:
-                return False
-        return True
-
-
-def new_client(url: str, token: str):
+def new_client(url: str, token: str, is_async: bool = False):
     """Builds a new client for interacting with 1Password Connect
     Parameters:
     url: The url of the 1Password Connect API
     token: The 1Password Service Account token
+    is_async: Initialize async or sync client
 
     Returns:
     Client: The 1Password Connect client
     """
-    return Client(url=url, token=token)
+    if is_async:
+        return AsyncClient(url, token)
+    return Client(url, token)
 
 
 def new_client_from_environment(url: str = None):
@@ -619,6 +407,7 @@ def new_client_from_environment(url: str = None):
     Client: The 1Password Connect client
     """
     token = os.environ.get(ENV_SERVICE_ACCOUNT_JWT_VARIABLE)
+    is_async = os.environ.get(ENV_IS_ASYNC_CLIENT) == "True"
 
     if url is None:
         url = os.environ.get(CONNECT_HOST_ENV_VARIABLE)
@@ -633,28 +422,4 @@ def new_client_from_environment(url: str = None):
             f"{ENV_SERVICE_ACCOUNT_JWT_VARIABLE} variable"
         )
 
-    return Client(url=url, token=token)
-
-
-class OnePasswordConnectSDKError(RuntimeError):
-    pass
-
-
-class EnvironmentTokenNotSetException(OnePasswordConnectSDKError, TypeError):
-    pass
-
-
-class EnvironmentHostNotSetException(OnePasswordConnectSDKError, TypeError):
-    pass
-
-
-class FailedToRetrieveItemException(OnePasswordConnectSDKError):
-    pass
-
-
-class FailedToRetrieveVaultException(OnePasswordConnectSDKError):
-    pass
-
-
-class FailedToDeserializeException(OnePasswordConnectSDKError, TypeError):
-    pass
+    return new_client(url, token, is_async)
diff --git a/src/onepasswordconnectsdk/connect.py b/src/onepasswordconnectsdk/connect.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/onepasswordconnectsdk/errors.py b/src/onepasswordconnectsdk/errors.py
new file mode 100644
index 0000000..add5db0
--- /dev/null
+++ b/src/onepasswordconnectsdk/errors.py
@@ -0,0 +1,22 @@
+class OnePasswordConnectSDKError(RuntimeError):
+    pass
+
+
+class EnvironmentTokenNotSetException(OnePasswordConnectSDKError, TypeError):
+    pass
+
+
+class EnvironmentHostNotSetException(OnePasswordConnectSDKError, TypeError):
+    pass
+
+
+class FailedToRetrieveItemException(OnePasswordConnectSDKError):
+    pass
+
+
+class FailedToRetrieveVaultException(OnePasswordConnectSDKError):
+    pass
+
+
+class FailedToDeserializeException(OnePasswordConnectSDKError, TypeError):
+    pass
diff --git a/src/onepasswordconnectsdk/serializer.py b/src/onepasswordconnectsdk/serializer.py
new file mode 100644
index 0000000..db07568
--- /dev/null
+++ b/src/onepasswordconnectsdk/serializer.py
@@ -0,0 +1,211 @@
+from dateutil.parser import parse
+import json
+import re
+import datetime
+import onepasswordconnectsdk
+from onepasswordconnectsdk.errors import FailedToDeserializeException
+
+
+class Serializer:
+    PRIMITIVE_TYPES = (float, bool, bytes, str, int)
+    NATIVE_TYPES_MAPPING = {
+        "int": int,
+        "float": float,
+        "str": str,
+        "bool": bool,
+        "date": datetime.date,
+        "datetime": datetime.datetime,
+        "object": object,
+    }
+
+    def deserialize(self, response, response_type):
+        """Deserializes response into an object.
+
+        :param response: RESTResponse object to be deserialized.
+        :param response_type: class literal for
+            deserialized object, or string of class name.
+
+        :return: deserialized object.
+        """
+        # fetch data from response object
+        try:
+            data = json.loads(response)
+        except ValueError:
+            data = response
+
+        return self.__deserialize(data, response_type)
+
+    def sanitize_for_serialization(self, obj):
+        """Builds a JSON POST object.
+
+        If obj is None, return None.
+        If obj is str, int, long, float, bool, return directly.
+        If obj is datetime.datetime, datetime.date convert to string
+        in iso8601 format.
+        If obj is list, sanitize each element in the list.
+        If obj is dict, return the dict.
+        If obj is OpenAPI model, return the properties dict.
+
+        :param obj: The data to serialize.
+        :return: The serialized form of data.
+        """
+        if obj is None:
+            return None
+        elif isinstance(obj, self.PRIMITIVE_TYPES):
+            return obj
+        elif isinstance(obj, list):
+            return [self.sanitize_for_serialization(sub_obj) for sub_obj in obj]  # noqa: E501
+        elif isinstance(obj, tuple):
+            return tuple(self.sanitize_for_serialization(sub_obj) for sub_obj in obj)  # noqa: E501
+        elif isinstance(obj, (datetime.datetime, datetime.date)):
+            return obj.isoformat()
+
+        if isinstance(obj, dict):
+            obj_dict = obj
+        else:
+            # Convert model obj to dict except
+            # attributes `openapi_types`, `attribute_map`
+            # and attributes which value is not None.
+            # Convert attribute name to json key in
+            # model definition for request.
+            obj_dict = {
+                obj.attribute_map[attr]: getattr(obj, attr)
+                for attr in obj.openapi_types.keys()
+                if getattr(obj, attr) is not None
+            }
+
+        return {
+            key: self.sanitize_for_serialization(val)
+            for key, val in obj_dict.items()
+        }
+
+    def __deserialize(self, data, klass):
+        """Deserializes dict, list, str into an object.
+
+        :param data: dict, list or str.
+        :param klass: class literal, or string of class name.
+
+        :return: object.
+        """
+        if data is None:
+            return None
+
+        if type(klass) == str:
+            if klass.startswith("list["):
+                sub_kls = re.match(r"list\[(.*)\]", klass).group(1)
+                return [self.__deserialize(sub_data, sub_kls) for sub_data in data]  # noqa: E501
+
+            if klass.startswith("dict("):
+                sub_kls = re.match(r"dict\(([^,]*), (.*)\)", klass).group(2)
+                return {
+                    k: self.__deserialize(v, sub_kls) for k, v in data.items()  # noqa: E501
+                }
+
+            # convert str to class
+            if klass in self.NATIVE_TYPES_MAPPING:
+                klass = self.NATIVE_TYPES_MAPPING[klass]
+            else:
+                klass = getattr(onepasswordconnectsdk.models, klass)
+
+        if klass in self.PRIMITIVE_TYPES:
+            return self.__deserialize_primitive(data, klass)
+        elif klass == object:
+            return self.__deserialize_object(data)
+        elif klass == datetime.date:
+            return self.__deserialize_date(data)
+        elif klass == datetime.datetime:
+            return self.__deserialize_datetime(data)
+        else:
+            return self.__deserialize_model(data, klass)
+
+    def __deserialize_primitive(self, data, klass):
+        """Deserializes string to primitive type.
+
+        :param data: str.
+        :param klass: class literal.
+
+        :return: int, long, float, str, bool.
+        """
+        try:
+            return klass(data)
+        except UnicodeEncodeError:
+            return str(data)
+        except TypeError:
+            return data
+
+    def __deserialize_object(self, value):
+        """Return an original value.
+
+        :return: object.
+        """
+        return value
+
+    def __deserialize_date(self, string):
+        """Deserializes string to date.
+
+        :param string: str.
+        :return: date.
+        """
+        try:
+            return parse(string).date()
+        except ImportError:
+            return string
+        except ValueError:
+            raise FailedToDeserializeException(
+                f'Failed to parse `{0}`\
+                 as date object".format(string)'
+            )
+
+    def __deserialize_datetime(self, string):
+        """Deserializes string to datetime.
+
+        The string should be in iso8601 datetime format.
+
+        :param string: str.
+        :return: datetime.
+        """
+        try:
+            return parse(string)
+        except ImportError:
+            return string
+        except ValueError:
+            raise FailedToDeserializeException(
+                f'Failed to parse `{0}`\
+                 as date object".format(string)'
+            )
+
+    def __deserialize_model(self, data, klass):
+        """Deserializes list or dict to model.
+
+        :param data: dict, list.
+        :param klass: class literal.
+        :return: model object.
+        """
+        has_discriminator = False
+        if (
+                hasattr(klass, "get_real_child_model")
+                and klass.discriminator_value_class_map
+        ):
+            has_discriminator = True
+
+        if not klass.openapi_types and has_discriminator is False:
+            return data
+
+        kwargs = {}
+        if (
+                data is not None
+                and klass.openapi_types is not None
+                and isinstance(data, (list, dict))
+        ):
+            for attr, attr_type in klass.openapi_types.items():
+                if klass.attribute_map[attr] in data:
+                    value = data[klass.attribute_map[attr]]
+                    kwargs[attr] = self.__deserialize(value, attr_type)
+
+        instance = klass(**kwargs)
+
+        if has_discriminator:
+            klass_name = instance.get_real_child_model(data)
+            if klass_name:
+                instance = self.__deserialize(data, klass_name)
+        return instance
diff --git a/src/onepasswordconnectsdk/utils.py b/src/onepasswordconnectsdk/utils.py
new file mode 100644
index 0000000..da35d50
--- /dev/null
+++ b/src/onepasswordconnectsdk/utils.py
@@ -0,0 +1,61 @@
+UUIDLength = 26
+
+
+def is_valid_uuid(uuid):
+    if len(uuid) is not UUIDLength:
+        return False
+    for c in uuid:
+        valid = (c >= 'a' and c <= 'z') or (c >= '0' and c <= '9')
+        if valid is False:
+            return False
+    return True
+
+
+def build_headers(token: str):
+    """Builds the headers needed to make a request to the server
+
+    Returns:
+        dict: The 1Password Connect API request headers
+    """
+    return {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
+
+
+class PathBuilder:
+    def __init__(self, version: str = "/v1"):
+        self.path: str = version
+
+    def build(self) -> str:
+        return self.path
+
+    def vaults(self, uuid: str = None) -> 'PathBuilder':
+        self._append_path("vaults")
+        if uuid is not None:
+            self._append_path(uuid)
+        return self
+
+    def items(self, uuid: str = None) -> 'PathBuilder':
+        self._append_path("items")
+        if uuid is not None:
+            self._append_path(uuid)
+        return self
+
+    def files(self, uuid: str = None) -> 'PathBuilder':
+        self._append_path("files")
+        if uuid is not None:
+            self._append_path(uuid)
+        return self
+
+    def content(self) -> 'PathBuilder':
+        self._append_path("content")
+        return self
+
+    def query(self, key: str, value: str) -> 'PathBuilder':
+        key_value_pair = f"{key}={value}"
+        self._append_path(query=key_value_pair)
+        return self
+
+    def _append_path(self, path_chunk: str = None, query: str = None) -> 'PathBuilder':
+        if path_chunk is not None:
+            self.path += f"/{path_chunk}"
+        if query is not None:
+            self.path += f"?{query}"


Test patch:
diff --git a/src/tests/test_client_items.py b/src/tests/test_client_items.py
index 0a494eb..1f5f74d 100644
--- a/src/tests/test_client_items.py
+++ b/src/tests/test_client_items.py
@@ -1,203 +1,274 @@
-import json
-from requests import Session, Response
-from unittest.mock import patch
+import pytest
+from httpx import Response
 from onepasswordconnectsdk import client, models
 
 VAULT_ID = "hfnjvi6aymbsnfc2xeeoheizda"
 VAULT_TITLE = "VaultA"
 ITEM_ID = "wepiqdxdzncjtnvmv5fegud4qy"
 ITEM_TITLE = "Test Login"
-HOST = "mock_host"
+HOST = "https://mock_host"
 TOKEN = "jwt_token"
 SS_CLIENT = client.new_client(HOST, TOKEN)
+SS_CLIENT_ASYNC = client.new_client(HOST, TOKEN, True)
 
 
-@patch.object(Session, 'request')
-def test_get_item_by_id(mock):
+def test_get_item_by_id(respx_mock):
     expected_item = get_item()
-    expected_path = f"{HOST}/v1/vaults/{VAULT_ID}/items/{ITEM_ID}"
+    expected_path = f"/v1/vaults/{VAULT_ID}/items/{ITEM_ID}"
 
-    mock.return_value.ok = True
-    response = Response()
-    response.status_code = 200
-    response._content = json.dumps(expected_item).encode("utf8")
-    mock.return_value = response
+    mock = respx_mock.get(expected_path).mock(return_value=Response(200, json=expected_item))
 
     item = SS_CLIENT.get_item_by_id(ITEM_ID, VAULT_ID)
     compare_items(expected_item, item)
-    mock.assert_called_with("GET", expected_path)
+    assert mock.called
 
 
-@patch.object(Session, 'request')
-def test_get_item_by_title(mock):
+@pytest.mark.asyncio
+async def test_get_item_by_id_async(respx_mock):
     expected_item = get_item()
-    expected_path_item_title = f"{HOST}/v1/vaults/{VAULT_ID}/items?filter=title eq \"{ITEM_TITLE}\""
-    expected_path_item = f"{HOST}/v1/vaults/{VAULT_ID}/items/{ITEM_ID}"
+    expected_path = f"/v1/vaults/{VAULT_ID}/items/{ITEM_ID}"
 
-    mock.return_value.ok = True
+    mock = respx_mock.get(expected_path).mock(return_value=Response(200, json=expected_item))
 
-    response_item_summary = Response()
-    response_item_summary.status_code = 200
-    response_item_summary._content = json.dumps(get_items()).encode("utf8")
+    item = await SS_CLIENT_ASYNC.get_item_by_id(ITEM_ID, VAULT_ID)
+    compare_items(expected_item, item)
+    assert mock.called
 
-    response_item = Response()
-    response_item.status_code = 200
-    response_item._content = json.dumps(get_item()).encode("utf8")
 
-    mock.side_effect = [response_item_summary, response_item]
+def test_get_item_by_title(respx_mock):
+    expected_item = get_item()
+    expected_path_item_title = f"/v1/vaults/{VAULT_ID}/items?filter=title eq \"{ITEM_TITLE}\""
+    expected_path_item = f"/v1/vaults/{VAULT_ID}/items/{ITEM_ID}"
+
+    items_summary_mock = respx_mock.get(expected_path_item_title).mock(return_value=Response(200, json=get_items()))
+    item_mock = respx_mock.get(expected_path_item).mock(return_value=Response(200, json=expected_item))
 
     item = SS_CLIENT.get_item_by_title(ITEM_TITLE, VAULT_ID)
     compare_items(expected_item, item)
-    mock.assert_any_call("GET", expected_path_item_title)
-    mock.assert_called_with("GET", expected_path_item)
+    assert items_summary_mock.called
+    assert item_mock.called
+
+
+@pytest.mark.asyncio
+async def test_get_item_by_title_async(respx_mock):
+    expected_item = get_item()
+    expected_path_item_title = f"/v1/vaults/{VAULT_ID}/items?filter=title eq \"{ITEM_TITLE}\""
+    expected_path_item = f"/v1/vaults/{VAULT_ID}/items/{ITEM_ID}"
+
+    items_summary_mock = respx_mock.get(expected_path_item_title).mock(return_value=Response(200, json=get_items()))
+    item_mock = respx_mock.get(expected_path_item).mock(return_value=Response(200, json=expected_item))
+
+    item = await SS_CLIENT_ASYNC.get_item_by_title(ITEM_TITLE, VAULT_ID)
+    compare_items(expected_item, item)
+    assert items_summary_mock.called
+    assert item_mock.called
 
 
-@patch.object(Session, 'request')
-def test_get_item_by_item_id_vault_id(mock):
+def test_get_item_by_item_id_vault_id(respx_mock):
     expected_item = get_item()
-    expected_path = f"{HOST}/v1/vaults/{VAULT_ID}/items/{ITEM_ID}"
+    expected_path = f"/v1/vaults/{VAULT_ID}/items/{ITEM_ID}"
 
-    mock.return_value.ok = True
-    response = Response()
-    response.status_code = 200
-    response._content = json.dumps(expected_item).encode("utf8")
-    mock.return_value = response
+    mock = respx_mock.get(expected_path).mock(return_value=Response(200, json=expected_item))
 
     item = SS_CLIENT.get_item(ITEM_ID, VAULT_ID)
     compare_items(expected_item, item)
-    mock.assert_called_with("GET", expected_path)
+    assert mock.called
 
 
-@patch.object(Session, 'request')
-def test_get_item_by_item_id_vault_title(mock):
+@pytest.mark.asyncio
+async def test_get_item_by_item_id_vault_id_async(respx_mock):
     expected_item = get_item()
-    expected_path_vault_title = f"{HOST}/v1/vaults?filter=name eq \"{VAULT_TITLE}\""
-    expected_path_item = f"{HOST}/v1/vaults/{VAULT_ID}/items/{ITEM_ID}"
+    expected_path = f"/v1/vaults/{VAULT_ID}/items/{ITEM_ID}"
+
+    mock = respx_mock.get(expected_path).mock(return_value=Response(200, json=expected_item))
 
-    mock.return_value.ok = True
+    item = await SS_CLIENT_ASYNC.get_item(ITEM_ID, VAULT_ID)
+    compare_items(expected_item, item)
+    assert mock.called
 
-    response_vault = Response()
-    response_vault.status_code = 200
-    response_vault._content = json.dumps(get_vaults()).encode("utf8")
 
-    response_item = Response()
-    response_item.status_code = 200
-    response_item._content = json.dumps(expected_item).encode("utf8")
+def test_get_item_by_item_id_vault_title(respx_mock):
+    expected_item = get_item()
+    expected_path_vault_title = f"/v1/vaults?filter=name eq \"{VAULT_TITLE}\""
+    expected_path_item = f"/v1/vaults/{VAULT_ID}/items/{ITEM_ID}"
 
-    mock.side_effect = [response_vault, response_item]
+    vaults_by_title_mock = respx_mock.get(expected_path_vault_title).mock(
+        return_value=Response(200, json=get_vaults()))
+    item_mock = respx_mock.get(expected_path_item).mock(return_value=Response(200, json=expected_item))
 
     item = SS_CLIENT.get_item(ITEM_ID, VAULT_TITLE)
     compare_items(expected_item, item)
-    mock.assert_any_call("GET", expected_path_vault_title)
-    mock.assert_called_with("GET", expected_path_item)
+    assert vaults_by_title_mock.called
+    assert item_mock.called
 
 
-@patch.object(Session, 'request')
-def test_get_item_by_item_title_vault_id(mock):
+@pytest.mark.asyncio
+async def test_get_item_by_item_id_vault_title_async(respx_mock):
     expected_item = get_item()
-    expected_path_item_title = f"{HOST}/v1/vaults/{VAULT_ID}/items?filter=title eq \"{ITEM_TITLE}\""
-    expected_path_item = f"{HOST}/v1/vaults/{VAULT_ID}/items/{ITEM_ID}"
+    expected_path_vault_title = f"/v1/vaults?filter=name eq \"{VAULT_TITLE}\""
+    expected_path_item = f"/v1/vaults/{VAULT_ID}/items/{ITEM_ID}"
 
-    mock.return_value.ok = True
+    vaults_by_title_mock = respx_mock.get(expected_path_vault_title).mock(
+        return_value=Response(200, json=get_vaults()))
+    item_mock = respx_mock.get(expected_path_item).mock(return_value=Response(200, json=expected_item))
 
-    response_item_summary = Response()
-    response_item_summary.status_code = 200
-    response_item_summary._content = json.dumps(get_items()).encode("utf8")
+    item = await SS_CLIENT_ASYNC.get_item(ITEM_ID, VAULT_TITLE)
+    compare_items(expected_item, item)
+    assert vaults_by_title_mock.called
+    assert item_mock.called
 
-    response_item = Response()
-    response_item.status_code = 200
-    response_item._content = json.dumps(get_item()).encode("utf8")
 
-    mock.side_effect = [response_item_summary, response_item]
+def test_get_item_by_item_title_vault_id(respx_mock):
+    expected_item = get_item()
+    expected_path_item_title = f"/v1/vaults/{VAULT_ID}/items?filter=title eq \"{ITEM_TITLE}\""
+    expected_path_item = f"/v1/vaults/{VAULT_ID}/items/{ITEM_ID}"
+
+    items_by_title_mock = respx_mock.get(expected_path_item_title).mock(
+        return_value=Response(200, json=get_items()))
+    item_mock = respx_mock.get(expected_path_item).mock(return_value=Response(200, json=expected_item))
 
     item = SS_CLIENT.get_item(ITEM_TITLE, VAULT_ID)
     compare_items(expected_item, item)
-    mock.assert_any_call("GET", expected_path_item_title)
-    mock.assert_called_with("GET", expected_path_item)
+    assert items_by_title_mock.called
+    assert item_mock.called
 
 
-@patch.object(Session, 'request')
-def test_get_item_by_item_title_vault_title(mock):
+@pytest.mark.asyncio
+async def test_get_item_by_item_title_vault_id_async(respx_mock):
     expected_item = get_item()
-    expected_path_vault_title = f"{HOST}/v1/vaults?filter=name eq \"{VAULT_TITLE}\""
-    expected_path_item_title = f"{HOST}/v1/vaults/{VAULT_ID}/items?filter=title eq \"{ITEM_TITLE}\""
-    expected_path_item = f"{HOST}/v1/vaults/{VAULT_ID}/items/{ITEM_ID}"
+    expected_path_item_title = f"/v1/vaults/{VAULT_ID}/items?filter=title eq \"{ITEM_TITLE}\""
+    expected_path_item = f"/v1/vaults/{VAULT_ID}/items/{ITEM_ID}"
 
-    mock.return_value.ok = True
+    items_by_title_mock = respx_mock.get(expected_path_item_title).mock(
+        return_value=Response(200, json=get_items()))
+    item_mock = respx_mock.get(expected_path_item).mock(return_value=Response(200, json=expected_item))
 
-    response_vault = Response()
-    response_vault.status_code = 200
-    response_vault._content = json.dumps(get_vaults()).encode("utf8")
+    item = await SS_CLIENT_ASYNC.get_item(ITEM_TITLE, VAULT_ID)
+    compare_items(expected_item, item)
+    assert items_by_title_mock.called
+    assert item_mock.called
 
-    response_item_summary = Response()
-    response_item_summary.status_code = 200
-    response_item_summary._content = json.dumps(get_items()).encode("utf8")
 
-    response_item = Response()
-    response_item.status_code = 200
-    response_item._content = json.dumps(get_item()).encode("utf8")
+def test_get_item_by_item_title_vault_title(respx_mock):
+    expected_item = get_item()
+    expected_path_vault_title = f"/v1/vaults?filter=name eq \"{VAULT_TITLE}\""
+    expected_path_item_title = f"/v1/vaults/{VAULT_ID}/items?filter=title eq \"{ITEM_TITLE}\""
+    expected_path_item = f"/v1/vaults/{VAULT_ID}/items/{ITEM_ID}"
 
-    mock.side_effect = [response_vault, response_item_summary, response_item]
+    vaults_by_title_mock = respx_mock.get(expected_path_vault_title).mock(
+        return_value=Response(200, json=get_vaults()))
+    items_by_title_mock = respx_mock.get(expected_path_item_title).mock(
+        return_value=Response(200, json=get_items()))
+    item_mock = respx_mock.get(expected_path_item).mock(return_value=Response(200, json=expected_item))
 
     item = SS_CLIENT.get_item(ITEM_TITLE, VAULT_TITLE)
     compare_items(expected_item, item)
-    mock.assert_any_call("GET", expected_path_vault_title)
-    mock.assert_any_call("GET", expected_path_item_title)
-    mock.assert_called_with("GET", expected_path_item)
+    assert vaults_by_title_mock.called
+    assert items_by_title_mock.called
+    assert item_mock.called
 
 
-@patch.object(Session, 'request')
-def test_get_items(mock):
+@pytest.mark.asyncio
+async def test_get_item_by_item_title_vault_title_async(respx_mock):
+    expected_item = get_item()
+    expected_path_vault_title = f"/v1/vaults?filter=name eq \"{VAULT_TITLE}\""
+    expected_path_item_title = f"/v1/vaults/{VAULT_ID}/items?filter=title eq \"{ITEM_TITLE}\""
+    expected_path_item = f"/v1/vaults/{VAULT_ID}/items/{ITEM_ID}"
+
+    vaults_by_title_mock = respx_mock.get(expected_path_vault_title).mock(
+        return_value=Response(200, json=get_vaults()))
+    items_by_title_mock = respx_mock.get(expected_path_item_title).mock(
+        return_value=Response(200, json=get_items()))
+    item_mock = respx_mock.get(expected_path_item).mock(return_value=Response(200, json=expected_item))
+
+    item = await SS_CLIENT_ASYNC.get_item(ITEM_TITLE, VAULT_TITLE)
+    compare_items(expected_item, item)
+    assert vaults_by_title_mock.called
+    assert items_by_title_mock.called
+    assert item_mock.called
+
+
+def test_get_items(respx_mock):
     expected_items = get_items()
-    expected_path = f"{HOST}/v1/vaults/{VAULT_ID}/items"
+    expected_path = f"/v1/vaults/{VAULT_ID}/items"
 
-    mock.return_value.ok = True
-    response = Response()
-    response.status_code = 200
-    response._content = json.dumps(expected_items).encode("utf8")
-    mock.return_value = response
+    mock = respx_mock.get(expected_path).mock(return_value=Response(200, json=expected_items))
 
     items = SS_CLIENT.get_items(VAULT_ID)
     assert len(expected_items) == len(items)
     compare_summary_items(expected_items[0], items[0])
-    mock.assert_called_with("GET", expected_path)
+    assert mock.called
+
+
+@pytest.mark.asyncio
+async def test_get_items_async(respx_mock):
+    expected_items = get_items()
+    expected_path = f"/v1/vaults/{VAULT_ID}/items"
+
+    mock = respx_mock.get(expected_path).mock(return_value=Response(200, json=expected_items))
+
+    items = await SS_CLIENT_ASYNC.get_items(VAULT_ID)
+    assert len(expected_items) == len(items)
+    compare_summary_items(expected_items[0], items[0])
+    assert mock.called
 
 
-@patch.object(Session, 'request')
-def test_delete_item(mock):
+def test_delete_item(respx_mock):
     expected_items = get_items()
-    expected_path = f"{HOST}/v1/vaults/{VAULT_ID}/items/{ITEM_ID}"
+    expected_path = f"/v1/vaults/{VAULT_ID}/items/{ITEM_ID}"
 
-    mock.return_value.ok = True
-    response = Response()
-    response.status_code = 200
-    response._content = json.dumps(expected_items).encode("utf8")
-    mock.return_value = response
+    mock = respx_mock.delete(expected_path).mock(return_value=Response(200, json=expected_items))
 
     SS_CLIENT.delete_item(ITEM_ID, VAULT_ID)
-    mock.assert_called_with("DELETE", expected_path)
+    assert mock.called
+
+
+@pytest.mark.asyncio
+async def test_delete_item_async(respx_mock):
+    expected_items = get_items()
+    expected_path = f"/v1/vaults/{VAULT_ID}/items/{ITEM_ID}"
+
+    mock = respx_mock.delete(expected_path).mock(return_value=Response(200, json=expected_items))
+
+    await SS_CLIENT_ASYNC.delete_item(ITEM_ID, VAULT_ID)
+    assert mock.called
+
 
 
-@patch.object(Session, 'request')
-def test_create_item(mock):
-    mock.return_value.ok = True
-    mock.side_effect = create_item_side_effect
+def test_create_item(respx_mock):
+    item = generate_full_item()
+    mock = respx_mock.post(f"/v1/vaults/{item.vault.id}/items").mock(return_value=Response(201, json=item.to_dict()))
+
+    created_item = SS_CLIENT.create_item(item.vault.id, item)
+    assert mock.called
+    compare_full_items(item, created_item)
+
 
+@pytest.mark.asyncio
+async def test_create_item_async(respx_mock):
     item = generate_full_item()
+    mock = respx_mock.post(f"/v1/vaults/{item.vault.id}/items").mock(return_value=Response(201, json=item.to_dict()))
 
-    created_item = SS_CLIENT.create_item(VAULT_ID, item)
+    created_item = await SS_CLIENT_ASYNC.create_item(item.vault.id, item)
     assert mock.called
     compare_full_items(item, created_item)
 
 
-@patch.object(Session, 'request')
-def test_update_item(mock):
-    mock.return_value.ok = True
-    mock.side_effect = create_item_side_effect
+def test_update_item(respx_mock):
+    item = generate_full_item()
+    mock = respx_mock.put(f"/v1/vaults/{item.vault.id}/items/{item.id}").mock(return_value=Response(200, json=item.to_dict()))
+
+    updated_item = SS_CLIENT.update_item(item.id, item.vault.id, item)
+    assert mock.called
+    compare_full_items(item, updated_item)
 
+
+@pytest.mark.asyncio
+async def test_update_item_async(respx_mock):
     item = generate_full_item()
+    mock = respx_mock.put(f"/v1/vaults/{item.vault.id}/items/{item.id}").mock(return_value=Response(200, json=item.to_dict()))
 
-    updated_item = SS_CLIENT.update_item(ITEM_ID, VAULT_ID, item)
+    updated_item = await SS_CLIENT_ASYNC.update_item(item.id, item.vault.id, item)
     assert mock.called
     compare_full_items(item, updated_item)
 
@@ -220,13 +291,6 @@ def compare_full_items(expected_item, returned_item):
         compare_full_item_fields(expected_item.fields[i], returned_item.fields[i])
 
 
-def create_item_side_effect(method, url, data):
-    response = Response()
-    response.status_code = 200
-    response._content = data
-    return response
-
-
 def compare_full_item_fields(expected_field, returned_field):
     assert expected_field.id == returned_field.id
     assert expected_field.label == returned_field.label
diff --git a/src/tests/test_client_vaults.py b/src/tests/test_client_vaults.py
index 92f5979..9a3eab3 100644
--- a/src/tests/test_client_vaults.py
+++ b/src/tests/test_client_vaults.py
@@ -1,61 +1,82 @@
-import json
-from requests import Session, Response
-from unittest.mock import patch
+import pytest
+from httpx import Response
 from onepasswordconnectsdk import client
 
 VAULT_ID = "hfnjvi6aymbsnfc2xeeoheizda"
 VAULT_NAME = "VaultA"
-HOST = "mock_host"
+HOST = "https://mock_host"
 TOKEN = "jwt_token"
 SS_CLIENT = client.new_client(HOST, TOKEN)
+SS_CLIENT_ASYNC = client.new_client(HOST, TOKEN, True)
 
 
-@patch.object(Session, 'request')
-def test_get_vaults(mock):
+def test_get_vaults(respx_mock):
     expected_vaults = list_vaults()
-    expected_path = f"{HOST}/v1/vaults"
+    expected_path = "/v1/vaults"
 
-    mock.return_value.ok = True
-    response = Response()
-    response.status_code = 200
-    response._content = json.dumps(expected_vaults).encode("utf8")
-    mock.return_value = response
+    mock = respx_mock.get(expected_path).mock(return_value=Response(200, json=expected_vaults))
 
     vaults = SS_CLIENT.get_vaults()
     compare_vaults(expected_vaults[0], vaults[0])
-    mock.assert_called_with("GET", expected_path)
+    assert mock.called
 
 
-@patch.object(Session, 'request')
-def test_get_vault(mock):
+@pytest.mark.asyncio
+async def test_get_vaults_async(respx_mock):
+    expected_vaults = list_vaults()
+    expected_path = "/v1/vaults"
+
+    mock = respx_mock.get(expected_path).mock(return_value=Response(200, json=expected_vaults))
+
+    vaults = await SS_CLIENT_ASYNC.get_vaults()
+    compare_vaults(expected_vaults[0], vaults[0])
+    assert mock.called
+
+
+def test_get_vault(respx_mock):
     expected_vault = get_vault()
-    expected_path = f"{HOST}/v1/vaults/{VAULT_ID}"
+    expected_path = f"/v1/vaults/{VAULT_ID}"
 
-    mock.return_value.ok = True
-    response = Response()
-    response.status_code = 200
-    response._content = json.dumps(expected_vault).encode("utf8")
-    mock.return_value = response
+    mock = respx_mock.get(expected_path).mock(return_value=Response(200, json=expected_vault))
 
     vault = SS_CLIENT.get_vault(VAULT_ID)
     compare_vaults(expected_vault, vault)
-    mock.assert_called_with("GET", expected_path)
+    assert mock.called
+
+
+@pytest.mark.asyncio
+async def test_get_vault_async(respx_mock):
+    expected_vault = get_vault()
+    expected_path = f"/v1/vaults/{VAULT_ID}"
+
+    mock = respx_mock.get(expected_path).mock(return_value=Response(200, json=expected_vault))
+
+    vault = await SS_CLIENT_ASYNC.get_vault(VAULT_ID)
+    compare_vaults(expected_vault, vault)
+    assert mock.called
 
 
-@patch.object(Session, 'request')
-def test_get_vault_by_title(mock):
+def test_get_vault_by_title(respx_mock):
     expected_vaults = list_vaults()
-    expected_path = f"{HOST}/v1/vaults?filter=name eq \"{VAULT_NAME}\""
+    expected_path = f"/v1/vaults?filter=name eq \"{VAULT_NAME}\""
 
-    mock.return_value.ok = True
-    response = Response()
-    response.status_code = 200
-    response._content = json.dumps(expected_vaults).encode("utf8")
-    mock.return_value = response
+    mock = respx_mock.get(expected_path).mock(return_value=Response(200, json=expected_vaults))
 
     vault = SS_CLIENT.get_vault_by_title(VAULT_NAME)
     compare_vaults(expected_vaults[0], vault)
-    mock.assert_called_with("GET", expected_path)
+    assert mock.called
+
+
+@pytest.mark.asyncio
+async def test_get_vault_by_title_async(respx_mock):
+    expected_vaults = list_vaults()
+    expected_path = f"/v1/vaults?filter=name eq \"{VAULT_NAME}\""
+
+    mock = respx_mock.get(expected_path).mock(return_value=Response(200, json=expected_vaults))
+
+    vault = await SS_CLIENT_ASYNC.get_vault_by_title(VAULT_NAME)
+    compare_vaults(expected_vaults[0], vault)
+    assert mock.called
 
 
 def list_vaults():
diff --git a/src/tests/test_config.py b/src/tests/test_config.py
index 2c15f13..551cbcf 100644
--- a/src/tests/test_config.py
+++ b/src/tests/test_config.py
@@ -1,13 +1,13 @@
-import json
-from requests import Session, Response
-from unittest.mock import patch
+from httpx import Response
 import onepasswordconnectsdk
 from onepasswordconnectsdk import client
 
 VAULT_ID = "abcdefghijklmnopqrstuvwxyz"
 ITEM_NAME1 = "TEST USER"
+ITEM_ID1 = "wepiqdxdzncjtnvmv5fegud4q1"
 ITEM_NAME2 = "Another User"
-HOST = "mock_host"
+ITEM_ID2 = "wepiqdxdzncjtnvmv5fegud4q2"
+HOST = "https://mock_host"
 TOKEN = "jwt_token"
 SS_CLIENT = client.new_client(HOST, TOKEN)
 
@@ -25,20 +25,29 @@ class Config:
 CONFIG_CLASS = Config()
 
 
-@patch.object(Session, 'request')
-def test_load(mock):
-    mock.return_value.ok = True
-    mock.side_effect = get_item_side_effect
+def test_load(respx_mock):
+    mock_items_list1 = respx_mock.get(f"v1/vaults/{VAULT_ID}/items?filter=title eq \"{ITEM_NAME1}\"").mock(
+        return_value=Response(200, json=[item])
+    )
+    mock_item1 = respx_mock.get(f"v1/vaults/{VAULT_ID}/items/{ITEM_ID1}").mock(return_value=Response(200, json=item))
+    mock_items_list2 = respx_mock.get(f"v1/vaults/{VAULT_ID}/items?filter=title eq \"{ITEM_NAME2}\"").mock(
+        return_value=Response(200, json=[item2])
+    )
+    mock_item2 = respx_mock.get(f"v1/vaults/{VAULT_ID}/items/{ITEM_ID2}").mock(return_value=Response(200, json=item2))
 
     config_with_values = onepasswordconnectsdk.load(SS_CLIENT, CONFIG_CLASS)
-    assert mock.called
+
+    assert mock_items_list1.called
+    assert mock_item1.called
+    assert mock_items_list2.called
+    assert mock_item2.called
+
     assert config_with_values.username == USERNAME_VALUE
     assert config_with_values.password == PASSWORD_VALUE
     assert config_with_values.host == HOST_VALUE
 
 
-@patch.object(Session, 'request')
-def test_load_dict(mock):
+def test_load_dict(respx_mock):
     config_dict = {
         "username": {
             "opitem": ITEM_NAME1,
@@ -51,76 +60,61 @@ def test_load_dict(mock):
             "opvault": VAULT_ID
         }
     }
-    mock.return_value.ok = True
-    mock.side_effect = get_item_side_effect
+
+    mock_item_list = respx_mock.get(f"v1/vaults/{VAULT_ID}/items?filter=title eq \"{ITEM_NAME1}\"").mock(
+        return_value=Response(200, json=[item]))
+    mock_item = respx_mock.get(f"v1/vaults/{VAULT_ID}/items/{ITEM_ID1}").mock(return_value=Response(200, json=item))
 
     config_with_values = onepasswordconnectsdk.load_dict(SS_CLIENT, config_dict)
-    assert mock.called
+
+    assert mock_item_list.called
+    assert mock_item.called
     assert config_with_values['username'] == USERNAME_VALUE
     assert config_with_values['password'] == PASSWORD_VALUE
 
 
-def get_item_side_effect(method, url):
-    response = Response()
-    response.status_code = 200
-
-    item = {
-        "id": ITEM_NAME1,
-        "title": ITEM_NAME1,
-        "vault": {
-            "id": VAULT_ID
-        },
-        "category": "LOGIN",
-        "sections": [
-            {
-                "id": "section1",
-                "label": "section1"
-            }
-        ],
-        "fields": [
-            {
-                "id": "password",
-                "label": "password",
-                "value": PASSWORD_VALUE,
-                "section": {
-                    "id": "section1"
-                }
-            },
-            {
-                "id": "716C5B0E95A84092B2FE2CC402E0DDDF",
-                "label": "username",
-                "value": USERNAME_VALUE
+item = {
+    "id": ITEM_ID1,
+    "title": ITEM_NAME1,
+    "vault": {
+        "id": VAULT_ID
+    },
+    "category": "LOGIN",
+    "sections": [
+        {
+            "id": "section1",
+            "label": "section1"
+        }
+    ],
+    "fields": [
+        {
+            "id": "password",
+            "label": "password",
+            "value": PASSWORD_VALUE,
+            "section": {
+                "id": "section1"
             }
-        ]
-    }
-
-    item2 = {
-        "id": ITEM_NAME2,
-        "title": ITEM_NAME2,
-        "vault": {
-            "id": VAULT_ID
         },
-        "category": "LOGIN",
-        "fields": [
-            {
-                "id": "716C5B0E95A84092B2FE2CC402E0DDDF",
-                "label": "host",
-                "value": HOST_VALUE
-            }
-        ]
-    }
-
-    if ITEM_NAME1 in url:
-        if "eq" in url:
-            item = [item]
-        else:
-            item = item
-    elif ITEM_NAME2 in url:
-        if "eq" in url:
-            item = [item2]
-        else:
-            item = item2
-
-    response._content = str.encode(json.dumps(item))
-
-    return response
+        {
+            "id": "username",
+            "label": "username",
+            "value": USERNAME_VALUE
+        }
+    ]
+}
+
+item2 = {
+    "id": ITEM_ID2,
+    "title": ITEM_NAME2,
+    "vault": {
+        "id": VAULT_ID
+    },
+    "category": "LOGIN",
+    "fields": [
+        {
+            "id": "716C5B0E95A84092B2FE2CC402E0DDDF",
+            "label": "host",
+            "value": HOST_VALUE
+        }
+    ]
+}
diff --git a/src/tests/test_utils.py b/src/tests/test_utils.py
new file mode 100644
index 0000000..2ed0374
--- /dev/null
+++ b/src/tests/test_utils.py
@@ -0,0 +1,45 @@
+from onepasswordconnectsdk.utils import PathBuilder
+
+VAULT_ID = "hfnjvi6aymbsnfc2xeeoheizda"
+ITEM_ID = "wepiqdxdzncjtnvmv5fegud4qy"
+FILE_ID = "fileqdxczsc2tn32vsfegud123"
+
+
+def test_all_vaults_path():
+    path = PathBuilder().vaults().build()
+    assert path == "/v1/vaults"
+
+
+def test_single_vault_path():
+    path = PathBuilder().vaults(VAULT_ID).build()
+    assert path == f"/v1/vaults/{VAULT_ID}"
+
+
+def test_all_items_path():
+    path = PathBuilder().vaults(VAULT_ID).items().build()
+    assert path == f"/v1/vaults/{VAULT_ID}/items"
+
+
+def test_filter_items_path():
+    path = PathBuilder().vaults(VAULT_ID).items().query("filter", "title").build()
+    assert path == f"/v1/vaults/{VAULT_ID}/items?filter=title"
+
+
+def test_single_item_path():
+    path = PathBuilder().vaults(VAULT_ID).items(ITEM_ID).build()
+    assert path == f"/v1/vaults/{VAULT_ID}/items/{ITEM_ID}"
+
+
+def test_all_files_path():
+    path = PathBuilder().vaults(VAULT_ID).items(ITEM_ID).files().build()
+    assert path == f"/v1/vaults/{VAULT_ID}/items/{ITEM_ID}/files"
+
+
+def test_single_file_path():
+    path = PathBuilder().vaults(VAULT_ID).items(ITEM_ID).files(FILE_ID).build()
+    assert path == f"/v1/vaults/{VAULT_ID}/items/{ITEM_ID}/files/{FILE_ID}"
+
+
+def test_file_conten_path():
+    path = PathBuilder().vaults(VAULT_ID).items(ITEM_ID).files(FILE_ID).content().build()
+    assert path == f"/v1/vaults/{VAULT_ID}/items/{ITEM_ID}/files/{FILE_ID}/content"