From 5ee681b2d87eeb241d0188f76755dca85c276f9d Mon Sep 17 00:00:00 2001 From: volodymyrZotov Date: Thu, 5 Jan 2023 13:58:39 +0100 Subject: [PATCH 01/22] Use httpx library instead of requests Signed-off-by: volodymyrZotov --- poetry.lock | 153 +++++++++++++++++++--------- pyproject.toml | 2 +- src/onepasswordconnectsdk/client.py | 60 +++++------ 3 files changed, 138 insertions(+), 77 deletions(-) diff --git a/poetry.lock b/poetry.lock index a81aa0d..396bc83 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,3 +1,21 @@ +[[package]] +name = "anyio" +version = "3.6.2" +description = "High level compatibility layer for multiple asynchronous event loop implementations" +category = "main" +optional = false +python-versions = ">=3.6.2" + +[package.dependencies] +idna = ">=2.8" +sniffio = ">=1.1" +typing-extensions = {version = "*", markers = "python_version < \"3.8\""} + +[package.extras] +doc = ["packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] +test = ["contextlib2", "coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "mock (>=4)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (<0.15)", "uvloop (>=0.15)"] +trio = ["trio (>=0.16,<0.22)"] + [[package]] name = "attrs" version = "22.2.0" @@ -21,17 +39,6 @@ category = "main" optional = false python-versions = ">=3.6" -[[package]] -name = "charset-normalizer" -version = "2.1.1" -description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." -category = "main" -optional = false -python-versions = ">=3.6.0" - -[package.extras] -unicode-backport = ["unicodedata2"] - [[package]] name = "colorama" version = "0.4.6" @@ -65,6 +72,55 @@ python-versions = ">=3.7" [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" + +[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" + +[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" + +[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" @@ -169,22 +225,18 @@ python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" six = ">=1.5" [[package]] -name = "requests" -version = "2.28.1" -description = "Python HTTP for Humans." +name = "rfc3986" +version = "1.5.0" +description = "Validating URI References per RFC 3986" category = "main" optional = false -python-versions = ">=3.7, <4" +python-versions = "*" [package.dependencies] -certifi = ">=2017.4.17" -charset-normalizer = ">=2,<3" -idna = ">=2.5,<4" -urllib3 = ">=1.21.1,<1.27" +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" @@ -194,6 +246,14 @@ category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +[[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" + [[package]] name = "tomli" version = "2.0.1" @@ -206,22 +266,9 @@ python-versions = ">=3.7" name = "typing-extensions" version = "4.4.0" description = "Backported and Experimental Type Hints for Python 3.7+" -category = "dev" -optional = false -python-versions = ">=3.7" - -[[package]] -name = "urllib3" -version = "1.26.13" -description = "HTTP library with thread-safe connection pooling, file post, and more." category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" - -[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)"] +python-versions = ">=3.7" [[package]] name = "zipp" @@ -238,9 +285,13 @@ testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools" [metadata] lock-version = "1.1" python-versions = "^3.7" -content-hash = "cac4a5815b13269da64f86e0b0481742124eba1200928ffb3fdd605c12d5c3c8" +content-hash = "56df1ba62090d75443508ab810b7cf8f235361c6725e52f0ecb060e6ee6fc22d" [metadata.files] +anyio = [ + {file = "anyio-3.6.2-py3-none-any.whl", hash = "sha256:fbbe32bd270d2a2ef3ed1c5d45041250284e31fc0a4df4a5a6071842051a51e3"}, + {file = "anyio-3.6.2.tar.gz", hash = "sha256:25ea0d673ae30af41a0c442f81cf3b38c7e79fdc7b60335a4c14e05eb0947421"}, +] attrs = [ {file = "attrs-22.2.0-py3-none-any.whl", hash = "sha256:29e95c7f6778868dbd49170f98f8818f78f3dc5e0e37c0b1f474e3561b240836"}, {file = "attrs-22.2.0.tar.gz", hash = "sha256:c9227bfc2f01993c03f68db37d1d15c9690188323c067c641f1a35ca58185f99"}, @@ -249,10 +300,6 @@ certifi = [ {file = "certifi-2022.12.7-py3-none-any.whl", hash = "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18"}, {file = "certifi-2022.12.7.tar.gz", hash = "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3"}, ] -charset-normalizer = [ - {file = "charset-normalizer-2.1.1.tar.gz", hash = "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845"}, - {file = "charset_normalizer-2.1.1-py3-none-any.whl", hash = "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f"}, -] colorama = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, @@ -314,6 +361,18 @@ exceptiongroup = [ {file = "exceptiongroup-1.1.0-py3-none-any.whl", hash = "sha256:327cbda3da756e2de031a3107b81ab7b3770a602c4d16ca618298c526f4bec1e"}, {file = "exceptiongroup-1.1.0.tar.gz", hash = "sha256:bcb67d800a4497e1b404c2dd44fca47d3b7a5e5433dbab67f96c1a685cdfdf23"}, ] +h11 = [ + {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, + {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, +] +httpcore = [ + {file = "httpcore-0.16.3-py3-none-any.whl", hash = "sha256:da1fb708784a938aa084bde4feb8317056c55037247c787bd7e19eb2c2949dc0"}, + {file = "httpcore-0.16.3.tar.gz", hash = "sha256:c5d6f04e2fc530f39e0c077e6a30caa53f1451096120f1f38b954afd0b17c0cb"}, +] +httpx = [ + {file = "httpx-0.23.3-py3-none-any.whl", hash = "sha256:a211fcce9b1254ea24f0cd6af9869b3d29aba40154e947d2a07bb499b3e310d6"}, + {file = "httpx-0.23.3.tar.gz", hash = "sha256:9818458eb565bb54898ccb9b8b251a28785dd4a55afbc23d0eb410754fe7d0f9"}, +] idna = [ {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, @@ -346,14 +405,18 @@ python-dateutil = [ {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, ] -requests = [ - {file = "requests-2.28.1-py3-none-any.whl", hash = "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349"}, - {file = "requests-2.28.1.tar.gz", hash = "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983"}, +rfc3986 = [ + {file = "rfc3986-1.5.0-py2.py3-none-any.whl", hash = "sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97"}, + {file = "rfc3986-1.5.0.tar.gz", hash = "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835"}, ] six = [ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, ] +sniffio = [ + {file = "sniffio-1.3.0-py3-none-any.whl", hash = "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"}, + {file = "sniffio-1.3.0.tar.gz", hash = "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101"}, +] tomli = [ {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, @@ -362,10 +425,6 @@ typing-extensions = [ {file = "typing_extensions-4.4.0-py3-none-any.whl", hash = "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e"}, {file = "typing_extensions-4.4.0.tar.gz", hash = "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa"}, ] -urllib3 = [ - {file = "urllib3-1.26.13-py2.py3-none-any.whl", hash = "sha256:47cc05d99aaa09c9e72ed5809b60e7ba354e64b59c9c173ac3018642d8bb41fc"}, - {file = "urllib3-1.26.13.tar.gz", hash = "sha256:c083dd0dce68dbfbe1129d5271cb90f9447dea7d52097c6e0126120c521ddea8"}, -] zipp = [ {file = "zipp-3.11.0-py3-none-any.whl", hash = "sha256:83a28fcb75844b5c0cdaf5aa4003c2d728c77e05f5aeabe8e95e56727005fbaa"}, {file = "zipp-3.11.0.tar.gz", hash = "sha256:a7a22e05929290a67401440b39690ae6563279bced5f314609d9d03798f56766"}, diff --git a/pyproject.toml b/pyproject.toml index 7f2595e..b2d40c1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,8 +13,8 @@ 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] pytest = "^7.2.0" diff --git a/src/onepasswordconnectsdk/client.py b/src/onepasswordconnectsdk/client.py index 2950b80..f7ee7d7 100644 --- a/src/onepasswordconnectsdk/client.py +++ b/src/onepasswordconnectsdk/client.py @@ -1,11 +1,11 @@ """Python Client for connecting to 1Password Connect""" +import httpx +from httpx import HTTPError from dateutil.parser import parse import json import os import re -import requests import datetime -from requests.exceptions import HTTPError import onepasswordconnectsdk from onepasswordconnectsdk.models import Item, ItemVault from onepasswordconnectsdk.models.constants import CONNECT_HOST_ENV_VARIABLE @@ -28,28 +28,12 @@ class Client: """Python Client Class""" - def __init__(self, url: str, token: str): + def __init__(self, session): """Initialize client""" - self.url = url - self.token = token - self.session = self.create_session() + self.session = session - def create_session(self): - session = requests.Session() - session.headers.update(self.build_headers()) - return session - - def build_headers(self): - """Builds the headers needed to make a request to the server - - 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}" @@ -250,7 +234,7 @@ def create_item(self, vault_id: str, item: Item): url = f"/v1/vaults/{vault_id}/items" - response: requests.Response = self.build_request("POST", url, item) + response = self.build_request("POST", url, item) try: response.raise_for_status() except HTTPError: @@ -279,7 +263,7 @@ def update_item(self, item_uuid: str, vault_id: str, item: Item): 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: @@ -380,13 +364,12 @@ 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) + 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): @@ -591,7 +574,20 @@ def _is_valid_UUID(self, uuid): return True -def new_client(url: str, token: str): +class AsyncClient: + pass + + +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"} + + +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 @@ -600,7 +596,13 @@ def new_client(url: str, token: str): Returns: Client: The 1Password Connect client """ - return Client(url=url, token=token) + headers = build_headers(token) + if is_async: + session = httpx.AsyncClient(base_url=url, headers=headers) + return AsyncClient(session=session) + + session = httpx.Client(base_url=url, headers=headers) + return Client(session=session) def new_client_from_environment(url: str = None): From f5d3cafe8c69f6ebe3f0dff894d9974aa8437782 Mon Sep 17 00:00:00 2001 From: volodymyrZotov Date: Thu, 5 Jan 2023 16:53:26 +0100 Subject: [PATCH 02/22] Extract serialization logic to standalone class Signed-off-by: volodymyrZotov --- src/onepasswordconnectsdk/client.py | 284 +++--------------------- src/onepasswordconnectsdk/errors.py | 22 ++ src/onepasswordconnectsdk/serializer.py | 211 ++++++++++++++++++ src/onepasswordconnectsdk/utils.py | 11 + 4 files changed, 271 insertions(+), 257 deletions(-) create mode 100644 src/onepasswordconnectsdk/errors.py create mode 100644 src/onepasswordconnectsdk/serializer.py create mode 100644 src/onepasswordconnectsdk/utils.py diff --git a/src/onepasswordconnectsdk/client.py b/src/onepasswordconnectsdk/client.py index f7ee7d7..9b8e288 100644 --- a/src/onepasswordconnectsdk/client.py +++ b/src/onepasswordconnectsdk/client.py @@ -1,36 +1,30 @@ """Python Client for connecting to 1Password Connect""" import httpx from httpx import HTTPError -from dateutil.parser import parse import json import os -import re -import datetime -import onepasswordconnectsdk + +from onepasswordconnectsdk.serializer import Serializer +from onepasswordconnectsdk.utils import is_valid_uuid +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 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, session): + def __init__(self, session, serializer): """Initialize client""" self.session = session + self.serializer = serializer def __del__(self): self.session.close() @@ -45,7 +39,7 @@ 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" @@ -58,7 +52,7 @@ 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): url = f"/v1/vaults/{vault_id}/items/{item_id}/files/{file_id}/content" @@ -99,10 +93,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) @@ -131,7 +125,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 @@ -165,7 +159,7 @@ 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): @@ -192,7 +186,7 @@ def get_items(self, vault_id: str): 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 @@ -242,7 +236,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. @@ -271,7 +265,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 @@ -296,7 +290,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 @@ -329,7 +323,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 @@ -352,7 +346,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 @@ -366,213 +360,12 @@ def build_request(self, method: str, path: str, body=None): """ if body: - serialized_body = json.dumps(self.sanitize_for_serialization(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): - """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 - - 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 - class AsyncClient: pass @@ -597,12 +390,13 @@ def new_client(url: str, token: str, is_async: bool = False): Client: The 1Password Connect client """ headers = build_headers(token) + serializer = Serializer() if is_async: session = httpx.AsyncClient(base_url=url, headers=headers) - return AsyncClient(session=session) + return AsyncClient(session=session, serializer=serializer) session = httpx.Client(base_url=url, headers=headers) - return Client(session=session) + return Client(session=session, serializer=serializer) def new_client_from_environment(url: str = None): @@ -632,27 +426,3 @@ def new_client_from_environment(url: str = None): ) 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 diff --git a/src/onepasswordconnectsdk/errors.py b/src/onepasswordconnectsdk/errors.py new file mode 100644 index 0000000..1a24bb4 --- /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 \ No newline at end of file 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..4a2c8e1 --- /dev/null +++ b/src/onepasswordconnectsdk/utils.py @@ -0,0 +1,11 @@ +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 From 7e286ee481e4649376b8b0df0dbba71a4921b821 Mon Sep 17 00:00:00 2001 From: volodymyrZotov Date: Fri, 6 Jan 2023 13:03:58 +0100 Subject: [PATCH 03/22] Add AsyncClient methods Signed-off-by: volodymyrZotov --- src/onepasswordconnectsdk/client.py | 344 +++++++++++++++++++++++++++- 1 file changed, 343 insertions(+), 1 deletion(-) diff --git a/src/onepasswordconnectsdk/client.py b/src/onepasswordconnectsdk/client.py index 9b8e288..c66c536 100644 --- a/src/onepasswordconnectsdk/client.py +++ b/src/onepasswordconnectsdk/client.py @@ -368,7 +368,349 @@ def build_request(self, method: str, path: str, body=None): class AsyncClient: - pass + """Python Async Client Class""" + + def __init__(self, session, serializer): + """Initialize client""" + self.session = session + self.serializer = serializer + + async 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}" + 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 = f"/v1/vaults/{vault_id}/items/{item_id}/files" + + 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): + url = f"/v1/vaults/{vault_id}/items/{item_id}/files/{file_id}/content" + + 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 + content = await self.get_file_content(file_id, item_id, vault_id) + 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_id = self.get_vault_by_title(vault).id + + if is_valid_uuid(item): + return self.get_item_by_id(item, vault_id) + else: + return 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 = f"/v1/vaults/{vault_id}/items/{item_id}" + + 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 = f"/v1/vaults/{vault_id}/items?filter={filter_query}" + + 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): + """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 + """ + url = f"/v1/vaults/{vault_id}/items" + + 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 = f"/v1/vaults/{vault_id}/items/{item_id}" + + 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 = f"/v1/vaults/{vault_id}/items" + + 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 = f"/v1/vaults/{vault_id}/items/{item_uuid}" + 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 = f"/v1/vaults/{vault_id}" + 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 = f"/v1/vaults?filter={filter_query}" + + 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 = "/v1/vaults" + 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 build_headers(token: str): From c073954b0f0bfbd71022ee13cb1e7141f4a9b0d5 Mon Sep 17 00:00:00 2001 From: volodymyrZotov Date: Mon, 9 Jan 2023 15:49:18 +0100 Subject: [PATCH 04/22] Make tests to use respx library Signed-off-by: volodymyrZotov --- poetry.lock | 127 ++++++++++-------- pyproject.toml | 1 + src/onepasswordconnectsdk/client.py | 4 +- src/tests/test_client_items.py | 192 +++++++++------------------- src/tests/test_client_vaults.py | 43 ++----- src/tests/test_config.py | 146 ++++++++++----------- 6 files changed, 217 insertions(+), 296 deletions(-) diff --git a/poetry.lock b/poetry.lock index 396bc83..0c37d5d 100644 --- a/poetry.lock +++ b/poetry.lock @@ -49,7 +49,7 @@ python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7 [[package]] name = "coverage" -version = "7.0.1" +version = "7.0.3" description = "Code coverage measurement for Python" category = "dev" optional = false @@ -131,7 +131,7 @@ python-versions = ">=3.5" [[package]] name = "importlib-metadata" -version = "5.2.0" +version = "6.0.0" description = "Read metadata from Python packages" category = "dev" optional = false @@ -224,6 +224,17 @@ python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" [package.dependencies] six = ">=1.5" +[[package]] +name = "respx" +version = "0.20.1" +description = "A utility for mocking out the Python HTTPX and HTTP Core libraries." +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +httpx = ">=0.21.0" + [[package]] name = "rfc3986" version = "1.5.0" @@ -285,7 +296,7 @@ testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools" [metadata] lock-version = "1.1" python-versions = "^3.7" -content-hash = "56df1ba62090d75443508ab810b7cf8f235361c6725e52f0ecb060e6ee6fc22d" +content-hash = "ebabcc843f5c0064cfbcff7b8940a475b4b4fb48f10731c60f131b13f1122417" [metadata.files] anyio = [ @@ -305,57 +316,57 @@ colorama = [ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] coverage = [ - {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.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f7c51b6074a8a3063c341953dffe48fd6674f8e4b1d3c8aa8a91f58d6e716a8"}, + {file = "coverage-7.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:628f47eaf66727fc986d3b190d6fa32f5e6b7754a243919d28bc0fd7974c449f"}, + {file = "coverage-7.0.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e89d5abf86c104de808108a25d171ad646c07eda96ca76c8b237b94b9c71e518"}, + {file = "coverage-7.0.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:75e43c6f4ea4d122dac389aabdf9d4f0e160770a75e63372f88005d90f5bcc80"}, + {file = "coverage-7.0.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49da0ff241827ebb52d5d6d5a36d33b455fa5e721d44689c95df99fd8db82437"}, + {file = "coverage-7.0.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0bce4ad5bdd0b02e177a085d28d2cea5fc57bb4ba2cead395e763e34cf934eb1"}, + {file = "coverage-7.0.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:f79691335257d60951638dd43576b9bcd6f52baa5c1c2cd07a509bb003238372"}, + {file = "coverage-7.0.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5722269ed05fbdb94eef431787c66b66260ff3125d1a9afcc00facff8c45adf9"}, + {file = "coverage-7.0.3-cp310-cp310-win32.whl", hash = "sha256:bdbda870e0fda7dd0fe7db7135ca226ec4c1ade8aa76e96614829b56ca491012"}, + {file = "coverage-7.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:e56fae4292e216b8deeee38ace84557b9fa85b52db005368a275427cdabb8192"}, + {file = "coverage-7.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b82343a5bc51627b9d606f0b6b6b9551db7b6311a5dd920fa52a94beae2e8959"}, + {file = "coverage-7.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fd0a8aa431f9b7ad9eb8264f55ef83cbb254962af3775092fb6e93890dea9ca2"}, + {file = "coverage-7.0.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:112cfead1bd22eada8a8db9ed387bd3e8be5528debc42b5d3c1f7da4ffaf9fb5"}, + {file = "coverage-7.0.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:af87e906355fa42447be5c08c5d44e6e1c005bf142f303f726ddf5ed6e0c8a4d"}, + {file = "coverage-7.0.3-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f30090e22a301952c5abd0e493a1c8358b4f0b368b49fa3e4568ed3ed68b8d1f"}, + {file = "coverage-7.0.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ae871d09901911eedda1981ea6fd0f62a999107293cdc4c4fd612321c5b34745"}, + {file = "coverage-7.0.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:ed7c9debf7bfc63c9b9f8b595409237774ff4b061bf29fba6f53b287a2fdeab9"}, + {file = "coverage-7.0.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:13121fa22dcd2c7b19c5161e3fd725692448f05377b788da4502a383573227b3"}, + {file = "coverage-7.0.3-cp311-cp311-win32.whl", hash = "sha256:037b51ee86bc600f99b3b957c20a172431c35c2ef9c1ca34bc813ab5b51fd9f5"}, + {file = "coverage-7.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:25fde928306034e8deecd5fc91a07432dcc282c8acb76749581a28963c9f4f3f"}, + {file = "coverage-7.0.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:7e8b0642c38b3d3b3c01417643ccc645345b03c32a2e84ef93cdd6844d6fe530"}, + {file = "coverage-7.0.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18b09811f849cc958d23f733a350a66b54a8de3fed1e6128ba55a5c97ffb6f65"}, + {file = "coverage-7.0.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:349d0b545520e8516f7b4f12373afc705d17d901e1de6a37a20e4ec9332b61f7"}, + {file = "coverage-7.0.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b5b38813eee5b4739f505d94247604c72eae626d5088a16dd77b08b8b1724ab3"}, + {file = "coverage-7.0.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:ba9af1218fa01b1f11c72271bc7290b701d11ad4dbc2ae97c445ecacf6858dba"}, + {file = "coverage-7.0.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c5648c7eec5cf1ba5db1cf2d6c10036a582d7f09e172990474a122e30c841361"}, + {file = "coverage-7.0.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d0df04495b76a885bfef009f45eebe8fe2fbf815ad7a83dabcf5aced62f33162"}, + {file = "coverage-7.0.3-cp37-cp37m-win32.whl", hash = "sha256:af6cef3796b8068713a48dd67d258dc9a6e2ebc3bd4645bfac03a09672fa5d20"}, + {file = "coverage-7.0.3-cp37-cp37m-win_amd64.whl", hash = "sha256:62ef3800c4058844e2e3fa35faa9dd0ccde8a8aba6c763aae50342e00d4479d4"}, + {file = "coverage-7.0.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:acef7f3a3825a2d218a03dd02f5f3cc7f27aa31d882dd780191d1ad101120d74"}, + {file = "coverage-7.0.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a530663a361eb27375cec28aea5cd282089b5e4b022ae451c4c3493b026a68a5"}, + {file = "coverage-7.0.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c58cd6bb46dcb922e0d5792850aab5964433d511b3a020867650f8d930dde4f4"}, + {file = "coverage-7.0.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f918e9ef4c98f477a5458238dde2a1643aed956c7213873ab6b6b82e32b8ef61"}, + {file = "coverage-7.0.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2b865aa679bee7fbd1c55960940dbd3252621dd81468268786c67122bbd15343"}, + {file = "coverage-7.0.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:c5d9b480ebae60fc2cbc8d6865194136bc690538fa542ba58726433bed6e04cc"}, + {file = "coverage-7.0.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:985ad2af5ec3dbb4fd75d5b0735752c527ad183455520055a08cf8d6794cabfc"}, + {file = "coverage-7.0.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ca15308ef722f120967af7474ba6a453e0f5b6f331251e20b8145497cf1bc14a"}, + {file = "coverage-7.0.3-cp38-cp38-win32.whl", hash = "sha256:c1cee10662c25c94415bbb987f2ec0e6ba9e8fce786334b10be7e6a7ab958f69"}, + {file = "coverage-7.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:44d6a556de4418f1f3bfd57094b8c49f0408df5a433cf0d253eeb3075261c762"}, + {file = "coverage-7.0.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e6dcc70a25cb95df0ae33dfc701de9b09c37f7dd9f00394d684a5b57257f8246"}, + {file = "coverage-7.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:bf76d79dfaea802f0f28f50153ffbc1a74ae1ee73e480baeda410b4f3e7ab25f"}, + {file = "coverage-7.0.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88834e5d56d01c141c29deedacba5773fe0bed900b1edc957595a8a6c0da1c3c"}, + {file = "coverage-7.0.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef001a60e888f8741e42e5aa79ae55c91be73761e4df5e806efca1ddd62fd400"}, + {file = "coverage-7.0.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4959dc506be74e4963bd2c42f7b87d8e4b289891201e19ec551e64c6aa5441f8"}, + {file = "coverage-7.0.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b791beb17b32ac019a78cfbe6184f992b6273fdca31145b928ad2099435e2fcb"}, + {file = "coverage-7.0.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:b07651e3b9af8f1a092861d88b4c74d913634a7f1f2280fca0ad041ad84e9e96"}, + {file = "coverage-7.0.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:55e46fa4168ccb7497c9be78627fcb147e06f474f846a10d55feeb5108a24ef0"}, + {file = "coverage-7.0.3-cp39-cp39-win32.whl", hash = "sha256:e3f1cd1cd65695b1540b3cf7828d05b3515974a9d7c7530f762ac40f58a18161"}, + {file = "coverage-7.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:d8249666c23683f74f8f93aeaa8794ac87cc61c40ff70374a825f3352a4371dc"}, + {file = "coverage-7.0.3-pp37.pp38.pp39-none-any.whl", hash = "sha256:b1ffc8f58b81baed3f8962e28c30d99442079b82ce1ec836a1f67c0accad91c1"}, + {file = "coverage-7.0.3.tar.gz", hash = "sha256:d5be4e93acce64f516bf4fd239c0e6118fc913c93fa1a3f52d15bdcc60d97b2d"}, ] exceptiongroup = [ {file = "exceptiongroup-1.1.0-py3-none-any.whl", hash = "sha256:327cbda3da756e2de031a3107b81ab7b3770a602c4d16ca618298c526f4bec1e"}, @@ -378,8 +389,8 @@ idna = [ {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, ] importlib-metadata = [ - {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.0.0-py3-none-any.whl", hash = "sha256:7efb448ec9a5e313a57655d35aa54cd3e01b7e1fbcf72dce1bf06119420f5bad"}, + {file = "importlib_metadata-6.0.0.tar.gz", hash = "sha256:e354bedeb60efa6affdcc8ae121b73544a7aa74156d047311948f6d711cd378d"}, ] iniconfig = [ {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, @@ -405,6 +416,10 @@ python-dateutil = [ {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, ] +respx = [ + {file = "respx-0.20.1-py2.py3-none-any.whl", hash = "sha256:372f06991c03d1f7f480a420a2199d01f1815b6ed5a802f4e4628043a93bd03e"}, + {file = "respx-0.20.1.tar.gz", hash = "sha256:cc47a86d7010806ab65abdcf3b634c56337a737bb5c4d74c19a0dfca83b3bc73"}, +] rfc3986 = [ {file = "rfc3986-1.5.0-py2.py3-none-any.whl", hash = "sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97"}, {file = "rfc3986-1.5.0.tar.gz", hash = "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835"}, diff --git a/pyproject.toml b/pyproject.toml index b2d40c1..26b5027 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,6 +19,7 @@ httpx = "^0.23.3" [tool.poetry.dev-dependencies] pytest = "^7.2.0" pytest-cov = "^4.0.0" +respx = "^0.20.1" [build-system] requires = ["poetry>=0.12"] diff --git a/src/onepasswordconnectsdk/client.py b/src/onepasswordconnectsdk/client.py index c66c536..3bd75ae 100644 --- a/src/onepasswordconnectsdk/client.py +++ b/src/onepasswordconnectsdk/client.py @@ -16,6 +16,7 @@ from onepasswordconnectsdk.models.constants import CONNECT_HOST_ENV_VARIABLE ENV_SERVICE_ACCOUNT_JWT_VARIABLE = "OP_CONNECT_TOKEN" +ENV_IS_ASYNC_CLIENT = "OP_CONNECT_CLIENT_ASYNC" class Client: @@ -753,6 +754,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) @@ -767,4 +769,4 @@ def new_client_from_environment(url: str = None): f"{ENV_SERVICE_ACCOUNT_JWT_VARIABLE} variable" ) - return Client(url=url, token=token) + return new_client(url, token, is_async) diff --git a/src/tests/test_client_items.py b/src/tests/test_client_items.py index 0a494eb..ec74a9a 100644 --- a/src/tests/test_client_items.py +++ b/src/tests/test_client_items.py @@ -1,203 +1,136 @@ -import json -from requests import Session, Response -from unittest.mock import patch +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) -@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): +def test_get_item_by_title(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}" - - mock.return_value.ok = True - - response_item_summary = Response() - response_item_summary.status_code = 200 - response_item_summary._content = json.dumps(get_items()).encode("utf8") + 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}" - 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] + 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 -@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): +def test_get_item_by_item_id_vault_title(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}" - - mock.return_value.ok = True + expected_path_vault_title = f"/v1/vaults?filter=name eq \"{VAULT_TITLE}\"" + expected_path_item = f"/v1/vaults/{VAULT_ID}/items/{ITEM_ID}" - 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") - - 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): +def test_get_item_by_item_title_vault_id(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}" - - mock.return_value.ok = True - - 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") + 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_item_summary, response_item] + 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): +def test_get_item_by_item_title_vault_title(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_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.return_value.ok = True - - response_vault = Response() - response_vault.status_code = 200 - response_vault._content = json.dumps(get_vaults()).encode("utf8") - - 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") - - 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): +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 -@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 -@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(VAULT_ID, item) + created_item = SS_CLIENT.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, VAULT_ID, item) + updated_item = SS_CLIENT.update_item(item.id, item.vault.id, item) assert mock.called compare_full_items(item, updated_item) @@ -220,13 +153,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..892f4db 100644 --- a/src/tests/test_client_vaults.py +++ b/src/tests/test_client_vaults.py @@ -1,61 +1,44 @@ -import json -from requests import Session, Response -from unittest.mock import patch +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) -@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): +def test_get_vault(respx_mock): expected_vault = get_vault() expected_path = f"{HOST}/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 -@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 def list_vaults(): diff --git a/src/tests/test_config.py b/src/tests/test_config.py index 2c15f13..64a0b61 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": "716C5B0E95A84092B2FE2CC402E0DDDF", + "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 + } + ] +} From 4eb89b33fc6ccb896a77c53bf77031fb667f4e08 Mon Sep 17 00:00:00 2001 From: volodymyrZotov Date: Mon, 9 Jan 2023 16:39:12 +0100 Subject: [PATCH 05/22] Cover AsyncClient with tests Signed-off-by: volodymyrZotov --- poetry.lock | 142 ++++++++++++++++------------ pyproject.toml | 3 +- src/onepasswordconnectsdk/client.py | 7 +- src/tests/test_client_items.py | 138 +++++++++++++++++++++++++++ src/tests/test_client_vaults.py | 40 +++++++- 5 files changed, 264 insertions(+), 66 deletions(-) diff --git a/poetry.lock b/poetry.lock index 0c37d5d..af1e32f 100644 --- a/poetry.lock +++ b/poetry.lock @@ -49,7 +49,7 @@ python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7 [[package]] name = "coverage" -version = "7.0.3" +version = "7.0.4" description = "Code coverage measurement for Python" category = "dev" optional = false @@ -148,15 +148,15 @@ testing = ["flake8 (<5)", "flufl.flake8", "importlib-resources (>=1.3)", "packag [[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" [[package]] name = "packaging" -version = "22.0" +version = "23.0" description = "Core utilities for Python packages" category = "dev" optional = false @@ -198,6 +198,22 @@ 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"] +[[package]] +name = "pytest-asyncio" +version = "0.20.3" +description = "Pytest support for asyncio" +category = "dev" +optional = false +python-versions = ">=3.7" + +[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" @@ -296,7 +312,7 @@ testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools" [metadata] lock-version = "1.1" python-versions = "^3.7" -content-hash = "ebabcc843f5c0064cfbcff7b8940a475b4b4fb48f10731c60f131b13f1122417" +content-hash = "5c06df5db167647617c8fb7afc9da451dcec9d10e24df37e6024124cccd6da60" [metadata.files] anyio = [ @@ -316,57 +332,57 @@ colorama = [ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] coverage = [ - {file = "coverage-7.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f7c51b6074a8a3063c341953dffe48fd6674f8e4b1d3c8aa8a91f58d6e716a8"}, - {file = "coverage-7.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:628f47eaf66727fc986d3b190d6fa32f5e6b7754a243919d28bc0fd7974c449f"}, - {file = "coverage-7.0.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e89d5abf86c104de808108a25d171ad646c07eda96ca76c8b237b94b9c71e518"}, - {file = "coverage-7.0.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:75e43c6f4ea4d122dac389aabdf9d4f0e160770a75e63372f88005d90f5bcc80"}, - {file = "coverage-7.0.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49da0ff241827ebb52d5d6d5a36d33b455fa5e721d44689c95df99fd8db82437"}, - {file = "coverage-7.0.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0bce4ad5bdd0b02e177a085d28d2cea5fc57bb4ba2cead395e763e34cf934eb1"}, - {file = "coverage-7.0.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:f79691335257d60951638dd43576b9bcd6f52baa5c1c2cd07a509bb003238372"}, - {file = "coverage-7.0.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5722269ed05fbdb94eef431787c66b66260ff3125d1a9afcc00facff8c45adf9"}, - {file = "coverage-7.0.3-cp310-cp310-win32.whl", hash = "sha256:bdbda870e0fda7dd0fe7db7135ca226ec4c1ade8aa76e96614829b56ca491012"}, - {file = "coverage-7.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:e56fae4292e216b8deeee38ace84557b9fa85b52db005368a275427cdabb8192"}, - {file = "coverage-7.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b82343a5bc51627b9d606f0b6b6b9551db7b6311a5dd920fa52a94beae2e8959"}, - {file = "coverage-7.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fd0a8aa431f9b7ad9eb8264f55ef83cbb254962af3775092fb6e93890dea9ca2"}, - {file = "coverage-7.0.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:112cfead1bd22eada8a8db9ed387bd3e8be5528debc42b5d3c1f7da4ffaf9fb5"}, - {file = "coverage-7.0.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:af87e906355fa42447be5c08c5d44e6e1c005bf142f303f726ddf5ed6e0c8a4d"}, - {file = "coverage-7.0.3-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f30090e22a301952c5abd0e493a1c8358b4f0b368b49fa3e4568ed3ed68b8d1f"}, - {file = "coverage-7.0.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ae871d09901911eedda1981ea6fd0f62a999107293cdc4c4fd612321c5b34745"}, - {file = "coverage-7.0.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:ed7c9debf7bfc63c9b9f8b595409237774ff4b061bf29fba6f53b287a2fdeab9"}, - {file = "coverage-7.0.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:13121fa22dcd2c7b19c5161e3fd725692448f05377b788da4502a383573227b3"}, - {file = "coverage-7.0.3-cp311-cp311-win32.whl", hash = "sha256:037b51ee86bc600f99b3b957c20a172431c35c2ef9c1ca34bc813ab5b51fd9f5"}, - {file = "coverage-7.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:25fde928306034e8deecd5fc91a07432dcc282c8acb76749581a28963c9f4f3f"}, - {file = "coverage-7.0.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:7e8b0642c38b3d3b3c01417643ccc645345b03c32a2e84ef93cdd6844d6fe530"}, - {file = "coverage-7.0.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18b09811f849cc958d23f733a350a66b54a8de3fed1e6128ba55a5c97ffb6f65"}, - {file = "coverage-7.0.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:349d0b545520e8516f7b4f12373afc705d17d901e1de6a37a20e4ec9332b61f7"}, - {file = "coverage-7.0.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b5b38813eee5b4739f505d94247604c72eae626d5088a16dd77b08b8b1724ab3"}, - {file = "coverage-7.0.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:ba9af1218fa01b1f11c72271bc7290b701d11ad4dbc2ae97c445ecacf6858dba"}, - {file = "coverage-7.0.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c5648c7eec5cf1ba5db1cf2d6c10036a582d7f09e172990474a122e30c841361"}, - {file = "coverage-7.0.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d0df04495b76a885bfef009f45eebe8fe2fbf815ad7a83dabcf5aced62f33162"}, - {file = "coverage-7.0.3-cp37-cp37m-win32.whl", hash = "sha256:af6cef3796b8068713a48dd67d258dc9a6e2ebc3bd4645bfac03a09672fa5d20"}, - {file = "coverage-7.0.3-cp37-cp37m-win_amd64.whl", hash = "sha256:62ef3800c4058844e2e3fa35faa9dd0ccde8a8aba6c763aae50342e00d4479d4"}, - {file = "coverage-7.0.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:acef7f3a3825a2d218a03dd02f5f3cc7f27aa31d882dd780191d1ad101120d74"}, - {file = "coverage-7.0.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a530663a361eb27375cec28aea5cd282089b5e4b022ae451c4c3493b026a68a5"}, - {file = "coverage-7.0.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c58cd6bb46dcb922e0d5792850aab5964433d511b3a020867650f8d930dde4f4"}, - {file = "coverage-7.0.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f918e9ef4c98f477a5458238dde2a1643aed956c7213873ab6b6b82e32b8ef61"}, - {file = "coverage-7.0.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2b865aa679bee7fbd1c55960940dbd3252621dd81468268786c67122bbd15343"}, - {file = "coverage-7.0.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:c5d9b480ebae60fc2cbc8d6865194136bc690538fa542ba58726433bed6e04cc"}, - {file = "coverage-7.0.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:985ad2af5ec3dbb4fd75d5b0735752c527ad183455520055a08cf8d6794cabfc"}, - {file = "coverage-7.0.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ca15308ef722f120967af7474ba6a453e0f5b6f331251e20b8145497cf1bc14a"}, - {file = "coverage-7.0.3-cp38-cp38-win32.whl", hash = "sha256:c1cee10662c25c94415bbb987f2ec0e6ba9e8fce786334b10be7e6a7ab958f69"}, - {file = "coverage-7.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:44d6a556de4418f1f3bfd57094b8c49f0408df5a433cf0d253eeb3075261c762"}, - {file = "coverage-7.0.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e6dcc70a25cb95df0ae33dfc701de9b09c37f7dd9f00394d684a5b57257f8246"}, - {file = "coverage-7.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:bf76d79dfaea802f0f28f50153ffbc1a74ae1ee73e480baeda410b4f3e7ab25f"}, - {file = "coverage-7.0.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88834e5d56d01c141c29deedacba5773fe0bed900b1edc957595a8a6c0da1c3c"}, - {file = "coverage-7.0.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef001a60e888f8741e42e5aa79ae55c91be73761e4df5e806efca1ddd62fd400"}, - {file = "coverage-7.0.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4959dc506be74e4963bd2c42f7b87d8e4b289891201e19ec551e64c6aa5441f8"}, - {file = "coverage-7.0.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b791beb17b32ac019a78cfbe6184f992b6273fdca31145b928ad2099435e2fcb"}, - {file = "coverage-7.0.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:b07651e3b9af8f1a092861d88b4c74d913634a7f1f2280fca0ad041ad84e9e96"}, - {file = "coverage-7.0.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:55e46fa4168ccb7497c9be78627fcb147e06f474f846a10d55feeb5108a24ef0"}, - {file = "coverage-7.0.3-cp39-cp39-win32.whl", hash = "sha256:e3f1cd1cd65695b1540b3cf7828d05b3515974a9d7c7530f762ac40f58a18161"}, - {file = "coverage-7.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:d8249666c23683f74f8f93aeaa8794ac87cc61c40ff70374a825f3352a4371dc"}, - {file = "coverage-7.0.3-pp37.pp38.pp39-none-any.whl", hash = "sha256:b1ffc8f58b81baed3f8962e28c30d99442079b82ce1ec836a1f67c0accad91c1"}, - {file = "coverage-7.0.3.tar.gz", hash = "sha256:d5be4e93acce64f516bf4fd239c0e6118fc913c93fa1a3f52d15bdcc60d97b2d"}, + {file = "coverage-7.0.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:daf91db39324e9939a9db919ee4fb42a1a23634a056616dae891a030e89f87ba"}, + {file = "coverage-7.0.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:55121fe140d7e42cb970999b93cf1c2b24484ce028b32bbd00238bb25c13e34a"}, + {file = "coverage-7.0.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c027fbb83a8c78a6e06a0302ea1799fdb70e5cda9845a5e000545b8e2b47ea39"}, + {file = "coverage-7.0.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:caf82db5b7f16b51ec32fe0bd2da0805b177c807aa8bfb478c7e6f893418c284"}, + {file = "coverage-7.0.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ba5cc54baf3c322c4388de2a43cc95f7809366f0600e743e5aae8ea9d1038b2"}, + {file = "coverage-7.0.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:260854160083f8275a9d9d49a05ab0ffc7a1f08f2ccccbfaec94a18aae9f407c"}, + {file = "coverage-7.0.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:ea45f0dba5a993e93b158f1a9dcfff2770e3bcabf2b80dbe7aa15dce0bcb3bf3"}, + {file = "coverage-7.0.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6abc91f6f8b3cc0ae1034e2c03f38769fba1952ab70d0b26953aa01691265c39"}, + {file = "coverage-7.0.4-cp310-cp310-win32.whl", hash = "sha256:053cdc47cae08257051d7e934a0de4d095b60eb8a3024fa9f1b2322fa1547137"}, + {file = "coverage-7.0.4-cp310-cp310-win_amd64.whl", hash = "sha256:1e9e94f2612ee549a4b3ee79cbc61bceed77e69cf38cfa05858bae939a886d16"}, + {file = "coverage-7.0.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5caa9dd91dcc5f054350dc57a02e053d79633907b9ccffff999568d13dcd19f8"}, + {file = "coverage-7.0.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:efc200fa75d9634525b40babc7a16342bd21c101db1a58ef84dc14f4bf6ac0fd"}, + {file = "coverage-7.0.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1791e5f74c5b52f76e83fe9f4bb9571cf76d40ee0c51952ee1e4ee935b7e98b9"}, + {file = "coverage-7.0.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3d9201cfa5a98652b9cef36ab202f17fe3ea83f497b4ba2a8ed39399dfb8fcd4"}, + {file = "coverage-7.0.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22d8ef6865cb6834cab2b72fff20747a55c714b57b675f7e11c9624fe4f7cb45"}, + {file = "coverage-7.0.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b84076e3de192fba0f95e279ac017b64c7c6ecd4f09f36f13420f5bed898a9c7"}, + {file = "coverage-7.0.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:dcfbf8ffc046f20d75fd775a92c378f6fc7b9bded6c6f2ab88b6b9cb5805a184"}, + {file = "coverage-7.0.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:4665a714af31f160403c2e448fb2fef330719d2e04e836b08d60d612707c1041"}, + {file = "coverage-7.0.4-cp311-cp311-win32.whl", hash = "sha256:2e59aef3fba5758059208c9eff10ae7ded3629e797972746ec33b56844f69411"}, + {file = "coverage-7.0.4-cp311-cp311-win_amd64.whl", hash = "sha256:2b854f7985b48122b6fe346631e86d67b63293f8255cb59a93d79e3d9f1574e3"}, + {file = "coverage-7.0.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e44b60b0b49aa85d548d392a2dca2c6a581cd4084e72e9e16bd58bd86ec20816"}, + {file = "coverage-7.0.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2904d7a0388911c61e7e3beefe48c29dfccaba938fc1158f63190101a21e04c2"}, + {file = "coverage-7.0.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bc74b64bfa89e2f862ea45dd6ac1def371d7cc883b76680d20bdd61a6f3daa20"}, + {file = "coverage-7.0.4-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c06046f54e719da21c79f98ecc0962581d1aee0b3798dc6b12b1217da8bf93f4"}, + {file = "coverage-7.0.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:bc9c77004970a364a1e5454cf7cb884e4277592b959c287689b2a0fd027ef552"}, + {file = "coverage-7.0.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:0815a09b32384e8ff00a5939ec9cd10efce8742347e019c2daca1a32f5ac2aae"}, + {file = "coverage-7.0.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:a78a80d131c067d67d8a6f9bd3d3f7ea7eac82c1c7259f97d7ab73f723da9d55"}, + {file = "coverage-7.0.4-cp37-cp37m-win32.whl", hash = "sha256:2b5936b624fbe711ed02dfd86edd678822e5ee68da02b6d231e5c01090b64590"}, + {file = "coverage-7.0.4-cp37-cp37m-win_amd64.whl", hash = "sha256:a63922765ee49d5b4c32afb2cd5516812c8665f3b78e64a0dd005bdfabf991b1"}, + {file = "coverage-7.0.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d68f2f7bddb3acdd3b36ef7f334b9d14f30b93e094f808fbbd8d288b8f9e2f9b"}, + {file = "coverage-7.0.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9dafdba3b2b9010abab08cb8c0dc6549bfca6e1630fe14d47b01dca00d39e694"}, + {file = "coverage-7.0.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0322354757b47640535daabd2d56384ff3cad2896248fc84d328c5fad4922d5c"}, + {file = "coverage-7.0.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4e8267466662aff93d66fa72b9591d02122dfc8a729b0a43dd70e0fb07ed9b37"}, + {file = "coverage-7.0.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f684d88eb4924ed0630cf488fd5606e334c6835594bb5fe36b50a509b10383ed"}, + {file = "coverage-7.0.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:70c294bb15ba576fb96b580db35895bf03749d683df044212b74e938a7f6821f"}, + {file = "coverage-7.0.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:34c0457e1ba450ae8b22dc8ea2fd36ada1010af61291e4c96963cd9d9633366f"}, + {file = "coverage-7.0.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b75aff2c35ceaa299691e772f7bf7c8aeab25f46acea2be3dd04cccb914a9860"}, + {file = "coverage-7.0.4-cp38-cp38-win32.whl", hash = "sha256:6c5554d55668381e131577f20e8f620d4882b04ad558f7e7f3f1f55b3124c379"}, + {file = "coverage-7.0.4-cp38-cp38-win_amd64.whl", hash = "sha256:c82f34fafaf5bc05d222fcf84423d6e156432ca35ca78672d4affd0c09c6ef6c"}, + {file = "coverage-7.0.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b8dfb5fed540f77e814bf4ec79619c241af6b4578fa1093c5e3389bbb7beab3f"}, + {file = "coverage-7.0.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ee32a080bab779b71c4d09a3eb5254bfca43ee88828a683dab27dfe8f582516e"}, + {file = "coverage-7.0.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2dfbee0bf0d633be3a2ab068f5a5731a70adf147d0ba17d9f9932b46c7c5782b"}, + {file = "coverage-7.0.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:32dc010713455ac0fe2fddb0e48aa43875cc7eb7b09768df10bad8ce45f9c430"}, + {file = "coverage-7.0.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9cb88a3019ad042eaa69fc7639ef077793fedbf313e89207aa82fefe92c97ebd"}, + {file = "coverage-7.0.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:73bc6114aab7753ca784f87bcd3b7613bc797aa255b5bca45e5654070ae9acfb"}, + {file = "coverage-7.0.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:92f135d370fcd7a6fb9659fa2eb716dd2ca364719cbb1756f74d90a221bca1a7"}, + {file = "coverage-7.0.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:f3d485e6ec6e09857bf2115ece572d666b7c498377d4c70e66bb06c63ed177c2"}, + {file = "coverage-7.0.4-cp39-cp39-win32.whl", hash = "sha256:c58921fcd9914b56444292e7546fe183d079db99528142c809549ddeaeacd8e9"}, + {file = "coverage-7.0.4-cp39-cp39-win_amd64.whl", hash = "sha256:f092d9f2ddaa30235d33335fbdb61eb8f3657af519ef5f9dd6bdae65272def11"}, + {file = "coverage-7.0.4-pp37.pp38.pp39-none-any.whl", hash = "sha256:cb8cfa3bf3a9f18211279458917fef5edeb5e1fdebe2ea8b11969ec2ebe48884"}, + {file = "coverage-7.0.4.tar.gz", hash = "sha256:f6c4ad409a0caf7e2e12e203348b1a9b19c514e7d078520973147bf2d3dcbc6f"}, ] exceptiongroup = [ {file = "exceptiongroup-1.1.0-py3-none-any.whl", hash = "sha256:327cbda3da756e2de031a3107b81ab7b3770a602c4d16ca618298c526f4bec1e"}, @@ -393,12 +409,12 @@ importlib-metadata = [ {file = "importlib_metadata-6.0.0.tar.gz", hash = "sha256:e354bedeb60efa6affdcc8ae121b73544a7aa74156d047311948f6d711cd378d"}, ] iniconfig = [ - {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"}, ] packaging = [ - {file = "packaging-22.0-py3-none-any.whl", hash = "sha256:957e2148ba0e1a3b282772e791ef1d8083648bc131c8ab0c1feba110ce1146c3"}, - {file = "packaging-22.0.tar.gz", hash = "sha256:2198ec20bd4c017b8f9717e00f0c8714076fc2fd93816750ab48e2c41de2cfd3"}, + {file = "packaging-23.0-py3-none-any.whl", hash = "sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2"}, + {file = "packaging-23.0.tar.gz", hash = "sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97"}, ] pluggy = [ {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, @@ -408,6 +424,10 @@ pytest = [ {file = "pytest-7.2.0-py3-none-any.whl", hash = "sha256:892f933d339f068883b6fd5a459f03d85bfcb355e4981e146d2c7616c21fef71"}, {file = "pytest-7.2.0.tar.gz", hash = "sha256:c4014eb40e10f11f355ad4e3c2fb2c6c6d1919c73f3b5a433de4708202cade59"}, ] +pytest-asyncio = [ + {file = "pytest-asyncio-0.20.3.tar.gz", hash = "sha256:83cbf01169ce3e8eb71c6c278ccb0574d1a7a3bb8eaaf5e50e0ad342afb33b36"}, + {file = "pytest_asyncio-0.20.3-py3-none-any.whl", hash = "sha256:f129998b209d04fcc65c96fc85c11e5316738358909a8399e93be553d7656442"}, +] pytest-cov = [ {file = "pytest-cov-4.0.0.tar.gz", hash = "sha256:996b79efde6433cdbd0088872dbc5fb3ed7fe1578b68cdbba634f14bb8dd0470"}, {file = "pytest_cov-4.0.0-py3-none-any.whl", hash = "sha256:2feb1b751d66a8bd934e5edfa2e961d11309dc37b73b0eabe73b5945fee20f6b"}, diff --git a/pyproject.toml b/pyproject.toml index 26b5027..4b9d1c6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,8 +16,9 @@ python = "^3.7" 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" diff --git a/src/onepasswordconnectsdk/client.py b/src/onepasswordconnectsdk/client.py index 3bd75ae..5bf0263 100644 --- a/src/onepasswordconnectsdk/client.py +++ b/src/onepasswordconnectsdk/client.py @@ -441,12 +441,13 @@ async def get_item(self, item: str, vault: str): vault_id = vault if not is_valid_uuid(vault): - vault_id = self.get_vault_by_title(vault).id + vault = await self.get_vault_by_title(vault) + vault_id = vault.id if is_valid_uuid(item): - return self.get_item_by_id(item, vault_id) + return await self.get_item_by_id(item, vault_id) else: - return self.get_item_by_title(item, vault_id) + 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 diff --git a/src/tests/test_client_items.py b/src/tests/test_client_items.py index ec74a9a..1f5f74d 100644 --- a/src/tests/test_client_items.py +++ b/src/tests/test_client_items.py @@ -1,3 +1,4 @@ +import pytest from httpx import Response from onepasswordconnectsdk import client, models @@ -8,6 +9,7 @@ HOST = "https://mock_host" TOKEN = "jwt_token" SS_CLIENT = client.new_client(HOST, TOKEN) +SS_CLIENT_ASYNC = client.new_client(HOST, TOKEN, True) def test_get_item_by_id(respx_mock): @@ -21,6 +23,18 @@ def test_get_item_by_id(respx_mock): assert mock.called +@pytest.mark.asyncio +async def test_get_item_by_id_async(respx_mock): + expected_item = get_item() + expected_path = f"/v1/vaults/{VAULT_ID}/items/{ITEM_ID}" + + mock = respx_mock.get(expected_path).mock(return_value=Response(200, json=expected_item)) + + item = await SS_CLIENT_ASYNC.get_item_by_id(ITEM_ID, VAULT_ID) + compare_items(expected_item, item) + assert mock.called + + 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}\"" @@ -35,6 +49,21 @@ def test_get_item_by_title(respx_mock): 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 + + def test_get_item_by_item_id_vault_id(respx_mock): expected_item = get_item() expected_path = f"/v1/vaults/{VAULT_ID}/items/{ITEM_ID}" @@ -46,6 +75,18 @@ def test_get_item_by_item_id_vault_id(respx_mock): assert mock.called +@pytest.mark.asyncio +async def test_get_item_by_item_id_vault_id_async(respx_mock): + expected_item = get_item() + expected_path = f"/v1/vaults/{VAULT_ID}/items/{ITEM_ID}" + + mock = respx_mock.get(expected_path).mock(return_value=Response(200, json=expected_item)) + + item = await SS_CLIENT_ASYNC.get_item(ITEM_ID, VAULT_ID) + compare_items(expected_item, item) + assert mock.called + + 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}\"" @@ -61,6 +102,22 @@ def test_get_item_by_item_id_vault_title(respx_mock): assert item_mock.called +@pytest.mark.asyncio +async def test_get_item_by_item_id_vault_title_async(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}" + + 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 = 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 + + 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}\"" @@ -76,6 +133,22 @@ def test_get_item_by_item_title_vault_id(respx_mock): assert item_mock.called +@pytest.mark.asyncio +async def test_get_item_by_item_title_vault_id_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_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_ID) + compare_items(expected_item, item) + assert items_by_title_mock.called + assert item_mock.called + + 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}\"" @@ -95,6 +168,26 @@ def test_get_item_by_item_title_vault_title(respx_mock): assert item_mock.called +@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"/v1/vaults/{VAULT_ID}/items" @@ -107,6 +200,19 @@ def test_get_items(respx_mock): 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 + + def test_delete_item(respx_mock): expected_items = get_items() expected_path = f"/v1/vaults/{VAULT_ID}/items/{ITEM_ID}" @@ -117,6 +223,18 @@ def test_delete_item(respx_mock): 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 + + + 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())) @@ -126,6 +244,16 @@ def test_create_item(respx_mock): 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 = await SS_CLIENT_ASYNC.create_item(item.vault.id, item) + assert mock.called + compare_full_items(item, created_item) + + 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())) @@ -135,6 +263,16 @@ def test_update_item(respx_mock): 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 = await SS_CLIENT_ASYNC.update_item(item.id, item.vault.id, item) + assert mock.called + compare_full_items(item, updated_item) + + def compare_full_items(expected_item, returned_item): assert expected_item.id == returned_item.id assert expected_item.title == returned_item.title diff --git a/src/tests/test_client_vaults.py b/src/tests/test_client_vaults.py index 892f4db..e896e3b 100644 --- a/src/tests/test_client_vaults.py +++ b/src/tests/test_client_vaults.py @@ -1,3 +1,4 @@ +import pytest from httpx import Response from onepasswordconnectsdk import client @@ -6,6 +7,7 @@ HOST = "https://mock_host" TOKEN = "jwt_token" SS_CLIENT = client.new_client(HOST, TOKEN) +SS_CLIENT_ASYNC = client.new_client(HOST, TOKEN, True) def test_get_vaults(respx_mock): @@ -19,9 +21,21 @@ def test_get_vaults(respx_mock): assert mock.called +@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 = respx_mock.get(expected_path).mock(return_value=Response(200, json=expected_vault)) @@ -30,6 +44,18 @@ def test_get_vault(respx_mock): 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 + + def test_get_vault_by_title(respx_mock): expected_vaults = list_vaults() expected_path = f"/v1/vaults?filter=name eq \"{VAULT_NAME}\"" @@ -41,6 +67,18 @@ def test_get_vault_by_title(respx_mock): assert mock.called +@pytest.mark.asyncio +async def test_get_vault_by_title(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(): return [ get_vault() From 68c04dab6e55abb57c5b781499b6f599aad35f89 Mon Sep 17 00:00:00 2001 From: volodymyrZotov Date: Fri, 27 Jan 2023 09:30:29 +0100 Subject: [PATCH 06/22] Update README with async/await feature Signed-off-by: volodymyrZotov --- README.md | 34 ++++++++++++++++++++++++----- src/onepasswordconnectsdk/client.py | 1 + 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 4d2f888..27eba6d 100644 --- a/README.md +++ b/README.md @@ -40,13 +40,16 @@ import onepasswordconnectsdk - `http://localhost:8080` if the Connect server is running in Docker on the same host. - `http(s)://:8080` or `http(s)://: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 NOT async client (this is used by default) **Create a Client** There are two methods available for creating a client: -- `new_client_from_environment`: Builds a new client for interacting with 1Password Connect using the `OP_CONNECT_TOKEN` and `OP_CONNECT_HOST` *environment variables*. -- `new_client`: Builds a new client for interacting with 1Password Connect. Accepts the hostname of 1Password Connect and the API token generated for the application. +- `new_client_from_environment`: Builds a new client for interacting with 1Password Connect using the `OP_CONNECT_TOKEN`, `OP_CONNECT_HOST` and `OP_CONNECT_CLIENT_ASYNC` *environment variables*. +- `new_client`: Builds a new client for interacting with 1Password Connect. Accepts the hostname of 1Password Connect, the API token generated for the application, is_async flag to initialize async client. ```python from onepasswordconnectsdk.client import ( @@ -55,13 +58,14 @@ from onepasswordconnectsdk.client import ( new_client ) -# creating client using OP_CONNECT_TOKEN and OP_CONNECT_HOST environment variables +# creating client using OP_CONNECT_TOKEN, OP_CONNECT_HOST and OP_CONNECT_CLIENT_ASYNC environment variables client_from_env: Client = new_client_from_environment() -# creates a client by supplying hostname and 1Password Connect API token +# creates a client by supplying hostname, 1Password Connect API token, is_async flag client_from_token: Client = new_client( "{1Password_Connect_Host}", - "{1Password_Connect_API_Token}") + "{1Password_Connect_API_Token}", + True) ``` **Get Item** @@ -173,6 +177,26 @@ Returns the contents of a given file. client.download_file("{file_id}", "{item_id}", "{vault_id}", "{content_path}") ``` +**Async way** + +All the examples above can work in async way. Here is an example: +```python +import asyncio + +# initialize async client by passing is_async = True +client: Client = new_client( + "{1Password_Connect_Host}", + "{1Password_Connect_API_Token}", + True) + +async def main(): + vaults = await client.get_vaults() + item = await client.get_item("{item_id}", "{vault_id}") + # do something with vaults and item + +asyncio.run(main()) +``` + **Load Configuration** Users can create `classes` or `dicts` that describe fields they wish to get the values from in 1Password. Two convienience methods are provided that will handle the fetching of values for these fields: diff --git a/src/onepasswordconnectsdk/client.py b/src/onepasswordconnectsdk/client.py index 5bf0263..7c5ef2f 100644 --- a/src/onepasswordconnectsdk/client.py +++ b/src/onepasswordconnectsdk/client.py @@ -729,6 +729,7 @@ def new_client(url: str, token: str, is_async: bool = False): Parameters: url: The url of the 1Password Connect API token: The 1Password Service Account token + is_async: Initialize async or regular client Returns: Client: The 1Password Connect client From d20201d6561d21ae6ec514bbabccb6118cd564d3 Mon Sep 17 00:00:00 2001 From: volodymyrZotov Date: Fri, 27 Jan 2023 09:50:10 +0100 Subject: [PATCH 07/22] Update README Signed-off-by: volodymyrZotov --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 27eba6d..8f2cebd 100644 --- a/README.md +++ b/README.md @@ -193,6 +193,7 @@ async def main(): vaults = await client.get_vaults() item = await 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()) ``` From d44f496be708a7167e8e8f76fb649d8975dd3bae Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Tue, 15 Aug 2023 10:53:46 -0500 Subject: [PATCH 08/22] Update README.md --- README.md | 46 +++++++++++++++++++++++----------------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 8f2cebd..d8d5c72 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ import onepasswordconnectsdk - **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 NOT async client (this is used by default) + - False - to use synchronous client (this is used by default) **Create a Client** @@ -61,7 +61,7 @@ from onepasswordconnectsdk.client import ( # creating client using OP_CONNECT_TOKEN, OP_CONNECT_HOST and OP_CONNECT_CLIENT_ASYNC environment variables client_from_env: Client = new_client_from_environment() -# creates a client by supplying hostname, 1Password Connect API token, is_async flag +# creates a client by supplying hostname, 1Password Connect API token and `is_async` flag client_from_token: Client = new_client( "{1Password_Connect_Host}", "{1Password_Connect_API_Token}", @@ -177,27 +177,6 @@ Returns the contents of a given file. client.download_file("{file_id}", "{item_id}", "{vault_id}", "{content_path}") ``` -**Async way** - -All the examples above can work in async way. Here is an example: -```python -import asyncio - -# initialize async client by passing is_async = True -client: Client = new_client( - "{1Password_Connect_Host}", - "{1Password_Connect_API_Token}", - True) - -async def main(): - vaults = await client.get_vaults() - item = await 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()) -``` - **Load Configuration** Users can create `classes` or `dicts` that describe fields they wish to get the values from in 1Password. Two convienience methods are provided that will handle the fetching of values for these fields: @@ -243,6 +222,27 @@ CONFIG = Config() values_object = onepasswordconnectsdk.load(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()) +``` + ## Development **Testing** From 39e0636ca8b66d85bc3efed98e7da06c468789f1 Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Tue, 15 Aug 2023 11:10:19 -0500 Subject: [PATCH 09/22] Address PR comments --- src/onepasswordconnectsdk/errors.py | 2 +- src/tests/test_client_vaults.py | 2 +- src/tests/test_config.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/onepasswordconnectsdk/errors.py b/src/onepasswordconnectsdk/errors.py index 1a24bb4..add5db0 100644 --- a/src/onepasswordconnectsdk/errors.py +++ b/src/onepasswordconnectsdk/errors.py @@ -19,4 +19,4 @@ class FailedToRetrieveVaultException(OnePasswordConnectSDKError): class FailedToDeserializeException(OnePasswordConnectSDKError, TypeError): - pass \ No newline at end of file + pass diff --git a/src/tests/test_client_vaults.py b/src/tests/test_client_vaults.py index e896e3b..9a3eab3 100644 --- a/src/tests/test_client_vaults.py +++ b/src/tests/test_client_vaults.py @@ -68,7 +68,7 @@ def test_get_vault_by_title(respx_mock): @pytest.mark.asyncio -async def test_get_vault_by_title(respx_mock): +async def test_get_vault_by_title_async(respx_mock): expected_vaults = list_vaults() expected_path = f"/v1/vaults?filter=name eq \"{VAULT_NAME}\"" diff --git a/src/tests/test_config.py b/src/tests/test_config.py index 64a0b61..551cbcf 100644 --- a/src/tests/test_config.py +++ b/src/tests/test_config.py @@ -96,7 +96,7 @@ def test_load_dict(respx_mock): } }, { - "id": "716C5B0E95A84092B2FE2CC402E0DDDF", + "id": "username", "label": "username", "value": USERNAME_VALUE } From 93edd505fee9240aadaab1856827bea2f739f0e3 Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Tue, 15 Aug 2023 11:35:14 -0500 Subject: [PATCH 10/22] Update poetry.lock --- poetry.lock | 372 +++++++++++++++++++++++++--------------------------- 1 file changed, 181 insertions(+), 191 deletions(-) diff --git a/poetry.lock b/poetry.lock index af1e32f..6ccac8c 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,43 +1,39 @@ +# This file is automatically @generated by Poetry 1.4.2 and should not be changed by hand. + [[package]] name = "anyio" -version = "3.6.2" +version = "3.7.1" description = "High level compatibility layer for multiple asynchronous event loop implementations" category = "main" optional = false -python-versions = ">=3.6.2" +python-versions = ">=3.7" +files = [ + {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] -doc = ["packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] -test = ["contextlib2", "coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "mock (>=4)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (<0.15)", "uvloop (>=0.15)"] -trio = ["trio (>=0.16,<0.22)"] - -[[package]] -name = "attrs" -version = "22.2.0" -description = "Classes Without Boilerplate" -category = "dev" -optional = false -python-versions = ">=3.6" - -[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 = "2022.12.7" +version = "2023.7.22" description = "Python package for providing Mozilla's CA Bundle." category = "main" optional = false python-versions = ">=3.6" +files = [ + {file = "certifi-2023.7.22-py3-none-any.whl", hash = "sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9"}, + {file = "certifi-2023.7.22.tar.gz", hash = "sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082"}, +] [[package]] name = "colorama" @@ -46,14 +42,80 @@ 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 = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] [[package]] name = "coverage" -version = "7.0.4" +version = "7.2.7" description = "Code coverage measurement for Python" category = "dev" optional = false python-versions = ">=3.7" +files = [ + {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] tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} @@ -63,11 +125,15 @@ toml = ["tomli"] [[package]] name = "exceptiongroup" -version = "1.1.0" +version = "1.1.3" description = "Backport of PEP 654 (exception groups)" -category = "dev" +category = "main" optional = false python-versions = ">=3.7" +files = [ + {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)"] @@ -79,6 +145,10 @@ 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\""} @@ -90,6 +160,10 @@ 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" @@ -108,6 +182,10 @@ 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 = "*" @@ -128,14 +206,22 @@ description = "Internationalized Domain Names in Applications (IDNA)" category = "main" optional = false python-versions = ">=3.5" +files = [ + {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, + {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, +] [[package]] name = "importlib-metadata" -version = "6.0.0" +version = "6.7.0" description = "Read metadata from Python packages" category = "dev" optional = false python-versions = ">=3.7" +files = [ + {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] typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} @@ -144,7 +230,7 @@ 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" @@ -153,22 +239,34 @@ description = "brain-dead simple config-ini parsing" category = "dev" optional = false python-versions = ">=3.7" +files = [ + {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 = "23.0" +version = "23.1" description = "Core utilities for Python packages" category = "dev" optional = false python-versions = ">=3.7" +files = [ + {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.2.0-py3-none-any.whl", hash = "sha256:c2fd55a7d7a3863cba1a013e4e2414658b1d07b6bc57b3919e0c63c9abb99849"}, + {file = "pluggy-1.2.0.tar.gz", hash = "sha256:d12f0c4b579b15f5e054301bb226ee85eeeba08ffec228092f8defbaa3a4c4b3"}, +] [package.dependencies] importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} @@ -179,14 +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.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\""} @@ -196,7 +297,7 @@ 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" @@ -205,6 +306,10 @@ 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" @@ -216,11 +321,15 @@ testing = ["coverage (>=6.2)", "flaky (>=3.5.0)", "hypothesis (>=5.7.1)", "mypy [[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.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6"}, + {file = "pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"}, +] [package.dependencies] coverage = {version = ">=5.2.1", extras = ["toml"]} @@ -236,17 +345,25 @@ description = "Extensions to the standard Python datetime module" category = "main" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +files = [ + {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, + {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, +] [package.dependencies] six = ">=1.5" [[package]] name = "respx" -version = "0.20.1" +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 = "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" @@ -258,6 +375,10 @@ 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] idna = {version = "*", optional = true, markers = "extra == \"idna2008\""} @@ -272,6 +393,10 @@ description = "Python 2 and 3 compatibility utilities" category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +files = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] [[package]] name = "sniffio" @@ -280,6 +405,10 @@ 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" @@ -288,179 +417,40 @@ description = "A lil' TOML parser" category = "dev" optional = false python-versions = ">=3.7" +files = [ + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, +] [[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.7.1-py3-none-any.whl", hash = "sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36"}, + {file = "typing_extensions-4.7.1.tar.gz", hash = "sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2"}, +] [[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.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 = "1.1" +lock-version = "2.0" python-versions = "^3.7" content-hash = "5c06df5db167647617c8fb7afc9da451dcec9d10e24df37e6024124cccd6da60" - -[metadata.files] -anyio = [ - {file = "anyio-3.6.2-py3-none-any.whl", hash = "sha256:fbbe32bd270d2a2ef3ed1c5d45041250284e31fc0a4df4a5a6071842051a51e3"}, - {file = "anyio-3.6.2.tar.gz", hash = "sha256:25ea0d673ae30af41a0c442f81cf3b38c7e79fdc7b60335a4c14e05eb0947421"}, -] -attrs = [ - {file = "attrs-22.2.0-py3-none-any.whl", hash = "sha256:29e95c7f6778868dbd49170f98f8818f78f3dc5e0e37c0b1f474e3561b240836"}, - {file = "attrs-22.2.0.tar.gz", hash = "sha256:c9227bfc2f01993c03f68db37d1d15c9690188323c067c641f1a35ca58185f99"}, -] -certifi = [ - {file = "certifi-2022.12.7-py3-none-any.whl", hash = "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18"}, - {file = "certifi-2022.12.7.tar.gz", hash = "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3"}, -] -colorama = [ - {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, - {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, -] -coverage = [ - {file = "coverage-7.0.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:daf91db39324e9939a9db919ee4fb42a1a23634a056616dae891a030e89f87ba"}, - {file = "coverage-7.0.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:55121fe140d7e42cb970999b93cf1c2b24484ce028b32bbd00238bb25c13e34a"}, - {file = "coverage-7.0.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c027fbb83a8c78a6e06a0302ea1799fdb70e5cda9845a5e000545b8e2b47ea39"}, - {file = "coverage-7.0.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:caf82db5b7f16b51ec32fe0bd2da0805b177c807aa8bfb478c7e6f893418c284"}, - {file = "coverage-7.0.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ba5cc54baf3c322c4388de2a43cc95f7809366f0600e743e5aae8ea9d1038b2"}, - {file = "coverage-7.0.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:260854160083f8275a9d9d49a05ab0ffc7a1f08f2ccccbfaec94a18aae9f407c"}, - {file = "coverage-7.0.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:ea45f0dba5a993e93b158f1a9dcfff2770e3bcabf2b80dbe7aa15dce0bcb3bf3"}, - {file = "coverage-7.0.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6abc91f6f8b3cc0ae1034e2c03f38769fba1952ab70d0b26953aa01691265c39"}, - {file = "coverage-7.0.4-cp310-cp310-win32.whl", hash = "sha256:053cdc47cae08257051d7e934a0de4d095b60eb8a3024fa9f1b2322fa1547137"}, - {file = "coverage-7.0.4-cp310-cp310-win_amd64.whl", hash = "sha256:1e9e94f2612ee549a4b3ee79cbc61bceed77e69cf38cfa05858bae939a886d16"}, - {file = "coverage-7.0.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5caa9dd91dcc5f054350dc57a02e053d79633907b9ccffff999568d13dcd19f8"}, - {file = "coverage-7.0.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:efc200fa75d9634525b40babc7a16342bd21c101db1a58ef84dc14f4bf6ac0fd"}, - {file = "coverage-7.0.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1791e5f74c5b52f76e83fe9f4bb9571cf76d40ee0c51952ee1e4ee935b7e98b9"}, - {file = "coverage-7.0.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3d9201cfa5a98652b9cef36ab202f17fe3ea83f497b4ba2a8ed39399dfb8fcd4"}, - {file = "coverage-7.0.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22d8ef6865cb6834cab2b72fff20747a55c714b57b675f7e11c9624fe4f7cb45"}, - {file = "coverage-7.0.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b84076e3de192fba0f95e279ac017b64c7c6ecd4f09f36f13420f5bed898a9c7"}, - {file = "coverage-7.0.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:dcfbf8ffc046f20d75fd775a92c378f6fc7b9bded6c6f2ab88b6b9cb5805a184"}, - {file = "coverage-7.0.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:4665a714af31f160403c2e448fb2fef330719d2e04e836b08d60d612707c1041"}, - {file = "coverage-7.0.4-cp311-cp311-win32.whl", hash = "sha256:2e59aef3fba5758059208c9eff10ae7ded3629e797972746ec33b56844f69411"}, - {file = "coverage-7.0.4-cp311-cp311-win_amd64.whl", hash = "sha256:2b854f7985b48122b6fe346631e86d67b63293f8255cb59a93d79e3d9f1574e3"}, - {file = "coverage-7.0.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e44b60b0b49aa85d548d392a2dca2c6a581cd4084e72e9e16bd58bd86ec20816"}, - {file = "coverage-7.0.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2904d7a0388911c61e7e3beefe48c29dfccaba938fc1158f63190101a21e04c2"}, - {file = "coverage-7.0.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bc74b64bfa89e2f862ea45dd6ac1def371d7cc883b76680d20bdd61a6f3daa20"}, - {file = "coverage-7.0.4-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c06046f54e719da21c79f98ecc0962581d1aee0b3798dc6b12b1217da8bf93f4"}, - {file = "coverage-7.0.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:bc9c77004970a364a1e5454cf7cb884e4277592b959c287689b2a0fd027ef552"}, - {file = "coverage-7.0.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:0815a09b32384e8ff00a5939ec9cd10efce8742347e019c2daca1a32f5ac2aae"}, - {file = "coverage-7.0.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:a78a80d131c067d67d8a6f9bd3d3f7ea7eac82c1c7259f97d7ab73f723da9d55"}, - {file = "coverage-7.0.4-cp37-cp37m-win32.whl", hash = "sha256:2b5936b624fbe711ed02dfd86edd678822e5ee68da02b6d231e5c01090b64590"}, - {file = "coverage-7.0.4-cp37-cp37m-win_amd64.whl", hash = "sha256:a63922765ee49d5b4c32afb2cd5516812c8665f3b78e64a0dd005bdfabf991b1"}, - {file = "coverage-7.0.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d68f2f7bddb3acdd3b36ef7f334b9d14f30b93e094f808fbbd8d288b8f9e2f9b"}, - {file = "coverage-7.0.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9dafdba3b2b9010abab08cb8c0dc6549bfca6e1630fe14d47b01dca00d39e694"}, - {file = "coverage-7.0.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0322354757b47640535daabd2d56384ff3cad2896248fc84d328c5fad4922d5c"}, - {file = "coverage-7.0.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4e8267466662aff93d66fa72b9591d02122dfc8a729b0a43dd70e0fb07ed9b37"}, - {file = "coverage-7.0.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f684d88eb4924ed0630cf488fd5606e334c6835594bb5fe36b50a509b10383ed"}, - {file = "coverage-7.0.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:70c294bb15ba576fb96b580db35895bf03749d683df044212b74e938a7f6821f"}, - {file = "coverage-7.0.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:34c0457e1ba450ae8b22dc8ea2fd36ada1010af61291e4c96963cd9d9633366f"}, - {file = "coverage-7.0.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b75aff2c35ceaa299691e772f7bf7c8aeab25f46acea2be3dd04cccb914a9860"}, - {file = "coverage-7.0.4-cp38-cp38-win32.whl", hash = "sha256:6c5554d55668381e131577f20e8f620d4882b04ad558f7e7f3f1f55b3124c379"}, - {file = "coverage-7.0.4-cp38-cp38-win_amd64.whl", hash = "sha256:c82f34fafaf5bc05d222fcf84423d6e156432ca35ca78672d4affd0c09c6ef6c"}, - {file = "coverage-7.0.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b8dfb5fed540f77e814bf4ec79619c241af6b4578fa1093c5e3389bbb7beab3f"}, - {file = "coverage-7.0.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ee32a080bab779b71c4d09a3eb5254bfca43ee88828a683dab27dfe8f582516e"}, - {file = "coverage-7.0.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2dfbee0bf0d633be3a2ab068f5a5731a70adf147d0ba17d9f9932b46c7c5782b"}, - {file = "coverage-7.0.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:32dc010713455ac0fe2fddb0e48aa43875cc7eb7b09768df10bad8ce45f9c430"}, - {file = "coverage-7.0.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9cb88a3019ad042eaa69fc7639ef077793fedbf313e89207aa82fefe92c97ebd"}, - {file = "coverage-7.0.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:73bc6114aab7753ca784f87bcd3b7613bc797aa255b5bca45e5654070ae9acfb"}, - {file = "coverage-7.0.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:92f135d370fcd7a6fb9659fa2eb716dd2ca364719cbb1756f74d90a221bca1a7"}, - {file = "coverage-7.0.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:f3d485e6ec6e09857bf2115ece572d666b7c498377d4c70e66bb06c63ed177c2"}, - {file = "coverage-7.0.4-cp39-cp39-win32.whl", hash = "sha256:c58921fcd9914b56444292e7546fe183d079db99528142c809549ddeaeacd8e9"}, - {file = "coverage-7.0.4-cp39-cp39-win_amd64.whl", hash = "sha256:f092d9f2ddaa30235d33335fbdb61eb8f3657af519ef5f9dd6bdae65272def11"}, - {file = "coverage-7.0.4-pp37.pp38.pp39-none-any.whl", hash = "sha256:cb8cfa3bf3a9f18211279458917fef5edeb5e1fdebe2ea8b11969ec2ebe48884"}, - {file = "coverage-7.0.4.tar.gz", hash = "sha256:f6c4ad409a0caf7e2e12e203348b1a9b19c514e7d078520973147bf2d3dcbc6f"}, -] -exceptiongroup = [ - {file = "exceptiongroup-1.1.0-py3-none-any.whl", hash = "sha256:327cbda3da756e2de031a3107b81ab7b3770a602c4d16ca618298c526f4bec1e"}, - {file = "exceptiongroup-1.1.0.tar.gz", hash = "sha256:bcb67d800a4497e1b404c2dd44fca47d3b7a5e5433dbab67f96c1a685cdfdf23"}, -] -h11 = [ - {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, - {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, -] -httpcore = [ - {file = "httpcore-0.16.3-py3-none-any.whl", hash = "sha256:da1fb708784a938aa084bde4feb8317056c55037247c787bd7e19eb2c2949dc0"}, - {file = "httpcore-0.16.3.tar.gz", hash = "sha256:c5d6f04e2fc530f39e0c077e6a30caa53f1451096120f1f38b954afd0b17c0cb"}, -] -httpx = [ - {file = "httpx-0.23.3-py3-none-any.whl", hash = "sha256:a211fcce9b1254ea24f0cd6af9869b3d29aba40154e947d2a07bb499b3e310d6"}, - {file = "httpx-0.23.3.tar.gz", hash = "sha256:9818458eb565bb54898ccb9b8b251a28785dd4a55afbc23d0eb410754fe7d0f9"}, -] -idna = [ - {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, - {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, -] -importlib-metadata = [ - {file = "importlib_metadata-6.0.0-py3-none-any.whl", hash = "sha256:7efb448ec9a5e313a57655d35aa54cd3e01b7e1fbcf72dce1bf06119420f5bad"}, - {file = "importlib_metadata-6.0.0.tar.gz", hash = "sha256:e354bedeb60efa6affdcc8ae121b73544a7aa74156d047311948f6d711cd378d"}, -] -iniconfig = [ - {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, - {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, -] -packaging = [ - {file = "packaging-23.0-py3-none-any.whl", hash = "sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2"}, - {file = "packaging-23.0.tar.gz", hash = "sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97"}, -] -pluggy = [ - {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, - {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, -] -pytest = [ - {file = "pytest-7.2.0-py3-none-any.whl", hash = "sha256:892f933d339f068883b6fd5a459f03d85bfcb355e4981e146d2c7616c21fef71"}, - {file = "pytest-7.2.0.tar.gz", hash = "sha256:c4014eb40e10f11f355ad4e3c2fb2c6c6d1919c73f3b5a433de4708202cade59"}, -] -pytest-asyncio = [ - {file = "pytest-asyncio-0.20.3.tar.gz", hash = "sha256:83cbf01169ce3e8eb71c6c278ccb0574d1a7a3bb8eaaf5e50e0ad342afb33b36"}, - {file = "pytest_asyncio-0.20.3-py3-none-any.whl", hash = "sha256:f129998b209d04fcc65c96fc85c11e5316738358909a8399e93be553d7656442"}, -] -pytest-cov = [ - {file = "pytest-cov-4.0.0.tar.gz", hash = "sha256:996b79efde6433cdbd0088872dbc5fb3ed7fe1578b68cdbba634f14bb8dd0470"}, - {file = "pytest_cov-4.0.0-py3-none-any.whl", hash = "sha256:2feb1b751d66a8bd934e5edfa2e961d11309dc37b73b0eabe73b5945fee20f6b"}, -] -python-dateutil = [ - {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, - {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, -] -respx = [ - {file = "respx-0.20.1-py2.py3-none-any.whl", hash = "sha256:372f06991c03d1f7f480a420a2199d01f1815b6ed5a802f4e4628043a93bd03e"}, - {file = "respx-0.20.1.tar.gz", hash = "sha256:cc47a86d7010806ab65abdcf3b634c56337a737bb5c4d74c19a0dfca83b3bc73"}, -] -rfc3986 = [ - {file = "rfc3986-1.5.0-py2.py3-none-any.whl", hash = "sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97"}, - {file = "rfc3986-1.5.0.tar.gz", hash = "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835"}, -] -six = [ - {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, - {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, -] -sniffio = [ - {file = "sniffio-1.3.0-py3-none-any.whl", hash = "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"}, - {file = "sniffio-1.3.0.tar.gz", hash = "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101"}, -] -tomli = [ - {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, - {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, -] -typing-extensions = [ - {file = "typing_extensions-4.4.0-py3-none-any.whl", hash = "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e"}, - {file = "typing_extensions-4.4.0.tar.gz", hash = "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa"}, -] -zipp = [ - {file = "zipp-3.11.0-py3-none-any.whl", hash = "sha256:83a28fcb75844b5c0cdaf5aa4003c2d728c77e05f5aeabe8e95e56727005fbaa"}, - {file = "zipp-3.11.0.tar.gz", hash = "sha256:a7a22e05929290a67401440b39690ae6563279bced5f314609d9d03798f56766"}, -] From 28422ebb8fff8704025ffd6fff7c7ca47b31df88 Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Tue, 15 Aug 2023 11:39:45 -0500 Subject: [PATCH 11/22] Update USAGE.md with Async client examples --- USAGE.md | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) 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)://:8080` or `http(s)://: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 From 1b453a52e10d8f33b94159a57fce69dd822a4fd5 Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Wed, 16 Aug 2023 18:41:29 -0500 Subject: [PATCH 12/22] Gracefully close httpx client when delete AsyncClient instance --- src/onepasswordconnectsdk/client.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/onepasswordconnectsdk/client.py b/src/onepasswordconnectsdk/client.py index 28d6b85..ce6189b 100644 --- a/src/onepasswordconnectsdk/client.py +++ b/src/onepasswordconnectsdk/client.py @@ -376,6 +376,9 @@ def __init__(self, session, serializer): self.session = session self.serializer = serializer + def __del__(self): + self.session.aclose() + async 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}" response = await self.build_request("GET", url) From a9d05e25f3106fd3a7cbbb3c2759f3ff93251d57 Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Fri, 18 Aug 2023 15:38:22 -0500 Subject: [PATCH 13/22] Keep Client and AsyncClient backward compatibility --- src/onepasswordconnectsdk/async_client.py | 377 +++++++++++++++++++++ src/onepasswordconnectsdk/client.py | 389 ++-------------------- src/onepasswordconnectsdk/utils.py | 9 + 3 files changed, 406 insertions(+), 369 deletions(-) create mode 100644 src/onepasswordconnectsdk/async_client.py diff --git a/src/onepasswordconnectsdk/async_client.py b/src/onepasswordconnectsdk/async_client.py new file mode 100644 index 0000000..a7eb76d --- /dev/null +++ b/src/onepasswordconnectsdk/async_client.py @@ -0,0 +1,377 @@ +"""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 +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) + + def __del__(self): + self.session.aclose() + + async 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}" + 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 = f"/v1/vaults/{vault_id}/items/{item_id}/files" + + 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): + url = f"/v1/vaults/{vault_id}/items/{item_id}/files/{file_id}/content" + + 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 + content = await self.get_file_content(file_id, item_id, vault_id) + 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 = f"/v1/vaults/{vault_id}/items/{item_id}" + + 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 = f"/v1/vaults/{vault_id}/items?filter={filter_query}" + + 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): + """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 + """ + url = f"/v1/vaults/{vault_id}/items" + + 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 = f"/v1/vaults/{vault_id}/items/{item_id}" + + 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 = f"/v1/vaults/{vault_id}/items" + + 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 = f"/v1/vaults/{vault_id}/items/{item_uuid}" + 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 = f"/v1/vaults/{vault_id}" + 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 = f"/v1/vaults?filter={filter_query}" + + 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 = "/v1/vaults" + 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) \ No newline at end of file diff --git a/src/onepasswordconnectsdk/client.py b/src/onepasswordconnectsdk/client.py index ce6189b..c86093c 100644 --- a/src/onepasswordconnectsdk/client.py +++ b/src/onepasswordconnectsdk/client.py @@ -4,8 +4,9 @@ import json import os +from onepasswordconnectsdk.async_client import AsyncClient from onepasswordconnectsdk.serializer import Serializer -from onepasswordconnectsdk.utils import is_valid_uuid +from onepasswordconnectsdk.utils import build_headers, is_valid_uuid from onepasswordconnectsdk.errors import ( FailedToRetrieveItemException, FailedToRetrieveVaultException, @@ -22,10 +23,18 @@ class Client: """Python Client Class""" - def __init__(self, session, serializer): + def __init__(self, url: str, token: str): """Initialize client""" - self.session = session - self.serializer = serializer + 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.Client(base_url=url, headers=self.build_headers(token)) + + def build_headers(self, token: str): + return build_headers(token) def __del__(self): self.session.close() @@ -367,364 +376,11 @@ def build_request(self, method: str, path: str, body=None): response = self.session.request(method, path) return response + def deserialize(self, response, response_type): + return self.serializer.deserialize(response, response_type) -class AsyncClient: - """Python Async Client Class""" - - def __init__(self, session, serializer): - """Initialize client""" - self.session = session - self.serializer = serializer - - def __del__(self): - self.session.aclose() - - async 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}" - 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 = f"/v1/vaults/{vault_id}/items/{item_id}/files" - - 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): - url = f"/v1/vaults/{vault_id}/items/{item_id}/files/{file_id}/content" - - 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 - content = await self.get_file_content(file_id, item_id, vault_id) - 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 = f"/v1/vaults/{vault_id}/items/{item_id}" - - 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 = f"/v1/vaults/{vault_id}/items?filter={filter_query}" - - 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): - """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 - """ - url = f"/v1/vaults/{vault_id}/items" - - 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 = f"/v1/vaults/{vault_id}/items/{item_id}" - - 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 = f"/v1/vaults/{vault_id}/items" - - 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 = f"/v1/vaults/{vault_id}/items/{item_uuid}" - 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 = f"/v1/vaults/{vault_id}" - 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 = f"/v1/vaults?filter={filter_query}" - - 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 = "/v1/vaults" - 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 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"} + def sanitize_for_serialization(self, obj): + return self.serializer.sanitize_for_serialization(obj) def new_client(url: str, token: str, is_async: bool = False): @@ -732,19 +388,14 @@ def new_client(url: str, token: str, is_async: bool = False): Parameters: url: The url of the 1Password Connect API token: The 1Password Service Account token - is_async: Initialize async or regular client + is_async: Initialize async or sync client Returns: Client: The 1Password Connect client """ - headers = build_headers(token) - serializer = Serializer() if is_async: - session = httpx.AsyncClient(base_url=url, headers=headers) - return AsyncClient(session=session, serializer=serializer) - - session = httpx.Client(base_url=url, headers=headers) - return Client(session=session, serializer=serializer) + return AsyncClient(url, token) + return Client(url, token) def new_client_from_environment(url: str = None): diff --git a/src/onepasswordconnectsdk/utils.py b/src/onepasswordconnectsdk/utils.py index 4a2c8e1..7d4a279 100644 --- a/src/onepasswordconnectsdk/utils.py +++ b/src/onepasswordconnectsdk/utils.py @@ -9,3 +9,12 @@ def is_valid_uuid(uuid): 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"} From 8e99bc6c7a16935f7c1670f32678a6210614877a Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Fri, 18 Aug 2023 16:19:47 -0500 Subject: [PATCH 14/22] Add Connect.PathBuilder --- src/onepasswordconnectsdk/connect.py | 39 +++++++++++++++++++++++++++ src/tests/test_connect.py | 40 ++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+) create mode 100644 src/onepasswordconnectsdk/connect.py create mode 100644 src/tests/test_connect.py diff --git a/src/onepasswordconnectsdk/connect.py b/src/onepasswordconnectsdk/connect.py new file mode 100644 index 0000000..0d50552 --- /dev/null +++ b/src/onepasswordconnectsdk/connect.py @@ -0,0 +1,39 @@ +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}" diff --git a/src/tests/test_connect.py b/src/tests/test_connect.py new file mode 100644 index 0000000..d3624e8 --- /dev/null +++ b/src/tests/test_connect.py @@ -0,0 +1,40 @@ +from onepasswordconnectsdk.connect 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}" From f994624d5d1b5187b84b844bbf69bf8289a18f5e Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Fri, 18 Aug 2023 16:52:03 -0500 Subject: [PATCH 15/22] Use PathBuilder to construct the connect url path --- src/onepasswordconnectsdk/async_client.py | 38 ++++++++++------------- src/onepasswordconnectsdk/client.py | 37 +++++++++------------- 2 files changed, 31 insertions(+), 44 deletions(-) diff --git a/src/onepasswordconnectsdk/async_client.py b/src/onepasswordconnectsdk/async_client.py index a7eb76d..cd51eb2 100644 --- a/src/onepasswordconnectsdk/async_client.py +++ b/src/onepasswordconnectsdk/async_client.py @@ -4,6 +4,7 @@ import json import os +from onepasswordconnectsdk.connect import PathBuilder from onepasswordconnectsdk.serializer import Serializer from onepasswordconnectsdk.utils import build_headers, is_valid_uuid from onepasswordconnectsdk.errors import ( @@ -33,7 +34,7 @@ def __del__(self): self.session.aclose() async 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 = await self.build_request("GET", url) try: response.raise_for_status() @@ -45,8 +46,7 @@ async def get_file(self, file_id: str, item_id: str, vault_id: str): return self.serializer.deserialize(response.content, "File") async 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 = await self.build_request("GET", url) try: response.raise_for_status() @@ -57,9 +57,10 @@ async def get_files(self, item_id: str, vault_id: str): ) return self.serializer.deserialize(response.content, "list[File]") - async def get_file_content(self, file_id: str, item_id: str, vault_id: str): - url = f"/v1/vaults/{vault_id}/items/{item_id}/files/{file_id}/content" - + 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() @@ -73,7 +74,7 @@ async def get_file_content(self, file_id: str, item_id: str, vault_id: str): 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 - content = await self.get_file_content(file_id, item_id, vault_id) + 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") @@ -120,7 +121,6 @@ async def get_item_by_id(self, item_id: str, vault_id: str): Item object: The found item """ url = f"/v1/vaults/{vault_id}/items/{item_id}" - response = await self.build_request("GET", url) try: response.raise_for_status() @@ -146,8 +146,7 @@ async 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 = await self.build_request("GET", url) try: response.raise_for_status() @@ -179,8 +178,7 @@ async def get_items(self, vault_id: str): Returns: List[SummaryItem]: A list of summarized items """ - url = f"/v1/vaults/{vault_id}/items" - + url = PathBuilder().vaults(vault_id).items().build() response = await self.build_request("GET", url) try: response.raise_for_status() @@ -204,8 +202,7 @@ async def delete_item(self, item_id: str, vault_id: str): 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 = await self.build_request("DELETE", url) try: response.raise_for_status() @@ -230,8 +227,7 @@ async def create_item(self, vault_id: str, item: Item): Item: The created item """ - url = f"/v1/vaults/{vault_id}/items" - + url = PathBuilder().vaults(vault_id).items().build() response = await self.build_request("POST", url, item) try: response.raise_for_status() @@ -257,7 +253,7 @@ async 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) @@ -284,7 +280,7 @@ async 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 = await self.build_request("GET", url) try: response.raise_for_status() @@ -310,8 +306,7 @@ async 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 = await self.build_request("GET", url) try: response.raise_for_status() @@ -339,9 +334,8 @@ async def get_vaults(self): Returns: List[Vault]: All vaults for the service account in use """ - url = "/v1/vaults" + url = PathBuilder().vaults().build() response = await self.build_request("GET", url) - try: response.raise_for_status() except HTTPError: diff --git a/src/onepasswordconnectsdk/client.py b/src/onepasswordconnectsdk/client.py index c86093c..a8c3c59 100644 --- a/src/onepasswordconnectsdk/client.py +++ b/src/onepasswordconnectsdk/client.py @@ -5,6 +5,7 @@ import os from onepasswordconnectsdk.async_client import AsyncClient +from onepasswordconnectsdk.connect import PathBuilder from onepasswordconnectsdk.serializer import Serializer from onepasswordconnectsdk.utils import build_headers, is_valid_uuid from onepasswordconnectsdk.errors import ( @@ -40,7 +41,7 @@ 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() @@ -52,8 +53,7 @@ def get_file(self, file_id: str, item_id: str, vault_id: str): 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() @@ -65,8 +65,9 @@ def get_files(self, item_id: str, vault_id: str): 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() @@ -125,8 +126,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() @@ -152,8 +152,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() @@ -185,8 +184,7 @@ def get_items(self, vault_id: str): Returns: List[SummaryItem]: A list of summarized items """ - url = f"/v1/vaults/{vault_id}/items" - + url = PathBuilder().vaults(vault_id).items().build() response = self.build_request("GET", url) try: response.raise_for_status() @@ -204,14 +202,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() @@ -236,8 +232,7 @@ def create_item(self, vault_id: str, item: Item): Item: The created item """ - url = f"/v1/vaults/{vault_id}/items" - + url = PathBuilder().vaults(vault_id).items().build() response = self.build_request("POST", url, item) try: response.raise_for_status() @@ -263,7 +258,7 @@ 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) @@ -290,7 +285,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,8 +311,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() @@ -345,9 +339,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: From feeeb87beb6781a1b968e7bb10506d2c3ba3b142 Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Tue, 5 Sep 2023 18:15:14 -0500 Subject: [PATCH 16/22] Update filter_query comment --- src/onepasswordconnectsdk/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/onepasswordconnectsdk/client.py b/src/onepasswordconnectsdk/client.py index dc0edb3..c54113e 100644 --- a/src/onepasswordconnectsdk/client.py +++ b/src/onepasswordconnectsdk/client.py @@ -176,7 +176,7 @@ def get_items(self, vault_id: str, filter_query: str = None): 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 From a7f5c06eef1aa804e43f59853dc77c00de521b4c Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Fri, 8 Sep 2023 11:13:38 -0500 Subject: [PATCH 17/22] Move path builder class to utils.py --- src/onepasswordconnectsdk/async_client.py | 3 +- src/onepasswordconnectsdk/client.py | 3 +- src/onepasswordconnectsdk/connect.py | 39 --------------------- src/onepasswordconnectsdk/utils.py | 41 +++++++++++++++++++++++ src/tests/test_connect.py | 2 +- 5 files changed, 44 insertions(+), 44 deletions(-) diff --git a/src/onepasswordconnectsdk/async_client.py b/src/onepasswordconnectsdk/async_client.py index cd51eb2..0776276 100644 --- a/src/onepasswordconnectsdk/async_client.py +++ b/src/onepasswordconnectsdk/async_client.py @@ -4,9 +4,8 @@ import json import os -from onepasswordconnectsdk.connect import PathBuilder from onepasswordconnectsdk.serializer import Serializer -from onepasswordconnectsdk.utils import build_headers, is_valid_uuid +from onepasswordconnectsdk.utils import build_headers, is_valid_uuid, PathBuilder from onepasswordconnectsdk.errors import ( FailedToRetrieveItemException, FailedToRetrieveVaultException, diff --git a/src/onepasswordconnectsdk/client.py b/src/onepasswordconnectsdk/client.py index c54113e..4202ccf 100644 --- a/src/onepasswordconnectsdk/client.py +++ b/src/onepasswordconnectsdk/client.py @@ -5,9 +5,8 @@ import os from onepasswordconnectsdk.async_client import AsyncClient -from onepasswordconnectsdk.connect import PathBuilder from onepasswordconnectsdk.serializer import Serializer -from onepasswordconnectsdk.utils import build_headers, is_valid_uuid +from onepasswordconnectsdk.utils import build_headers, is_valid_uuid, PathBuilder from onepasswordconnectsdk.errors import ( FailedToRetrieveItemException, FailedToRetrieveVaultException, diff --git a/src/onepasswordconnectsdk/connect.py b/src/onepasswordconnectsdk/connect.py index 0d50552..e69de29 100644 --- a/src/onepasswordconnectsdk/connect.py +++ b/src/onepasswordconnectsdk/connect.py @@ -1,39 +0,0 @@ -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}" diff --git a/src/onepasswordconnectsdk/utils.py b/src/onepasswordconnectsdk/utils.py index 7d4a279..da35d50 100644 --- a/src/onepasswordconnectsdk/utils.py +++ b/src/onepasswordconnectsdk/utils.py @@ -18,3 +18,44 @@ def build_headers(token: str): 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}" diff --git a/src/tests/test_connect.py b/src/tests/test_connect.py index d3624e8..8db556b 100644 --- a/src/tests/test_connect.py +++ b/src/tests/test_connect.py @@ -1,4 +1,4 @@ -from onepasswordconnectsdk.connect import PathBuilder +from onepasswordconnectsdk.utils import PathBuilder VAULT_ID = "hfnjvi6aymbsnfc2xeeoheizda" ITEM_ID = "wepiqdxdzncjtnvmv5fegud4qy" From c79a37bdb8872680882289aef15af14355a9dfbd Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Fri, 8 Sep 2023 11:18:50 -0500 Subject: [PATCH 18/22] Add test for file content path --- src/tests/{test_connect.py => test_utils.py} | 5 +++++ 1 file changed, 5 insertions(+) rename src/tests/{test_connect.py => test_utils.py} (85%) diff --git a/src/tests/test_connect.py b/src/tests/test_utils.py similarity index 85% rename from src/tests/test_connect.py rename to src/tests/test_utils.py index 8db556b..2ed0374 100644 --- a/src/tests/test_connect.py +++ b/src/tests/test_utils.py @@ -38,3 +38,8 @@ def test_all_files_path(): 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" From a52019e0e3ee73e5337af82bae0ae4b92bff4c69 Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Fri, 8 Sep 2023 11:21:37 -0500 Subject: [PATCH 19/22] Use __aexit__ instead of __del__ to properly close async client --- src/onepasswordconnectsdk/async_client.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/onepasswordconnectsdk/async_client.py b/src/onepasswordconnectsdk/async_client.py index 0776276..4441a79 100644 --- a/src/onepasswordconnectsdk/async_client.py +++ b/src/onepasswordconnectsdk/async_client.py @@ -29,8 +29,8 @@ def create_session(self, url: str, token: str): def build_headers(self, token: str): return build_headers(token) - def __del__(self): - self.session.aclose() + 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() @@ -367,4 +367,4 @@ 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) \ No newline at end of file + return self.serializer.sanitize_for_serialization(obj) From 56a1c370efcbcf210f9f419a4a7ac22f072bfd7a Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Fri, 8 Sep 2023 11:28:43 -0500 Subject: [PATCH 20/22] Use PathBuilder in get_item_by_id method --- src/onepasswordconnectsdk/async_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/onepasswordconnectsdk/async_client.py b/src/onepasswordconnectsdk/async_client.py index 4441a79..84aff20 100644 --- a/src/onepasswordconnectsdk/async_client.py +++ b/src/onepasswordconnectsdk/async_client.py @@ -119,7 +119,7 @@ async 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 = await self.build_request("GET", url) try: response.raise_for_status() From 818d055e5f15a55a00509dce22da9dcf45ae3ae7 Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Fri, 8 Sep 2023 11:30:31 -0500 Subject: [PATCH 21/22] Set default file name as "1password_item_file.txt" in download_file method --- src/onepasswordconnectsdk/async_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/onepasswordconnectsdk/async_client.py b/src/onepasswordconnectsdk/async_client.py index 84aff20..aca7324 100644 --- a/src/onepasswordconnectsdk/async_client.py +++ b/src/onepasswordconnectsdk/async_client.py @@ -72,7 +72,7 @@ async def get_file_content(self, file_id: str, item_id: str, vault_id: str, cont 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 + 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) From 8ae9ffae375c9f287d9d6f96bc500eefad22dcb6 Mon Sep 17 00:00:00 2001 From: Volodymyr Zotov Date: Fri, 8 Sep 2023 11:32:18 -0500 Subject: [PATCH 22/22] Add ability to filter in get_items method --- src/onepasswordconnectsdk/async_client.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/onepasswordconnectsdk/async_client.py b/src/onepasswordconnectsdk/async_client.py index aca7324..415924c 100644 --- a/src/onepasswordconnectsdk/async_client.py +++ b/src/onepasswordconnectsdk/async_client.py @@ -164,7 +164,7 @@ async def get_item_by_title(self, title: str, vault_id: str): 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): + async def get_items(self, vault_id: str, filter_query: str = None): """Returns a list of item summaries for the specified vault Args: @@ -177,7 +177,10 @@ async def get_items(self, vault_id: str): Returns: List[SummaryItem]: A list of summarized items """ - url = PathBuilder().vaults(vault_id).items().build() + 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()