diff --git a/.gitignore b/.gitignore index 5e64aca7d..47a0783f7 100644 --- a/.gitignore +++ b/.gitignore @@ -139,6 +139,7 @@ celerybeat.pid # Environments .env +tests/clients/.env .venv env/ venv/ diff --git a/__init__.py b/__init__.py index f40d53e87..43c4ab005 100644 --- a/__init__.py +++ b/__init__.py @@ -1 +1 @@ -__version__='0.6.1' +__version__ = "0.6.1" diff --git a/poetry.lock b/poetry.lock index 630086384..3c72b07d1 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.7.0 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. [[package]] name = "annotated-types" @@ -531,6 +531,16 @@ files = [ {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac"}, {file = "MarkupSafe-2.1.3-cp311-cp311-win32.whl", hash = "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb"}, {file = "MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f698de3fd0c4e6972b92290a45bd9b1536bffe8c6759c62471efaa8acb4c37bc"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aa57bd9cf8ae831a362185ee444e15a93ecb2e344c8e52e4d721ea3ab6ef1823"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffcc3f7c66b5f5b7931a5aa68fc9cecc51e685ef90282f4a82f0f5e9b704ad11"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47d4f1c5f80fc62fdd7777d0d40a2e9dda0a05883ab11374334f6c4de38adffd"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1f67c7038d560d92149c060157d623c542173016c4babc0c1913cca0564b9939"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9aad3c1755095ce347e26488214ef77e0485a3c34a50c5a5e2471dff60b9dd9c"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:14ff806850827afd6b07a5f32bd917fb7f45b046ba40c57abdb636674a8b559c"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8f9293864fe09b8149f0cc42ce56e3f0e54de883a9de90cd427f191c346eb2e1"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-win32.whl", hash = "sha256:715d3562f79d540f251b99ebd6d8baa547118974341db04f5ad06d5ea3eb8007"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-win_amd64.whl", hash = "sha256:1b8dd8c3fd14349433c79fa8abeb573a55fc0fdd769133baac1f5e07abf54aeb"}, {file = "MarkupSafe-2.1.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2"}, {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b"}, {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707"}, @@ -884,6 +894,20 @@ files = [ [package.dependencies] pytest = ">=5.0.0" +[[package]] +name = "python-dotenv" +version = "1.0.1" +description = "Read key-value pairs from a .env file and set them as environment variables" +optional = false +python-versions = ">=3.8" +files = [ + {file = "python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca"}, + {file = "python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"}, +] + +[package.extras] +cli = ["click (>=5.0)"] + [[package]] name = "pyyaml" version = "6.0.1" @@ -896,6 +920,7 @@ files = [ {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, + {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, @@ -903,8 +928,16 @@ files = [ {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, + {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, + {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, + {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, + {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, @@ -921,6 +954,7 @@ files = [ {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, + {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, @@ -928,6 +962,7 @@ files = [ {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, + {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, @@ -1286,4 +1321,4 @@ test = ["pytest (>=6.0.0)", "setuptools (>=65)"] [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "8d0c5643cef5d4f3aaec6eb85446194a5c70e25ce83a6f5e5e9cea643dbd3da8" +content-hash = "563c86d7778831a1497ffc635d7f67e950da298ec7a03cd742a4df080b9e92a7" diff --git a/pyproject.toml b/pyproject.toml index 39b1cc18f..ed99de0c4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -52,6 +52,7 @@ datamodel-code-generator = "^0.23.0" isort = "^5.12" pytest = "^7.1" pytest-timeout = "^2.1" +python-dotenv = "^1.0.1" requests-mock = "^1.11" typer = "^0.9.0" diff --git a/src/pyconnectwise/clients/change_request_client.py b/src/pyconnectwise/clients/change_request_client.py new file mode 100644 index 000000000..b757a64a6 --- /dev/null +++ b/src/pyconnectwise/clients/change_request_client.py @@ -0,0 +1,217 @@ +import json +import typing +from datetime import datetime, timedelta +from typing import Any + +from pyconnectwise.clients.connectwise_client import ConnectWiseClient +from pyconnectwise.config import Config +from pyconnectwise.endpoints.automate.ContactsEndpoint import ContactsEndpoint +from pyconnectwise.endpoints.change_request import GetStatsEndpoint +from pyconnectwise.endpoints.change_request.AclsRolesEndpoint import AclsRolesEndpoint +from pyconnectwise.endpoints.change_request.ConfigurationEndpoint import ConfigurationEndpoint +from pyconnectwise.endpoints.change_request.LoginEndpoint import LoginEndpoint +from pyconnectwise.endpoints.change_request.SettingsEndpoint import SettingsEndpoint +from pyconnectwise.endpoints.change_request.TemplateEndpoint import TemplateEndpoint +from pyconnectwise.endpoints.change_request.UsersEndpoint import UsersEndpoint +from pyconnectwise.models.change_request import LoginMsg, LoginObject + +if typing.TYPE_CHECKING: + from pyconnectwise.endpoints.change_request.ChangeRequestsEndpoint import ChangeRequestsEndpoint + from pyconnectwise.endpoints.change_request.ChangeTypeEndpoint import ChangeTypeEndpoint + + +class ManageCodebaseError(Exception): + def __init__(self) -> None: + super().__init__("Could not retrieve codebase from API.") + + +class ConnectWiseChangeApprovalClient(ConnectWiseClient): + """ + ConnectWise Change Approval API client. Handles the connection to the ConnectWise Change Approval API + and the configuration of all the available endpoints. + """ + + def __init__( + self, + company_name: str, + manage_url: str, + client_id: str, + member_hash: str, + member_id: str, + login_username: str, + login_password: str, + login_role: str, + login_partner_id: str, + config: Config | None = None, + ) -> None: + """ + Initializes the client with the given credentials and optionally a specific codebase. + If no codebase is given, it tries to get it from the API. + + Parameters: + company_name (str): Name of your company. + manage_url (str): URL of your ConnectWise Change Approval instance. + client_id (str): Your ConnectWise Manage API Client ID. + public_key (str): Your ConnectWise Manage API Public key. + private_key (str): Your ConnectWise Manage API Private key. + config (Config, optional): Optional additional configuration for API interactions + """ + self.login_msg: LoginMsg | None = None + self.login_partner_id: str = login_partner_id + self.login_role: str = login_role + self.login_password: str = login_password + self.login_username: str = login_username + self.client_id: str = client_id + self.company_name: str = company_name + self.manage_url: str = manage_url + self.member_hash: str = member_hash + self.member_id: str = member_id + self.__change_approval_cookie: str = "" + self.__change_approval_cookie_expiration: datetime | None = None + + if config: + self.config = config + + # Initializing endpoints + @property + def change_request(self) -> "ChangeRequestsEndpoint": + from pyconnectwise.endpoints.change_request.ChangeRequestsEndpoint import ChangeRequestsEndpoint + + return ChangeRequestsEndpoint(self) + + @property + def change_type(self) -> "ChangeTypeEndpoint": + from pyconnectwise.endpoints.change_request.ChangeTypeEndpoint import ChangeTypeEndpoint + + return ChangeTypeEndpoint(self) + + @property + def template(self) -> "TemplateEndpoint": + from pyconnectwise.endpoints.change_request.TemplateEndpoint import TemplateEndpoint + + return TemplateEndpoint(self) + + @property + def contacts(self) -> "ContactsEndpoint": + from pyconnectwise.endpoints.change_request.ContactsEndpoint import ContactsEndpoint + + return ContactsEndpoint(self) + + @property + def acl_roles(self) -> "AclsRolesEndpoint": + from pyconnectwise.endpoints.change_request.AclsRolesEndpoint import AclsRolesEndpoint + + return AclsRolesEndpoint(self) + + @property + def configuration(self) -> "ConfigurationEndpoint": + from pyconnectwise.endpoints.change_request.ConfigurationEndpoint import ConfigurationEndpoint + + return ConfigurationEndpoint(self) + + @property + def members(self): + raise NotImplementedError("Members endpoint not implemented yet.") + + @property + def company(self): + # TODO: /company/{id}/getSites + raise NotImplementedError("Company endpoint not implemented yet.") + + @property + def audit_logs(self): + raise NotImplementedError("Audit Logs endpoint not implemented yet.") + + @property + def approval_groups(self): + raise NotImplementedError("approval groups endpoint not implemented yet.") + + @property + def audit_log(self): + # Yes, this has a different route than the plural version + raise NotImplementedError("audit log singular endpoint not implemented yet.") + + @property + def users(self) -> "UsersEndpoint": + from pyconnectwise.endpoints.change_request.UsersEndpoint import UsersEndpoint + + return UsersEndpoint(self) + + @property + def login(self) -> "LoginEndpoint": + from pyconnectwise.endpoints.change_request.LoginEndpoint import LoginEndpoint + + return LoginEndpoint(self) + + @property + def settings(self) -> "SettingsEndpoint": + from pyconnectwise.endpoints.change_request.SettingsEndpoint import SettingsEndpoint + + return SettingsEndpoint(self) + + @property + def get_stats(self) -> "GetStatsEndpoint": + from pyconnectwise.endpoints.change_request.GetStatsEndpoint import GetStatsEndpoint + + return GetStatsEndpoint(self) + + def auth_login(self) -> None: + """ + Logs in to the ConnectWise Change Approval API and retrieves the new cookie + """ + url = f"https://{self.manage_url}/api/login" + login_data = { + "userName": self.login_username, + "password": self.login_password, + "role": self.login_role, + "partnerId": self.login_partner_id, + "cwversion": "2024.1", # TODO - Parameterize? + "context": "web", + } + result = self._make_request("POST", url, data=login_data) + result.raise_for_status() + self.__change_approval_cookie = result.cookies["changeapproval"] + login_obj = LoginObject.model_validate(result.json()) + self.login_msg = login_obj.msg + # TODO - We get back expires=None, so log in every 6 hours? + self.__change_approval_cookie_expiration = datetime.now() + timedelta(hours=6) + + def _get_cookies(self) -> dict[str, str]: + """ + Generates and returns the cookies required for making API requests. + + Returns: + dict[str, str]: Dictionary of cookies. + """ + + return { + "companyName": self.company_name, + "MemberID": self.member_id, + "MemberHash": self.member_hash, + "changeapproval": self.__change_approval_cookie, + } + + def _get_url(self) -> str: + """ + Generates and returns the URL for the ConnectWise Manage API endpoints based on the company url and codebase. + + Returns: + str: API URL. + """ + return f"https://{self.manage_url}/api" + + def _get_headers(self) -> dict[str, str]: + """ + Generates and returns the headers required for making API requests. + + Returns: + dict[str, str]: Dictionary of headers including Content-Type, Client ID, and Authorization. + """ + return { + "Content-Type": "application/json", + "clientId": self.client_id, + } + + def _get_query(self, x) -> dict[str, Any]: + # Because ChangeApproval API is weird + return {"query": json.dumps(x)} diff --git a/src/pyconnectwise/clients/connectwise_client.py b/src/pyconnectwise/clients/connectwise_client.py index 2372d5c4c..1152e6e8c 100644 --- a/src/pyconnectwise/clients/connectwise_client.py +++ b/src/pyconnectwise/clients/connectwise_client.py @@ -33,10 +33,19 @@ class ConnectWiseClient(ABC): def _get_headers(self) -> dict[str, str]: pass + @abstractmethod + def _get_cookies(self) -> dict[str, str]: + return {} + @abstractmethod def _get_url(self) -> str: pass + @abstractmethod + def _get_query(self, x) -> dict[str, Any]: + # TODO - This feels like an inversion of control for the client to be asking the endpoint for the query + return x + def _make_request( # noqa: C901 self, method: RequestMethod, @@ -46,6 +55,7 @@ def _make_request( # noqa: C901 headers: dict[str, str] | None = None, retry_count: int = 0, stream: bool = False, # noqa: FBT001, FBT002 + cookies: dict[str, str] | None = None, ) -> Response: """ Make an API request using the specified method, endpoint, data, and parameters. @@ -68,24 +78,30 @@ def _make_request( # noqa: C901 if not headers: headers = self._get_headers() + if not cookies: + cookies = self._get_cookies() + params = self._get_query(params) if params else None + # I don't like having to cast the params to a dict, but it's the only way I can get mypy to stop complaining about the type. # TypedDicts aren't compatible with the dict type and this is the best way I can think of to handle this. if data: - response = requests.request( # noqa: S113 + response = requests.request( method, url, headers=headers, json=data, params=cast(dict[str, Any], params or {}), stream=stream, + cookies=cookies, ) else: - response = requests.request( # noqa: S113 + response = requests.request( method, url, headers=headers, params=cast(dict[str, Any], params or {}), stream=stream, + cookies=cookies, ) if not response.ok: with contextlib.suppress(json.JSONDecodeError): diff --git a/src/pyconnectwise/endpoints/change_request/AclsRolesEndpoint.py b/src/pyconnectwise/endpoints/change_request/AclsRolesEndpoint.py new file mode 100644 index 000000000..a9deae37f --- /dev/null +++ b/src/pyconnectwise/endpoints/change_request/AclsRolesEndpoint.py @@ -0,0 +1,34 @@ +from typing import TYPE_CHECKING + +from pyconnectwise.endpoints.base.connectwise_endpoint import ConnectWiseEndpoint +from pyconnectwise.interfaces import IGettable +from pyconnectwise.models.change_request import AclRolesData, AclRolesObject +from pyconnectwise.types import JSON, ConnectWiseChangeApprovalRequestParams + +if TYPE_CHECKING: + from pyconnectwise.clients.connectwise_client import ConnectWiseClient + + +class AclsRolesEndpoint( + ConnectWiseEndpoint, + IGettable[list[AclRolesData], ConnectWiseChangeApprovalRequestParams], +): + def __init__(self, client: "ConnectWiseClient", parent_endpoint: ConnectWiseEndpoint = None) -> None: + ConnectWiseEndpoint.__init__(self, client, "acls_roles", parent_endpoint=parent_endpoint) + IGettable.__init__(self, list[AclRolesData]) + + def get( + self, data: JSON | None = None, params: ConnectWiseChangeApprovalRequestParams | None = None + ) -> list[AclRolesData]: + """ + Performs a GET request against the /api/change_requests endpoint. + + Parameters: + data (dict[str, Any]): The data to send in the request body. + params (dict[str, int | str]): The parameters to send in the request query string. + Returns: + list[AclRolesMsg]: The parsed response data. + """ + obj = self._parse_one(AclRolesObject, super()._make_request("GET", data=data, params=params).json()) + # TODO - `total`, `current`, which is paginated? + return obj.msg.data diff --git a/src/pyconnectwise/endpoints/change_request/ChangeRequestIdEndpoint.py b/src/pyconnectwise/endpoints/change_request/ChangeRequestIdEndpoint.py new file mode 100644 index 000000000..7006a0782 --- /dev/null +++ b/src/pyconnectwise/endpoints/change_request/ChangeRequestIdEndpoint.py @@ -0,0 +1,33 @@ +from typing import TYPE_CHECKING + +from pyconnectwise.endpoints.base.connectwise_endpoint import ConnectWiseEndpoint +from pyconnectwise.interfaces import IGettable +from pyconnectwise.models.change_request import ChangeRequestMsg, ChangeRequestObject +from pyconnectwise.types import JSON, ConnectWiseChangeApprovalRequestParams + +if TYPE_CHECKING: + from pyconnectwise.clients.connectwise_client import ConnectWiseClient + + +class ChangeRequestIdEndpoint( + ConnectWiseEndpoint, + IGettable[list[ChangeRequestMsg], ConnectWiseChangeApprovalRequestParams], +): + def __init__(self, client: "ConnectWiseClient", parent_endpoint: ConnectWiseEndpoint = None) -> None: + ConnectWiseEndpoint.__init__(self, client, "{id}", parent_endpoint=parent_endpoint) + IGettable.__init__(self, list[ChangeRequestMsg]) + + def get( + self, data: JSON | None = None, params: ConnectWiseChangeApprovalRequestParams | None = None + ) -> ChangeRequestMsg: + """ + Performs a GET request against the /api/change_requests endpoint. + + Parameters: + data (dict[str, Any]): The data to send in the request body. + params (dict[str, int | str]): The parameters to send in the request query string. + Returns: + list[ChangeRequestMsg]: The parsed response data. + """ + obj = self._parse_one(ChangeRequestObject, super()._make_request("GET", data=data, params=params).json()) + return obj.msg diff --git a/src/pyconnectwise/endpoints/change_request/ChangeRequestsEndpoint.py b/src/pyconnectwise/endpoints/change_request/ChangeRequestsEndpoint.py new file mode 100644 index 000000000..67c6a7438 --- /dev/null +++ b/src/pyconnectwise/endpoints/change_request/ChangeRequestsEndpoint.py @@ -0,0 +1,63 @@ +from typing import TYPE_CHECKING + +from pyconnectwise.endpoints.base.connectwise_endpoint import ConnectWiseEndpoint +from pyconnectwise.endpoints.change_request.ChangeRequestIdEndpoint import ChangeRequestIdEndpoint +from pyconnectwise.interfaces import IGettable, IPostable +from pyconnectwise.models.change_request import ChangeRequestGetObject, ChangeRequestMsg +from pyconnectwise.types import JSON, ConnectWiseChangeApprovalRequestParams + +if TYPE_CHECKING: + from pyconnectwise.clients.connectwise_client import ConnectWiseClient + + +class ChangeRequestsEndpoint( + ConnectWiseEndpoint, + IGettable[list[ChangeRequestMsg], ConnectWiseChangeApprovalRequestParams], + IPostable[ChangeRequestMsg, ConnectWiseChangeApprovalRequestParams], +): + def __init__(self, client: "ConnectWiseClient", parent_endpoint: ConnectWiseEndpoint = None) -> None: + ConnectWiseEndpoint.__init__(self, client, "change_request", parent_endpoint=parent_endpoint) + IGettable.__init__(self, list[ChangeRequestMsg]) + IPostable.__init__(self, ChangeRequestMsg) + + def id(self, _id: int) -> ChangeRequestIdEndpoint: + """ + Sets the ID for this endpoint and returns an initialized ChangeRequestIdEndpoint object to move down the chain. + + Parameters: + _id (int): The ID to set. + Returns: + ChangeRequestIdEndpoint: The initialized ChangeRequestIdEndpoint object. + """ + child = ChangeRequestIdEndpoint(self.client, parent_endpoint=self) + child._id = _id + return child + + def get( + self, data: JSON | None = None, params: ConnectWiseChangeApprovalRequestParams | None = None + ) -> list[ChangeRequestMsg]: + """ + Performs a GET request against the /api/change_requests endpoint. + + Parameters: + data (dict[str, Any]): The data to send in the request body. + params (dict[str, int | str]): The parameters to send in the request query string. + Returns: + list[ChangeRequestMsg]: The parsed response data. + """ + obj = self._parse_one(ChangeRequestGetObject, super()._make_request("GET", data=data, params=params).json()) + return obj.msg.data + + def post( + self, data: JSON | None = None, params: ConnectWiseChangeApprovalRequestParams | None = None + ) -> ChangeRequestMsg: + """ + Performs a POST request against the /api/change_requests endpoint. + + Parameters: + data (dict[str, Any]): The data to send in the request body. + params (dict[str, int | str]): The parameters to send in the request query string. + Returns: + ChangeRequestMsg: The parsed response data. + """ + return self._parse_one(ChangeRequestMsg, super()._make_request("POST", data=data, params=params).json()) diff --git a/src/pyconnectwise/endpoints/change_request/ChangeTypeEndpoint.py b/src/pyconnectwise/endpoints/change_request/ChangeTypeEndpoint.py new file mode 100644 index 000000000..e8e177bcf --- /dev/null +++ b/src/pyconnectwise/endpoints/change_request/ChangeTypeEndpoint.py @@ -0,0 +1,38 @@ +from typing import TYPE_CHECKING + +from pyconnectwise.endpoints.base.connectwise_endpoint import ConnectWiseEndpoint +from pyconnectwise.interfaces import IGettable +from pyconnectwise.models.change_request import ChangeRequestMsg, ChangeTypeData +from pyconnectwise.types import JSON, ConnectWiseChangeApprovalRequestParams + +if TYPE_CHECKING: + from pyconnectwise.clients.connectwise_client import ConnectWiseClient + + +class ChangeTypeEndpoint( + ConnectWiseEndpoint, + IGettable[list[ChangeRequestMsg], ConnectWiseChangeApprovalRequestParams], +): + def __init__(self, client: "ConnectWiseClient", parent_endpoint: ConnectWiseEndpoint = None) -> None: + ConnectWiseEndpoint.__init__(self, client, "change_type", parent_endpoint=parent_endpoint) + IGettable.__init__(self, list[ChangeTypeData]) + + # TODO - Handle paginated? + + # TODO - Figure out if there are other endpoints! + # TODO - Handle the fact that the TLD is different! + + def get( + self, data: JSON | None = None, params: ConnectWiseChangeApprovalRequestParams | None = None + ) -> list[ChangeTypeData]: + """ + Performs a GET request against the /api/change_requests endpoint. + + Parameters: + data (dict[str, Any]): The data to send in the request body. + params (dict[str, int | str]): The parameters to send in the request query string. + Returns: + list[ChangeTypeData]: The parsed response data. + """ + # TODO - Throw out the msg total, current information + return self._parse_many(ChangeTypeData, super()._make_request("GET", data=data, params=params).json()) diff --git a/src/pyconnectwise/endpoints/change_request/DefaultSettingsEndpoint.py b/src/pyconnectwise/endpoints/change_request/DefaultSettingsEndpoint.py new file mode 100644 index 000000000..d452e5c2a --- /dev/null +++ b/src/pyconnectwise/endpoints/change_request/DefaultSettingsEndpoint.py @@ -0,0 +1,18 @@ +from typing import TYPE_CHECKING + +from pyconnectwise.endpoints.base.connectwise_endpoint import ConnectWiseEndpoint +from pyconnectwise.endpoints.change_request.SetSettingsEndpoint import SetSettingsEndpoint + +if TYPE_CHECKING: + from pyconnectwise.clients.connectwise_client import ConnectWiseClient + + +class DefaultSettingsEndpoint( + ConnectWiseEndpoint, +): + def __init__(self, client: "ConnectWiseClient", parent_endpoint: ConnectWiseEndpoint = None) -> None: + ConnectWiseEndpoint.__init__(self, client, "DefaultSettings", parent_endpoint=parent_endpoint) + + @property + def set_settings(self) -> SetSettingsEndpoint: + return SetSettingsEndpoint(self.client, parent_endpoint=self) diff --git a/src/pyconnectwise/endpoints/change_request/GetStatsEndpoint.py b/src/pyconnectwise/endpoints/change_request/GetStatsEndpoint.py new file mode 100644 index 000000000..47528e993 --- /dev/null +++ b/src/pyconnectwise/endpoints/change_request/GetStatsEndpoint.py @@ -0,0 +1,34 @@ +from typing import TYPE_CHECKING + +from pyconnectwise.endpoints.base.connectwise_endpoint import ConnectWiseEndpoint +from pyconnectwise.interfaces import IPostable +from pyconnectwise.models.change_request import StatsMsg, StatsObject +from pyconnectwise.types import JSON, ConnectWiseChangeApprovalRequestParams + +if TYPE_CHECKING: + from pyconnectwise.clients.connectwise_client import ConnectWiseClient + + +class GetStatsEndpoint( + ConnectWiseEndpoint, + IPostable[list[StatsMsg], ConnectWiseChangeApprovalRequestParams], +): + def __init__(self, client: "ConnectWiseClient", parent_endpoint: ConnectWiseEndpoint = None) -> None: + # TODO - Figure out a cleaner implementation for the endpoint URL + ConnectWiseEndpoint.__init__(self, client, "change_request/dummy/getStats", parent_endpoint=parent_endpoint) + IPostable.__init__(self, list[StatsMsg]) + + def post( + self, data: JSON | None = None, params: ConnectWiseChangeApprovalRequestParams | None = None + ) -> list[StatsMsg]: + """ + Performs a GET request against the /api/change_requests endpoint. + + Parameters: + data (dict[str, Any]): The data to send in the request body. + params (dict[str, int | str]): The parameters to send in the request query string. + Returns: + list[ChangeRequestMsg]: The parsed response data. + """ + obj = self._parse_one(StatsObject, super()._make_request("POST", data=data, params=params).json()) + return obj.msg diff --git a/src/pyconnectwise/endpoints/change_request/LoginEndpoint.py b/src/pyconnectwise/endpoints/change_request/LoginEndpoint.py new file mode 100644 index 000000000..f76da7a7d --- /dev/null +++ b/src/pyconnectwise/endpoints/change_request/LoginEndpoint.py @@ -0,0 +1,31 @@ +from typing import TYPE_CHECKING + +from pyconnectwise.endpoints.base.connectwise_endpoint import ConnectWiseEndpoint +from pyconnectwise.interfaces import IPostable +from pyconnectwise.models.change_request import LoginMsg, LoginObject +from pyconnectwise.types import JSON, ConnectWiseChangeApprovalRequestParams + +if TYPE_CHECKING: + from pyconnectwise.clients.connectwise_client import ConnectWiseClient + + +class LoginEndpoint( + ConnectWiseEndpoint, + IPostable[LoginMsg, ConnectWiseChangeApprovalRequestParams], +): + def __init__(self, client: "ConnectWiseClient", parent_endpoint: ConnectWiseEndpoint = None) -> None: + ConnectWiseEndpoint.__init__(self, client, "login", parent_endpoint=parent_endpoint) + IPostable.__init__(self, LoginMsg) + + def post(self, data: JSON | None = None, params: ConnectWiseChangeApprovalRequestParams | None = None) -> LoginMsg: + """ + Performs a GET request against the /api/change_requests endpoint. + + Parameters: + data (dict[str, Any]): The data to send in the request body. + params (dict[str, int | str]): The parameters to send in the request query string. + Returns: + list[ChangeRequestMsg]: The parsed response data. + """ + obj = self._parse_one(LoginObject, super()._make_request("POST", data=data, params=params).json()) + return obj.msg diff --git a/src/pyconnectwise/endpoints/change_request/SetSettingsEndpoint.py b/src/pyconnectwise/endpoints/change_request/SetSettingsEndpoint.py new file mode 100644 index 000000000..fcfddd830 --- /dev/null +++ b/src/pyconnectwise/endpoints/change_request/SetSettingsEndpoint.py @@ -0,0 +1,32 @@ +from typing import TYPE_CHECKING + +from pyconnectwise.endpoints.base.connectwise_endpoint import ConnectWiseEndpoint +from pyconnectwise.interfaces import IPostable +from pyconnectwise.models.change_request import SetSettingsMsg, SetSettingsObject +from pyconnectwise.types import JSON, ConnectWiseChangeApprovalRequestParams + +if TYPE_CHECKING: + from pyconnectwise.clients.connectwise_client import ConnectWiseClient + + +class SetSettingsEndpoint( + ConnectWiseEndpoint, + IPostable[SetSettingsMsg, ConnectWiseChangeApprovalRequestParams], +): + def __init__(self, client: "ConnectWiseClient", parent_endpoint: ConnectWiseEndpoint = None) -> None: + ConnectWiseEndpoint.__init__(self, client, "set_settings", parent_endpoint=parent_endpoint) + + def post( + self, data: JSON | None = None, params: ConnectWiseChangeApprovalRequestParams | None = None + ) -> SetSettingsMsg: + """ + Performs a GET request against the /api/change_requests endpoint. + + Parameters: + data (dict[str, Any]): The data to send in the request body. + params (dict[str, int | str]): The parameters to send in the request query string. + Returns: + list[ChangeRequestMsg]: The parsed response data. + """ + obj = self._parse_one(SetSettingsObject, super()._make_request("POST", data=data, params=params).json()) + return obj.msg diff --git a/src/pyconnectwise/endpoints/change_request/SettingsEndpoint.py b/src/pyconnectwise/endpoints/change_request/SettingsEndpoint.py new file mode 100644 index 000000000..8b5509552 --- /dev/null +++ b/src/pyconnectwise/endpoints/change_request/SettingsEndpoint.py @@ -0,0 +1,45 @@ +from typing import TYPE_CHECKING + +from pyconnectwise.endpoints.base.connectwise_endpoint import ConnectWiseEndpoint +from pyconnectwise.endpoints.change_request.DefaultSettingsEndpoint import DefaultSettingsEndpoint +from pyconnectwise.interfaces import IGettable +from pyconnectwise.models.change_request import SettingsData, SettingsObject +from pyconnectwise.types import JSON, ConnectWiseChangeApprovalRequestParams + +if TYPE_CHECKING: + from pyconnectwise.clients.connectwise_client import ConnectWiseClient + + +class SettingsEndpoint( + ConnectWiseEndpoint, + IGettable[list[SettingsData], ConnectWiseChangeApprovalRequestParams], +): + def __init__(self, client: "ConnectWiseClient", parent_endpoint: ConnectWiseEndpoint = None) -> None: + ConnectWiseEndpoint.__init__(self, client, "settings", parent_endpoint=parent_endpoint) + IGettable.__init__(self, list[SettingsData]) + + def get( + self, data: JSON | None = None, params: ConnectWiseChangeApprovalRequestParams | None = None + ) -> list[SettingsData]: + """ + Performs a GET request against the /api/change_requests endpoint. + + Parameters: + data (dict[str, Any]): The data to send in the request body. + params (dict[str, int | str]): The parameters to send in the request query string. + Returns: + list[ChangeRequestMsg]: The parsed response data. + """ + obj = self._parse_one(SettingsObject, super()._make_request("GET", data=data, params=params).json()) + # TODO - `total`, `current`, which is paginated? + return obj.msg.data + + @property + def default_settings(self) -> DefaultSettingsEndpoint: + """ + Get the default settings for change requests. + + Returns: + SettingsData: The parsed response data. + """ + return DefaultSettingsEndpoint(self.client, parent_endpoint=self) diff --git a/src/pyconnectwise/endpoints/change_request/TemplateEndpoint.py b/src/pyconnectwise/endpoints/change_request/TemplateEndpoint.py new file mode 100644 index 000000000..e05249385 --- /dev/null +++ b/src/pyconnectwise/endpoints/change_request/TemplateEndpoint.py @@ -0,0 +1,33 @@ +from typing import TYPE_CHECKING + +from pyconnectwise.endpoints.base.connectwise_endpoint import ConnectWiseEndpoint +from pyconnectwise.interfaces import IGettable +from pyconnectwise.models.change_request import TemplateData, TemplateObject +from pyconnectwise.types import JSON, ConnectWiseChangeApprovalRequestParams + +if TYPE_CHECKING: + from pyconnectwise.clients.connectwise_client import ConnectWiseClient + + +class TemplateEndpoint( + ConnectWiseEndpoint, + IGettable[list[TemplateData], ConnectWiseChangeApprovalRequestParams], +): + def __init__(self, client: "ConnectWiseClient", parent_endpoint: ConnectWiseEndpoint = None) -> None: + ConnectWiseEndpoint.__init__(self, client, "template", parent_endpoint=parent_endpoint) + IGettable.__init__(self, list[TemplateData]) + + def get( + self, data: JSON | None = None, params: ConnectWiseChangeApprovalRequestParams | None = None + ) -> list[TemplateData]: + """ + Performs a GET request against the /api/change_requests endpoint. + + Parameters: + data (dict[str, Any]): The data to send in the request body. + params (dict[str, int | str]): The parameters to send in the request query string. + Returns: + list[ChangeRequestMsg]: The parsed response data. + """ + obj = self._parse_one(TemplateObject, super()._make_request("GET", data=data, params=params).json()) + return obj.msg.data diff --git a/src/pyconnectwise/endpoints/change_request/UserIdEndpoint.py b/src/pyconnectwise/endpoints/change_request/UserIdEndpoint.py new file mode 100644 index 000000000..f9979b685 --- /dev/null +++ b/src/pyconnectwise/endpoints/change_request/UserIdEndpoint.py @@ -0,0 +1,31 @@ +from typing import TYPE_CHECKING + +from pyconnectwise.endpoints.base.connectwise_endpoint import ConnectWiseEndpoint +from pyconnectwise.interfaces import IGettable +from pyconnectwise.models.change_request import ChangeRequestMsg, UserIdMsg, UserIdObject +from pyconnectwise.types import JSON, ConnectWiseChangeApprovalRequestParams + +if TYPE_CHECKING: + from pyconnectwise.clients.connectwise_client import ConnectWiseClient + + +class UserIdEndpoint( + ConnectWiseEndpoint, + IGettable[list[ChangeRequestMsg], ConnectWiseChangeApprovalRequestParams], +): + def __init__(self, client: "ConnectWiseClient", parent_endpoint: ConnectWiseEndpoint = None) -> None: + ConnectWiseEndpoint.__init__(self, client, "{id}", parent_endpoint=parent_endpoint) + IGettable.__init__(self, list[ChangeRequestMsg]) + + def get(self, data: JSON | None = None, params: ConnectWiseChangeApprovalRequestParams | None = None) -> UserIdMsg: + """ + Performs a GET request against the /api/change_requests endpoint. + + Parameters: + data (dict[str, Any]): The data to send in the request body. + params (dict[str, int | str]): The parameters to send in the request query string. + Returns: + list[ChangeRequestMsg]: The parsed response data. + """ + obj = self._parse_one(UserIdObject, super()._make_request("GET", data=data, params=params).json()) + return obj.msg diff --git a/src/pyconnectwise/endpoints/change_request/UsersEndpoint.py b/src/pyconnectwise/endpoints/change_request/UsersEndpoint.py new file mode 100644 index 000000000..8157011c4 --- /dev/null +++ b/src/pyconnectwise/endpoints/change_request/UsersEndpoint.py @@ -0,0 +1,45 @@ +from typing import TYPE_CHECKING + +from pyconnectwise.endpoints.base.connectwise_endpoint import ConnectWiseEndpoint +from pyconnectwise.endpoints.change_request.UserIdEndpoint import UserIdEndpoint +from pyconnectwise.interfaces import IGettable +from pyconnectwise.models.change_request import ChangeRequestMsg, ChangeTypeData, UserIdMsg, UserIdObject +from pyconnectwise.types import JSON, ConnectWiseChangeApprovalRequestParams + +if TYPE_CHECKING: + from pyconnectwise.clients.connectwise_client import ConnectWiseClient + + +class UsersEndpoint( + ConnectWiseEndpoint, + IGettable[list[ChangeRequestMsg], ConnectWiseChangeApprovalRequestParams], +): + def __init__(self, client: "ConnectWiseClient", parent_endpoint: ConnectWiseEndpoint = None) -> None: + ConnectWiseEndpoint.__init__(self, client, "users", parent_endpoint=parent_endpoint) + IGettable.__init__(self, list[ChangeTypeData]) + + # TODO - Handle paginated? + + # TODO - Figure out if there are other endpoints! + # TODO - Handle the fact that the TLD is different! + + def id(self, _id: str) -> UserIdEndpoint: + child = UserIdEndpoint(self.client, parent_endpoint=self) + child._id = _id + return child + + def get( + self, data: JSON | None = None, params: ConnectWiseChangeApprovalRequestParams | None = None + ) -> list[UserIdMsg]: + """ + Performs a GET request against the /api/change_requests endpoint. + + Parameters: + data (dict[str, Any]): The data to send in the request body. + params (dict[str, int | str]): The parameters to send in the request query string. + Returns: + list[ChangeTypeData]: The parsed response data. + """ + # TODO - Throw out the msg total, current information + obj = self._parse_one(UserIdObject, super()._make_request("GET", data=data, params=params).json()) + return obj.msg diff --git a/tests/__init__.py b/src/pyconnectwise/endpoints/change_request/__init__.py similarity index 100% rename from tests/__init__.py rename to src/pyconnectwise/endpoints/change_request/__init__.py diff --git a/src/pyconnectwise/models/change_request/__init__.py b/src/pyconnectwise/models/change_request/__init__.py new file mode 100644 index 000000000..cadd43eec --- /dev/null +++ b/src/pyconnectwise/models/change_request/__init__.py @@ -0,0 +1,480 @@ +from datetime import datetime +from typing import Annotated + +from pydantic import Field + +from pyconnectwise.models.base.connectwise_model import ConnectWiseModel + + +class ChangeRequestBundledtickets(ConnectWiseModel): + company_name: Annotated[str | None, Field(alias="CompanyName")] = None + ticket_number: Annotated[str | None, Field(alias="ticketNumber")] = None + summary: Annotated[str | None, Field(alias="summary")] = None + + +class ChangeRequestAffectedci(ConnectWiseModel): + id: Annotated[str | None, Field(alias="Id")] = None + configuration_type: Annotated[str | None, Field(alias="ConfigurationType")] = None + status: Annotated[str | None, Field(alias="Status")] = None + configuration_name: Annotated[str | None, Field(alias="ConfigurationName")] = None + serial_number: Annotated[str | None, Field(alias="SerialNumber")] = None + contact_name: Annotated[str | None, Field(alias="ContactName")] = None + + +class ChangeRequestSelectedsitedetails(ConnectWiseModel): + site_name: Annotated[str | None, Field(alias="SiteName")] = None + country: Annotated[str | None, Field(alias="Country")] = None + + +class ChangeRequestOwnerselected(ConnectWiseModel): + member_rec_id: Annotated[str | None, Field(alias="Member_RecID")] = None + member_id: Annotated[str | None, Field(alias="Member_ID")] = None + first_name: Annotated[str | None, Field(alias="First_Name")] = None + last_name: Annotated[str | None, Field(alias="Last_Name")] = None + country: Annotated[str | None, Field(alias="Country")] = None + work_role: Annotated[str | None, Field(alias="Work_Role")] = None + + +class ChangeRequestContactselected(ConnectWiseModel): + contact_rec_id: Annotated[str | None, Field(alias="Contact_RecID")] = None + company_rec_id: Annotated[str | None, Field(alias="Company_RecID")] = None + company_id: Annotated[str | None, Field(alias="Company_ID")] = None + first_name: Annotated[str | None, Field(alias="First_Name")] = None + last_name: Annotated[str | None, Field(alias="Last_Name")] = None + default_flag: Annotated[str | None, Field(alias="Default_Flag")] = None + default_phone: Annotated[str | None, Field(alias="Default_Phone")] = None + default_email: Annotated[str | None, Field(alias="Default_Email")] = None + country: Annotated[str | None, Field(alias="Country")] = None + + +class ChangeRequestCompanyselected(ConnectWiseModel): + city: Annotated[str | None, Field(alias="City")] = None + fax_number: Annotated[str | None, Field(alias="FaxNumber")] = None + default_contact_id: Annotated[str | None, Field(alias="DefaultContactId")] = None + zip: Annotated[str | None, Field(alias="Zip")] = None + website: Annotated[str | None, Field(alias="Website")] = None + country: Annotated[str | None, Field(alias="Country")] = None + site: Annotated[str | None, Field(alias="Site")] = None + updated_by: Annotated[str | None, Field(alias="UpdatedBy")] = None + territory: Annotated[str | None, Field(alias="Territory")] = None + last_updated: Annotated[str | None, Field(alias="LastUpdated")] = None + company_rec_id: Annotated[str | None, Field(alias="Company_RecID")] = None + company_name: Annotated[str | None, Field(alias="Company_Name")] = None + company_id: Annotated[str | None, Field(alias="Company_ID")] = None + lead_flag: Annotated[str | None, Field(alias="Lead_Flag")] = None + phone_nbr: Annotated[str | None, Field(alias="PhoneNbr")] = None + status: Annotated[str | None, Field(alias="status")] = None + state_id: Annotated[str | None, Field(alias="State_ID")] = None + address_line1: Annotated[str | None, Field(alias="Address_Line1")] = None + address_line2: Annotated[str | None, Field(alias="Address_Line2")] = None + + +class ChangeRequestCustomform(ConnectWiseModel): + id: Annotated[str | None, Field(alias="id")] = None + label: Annotated[str | None, Field(alias="label")] = None + value: Annotated[str | None, Field(alias="value")] = None + required: Annotated[bool | None, Field(alias="required")] = None + component: Annotated[str | None, Field(alias="component")] = None + + +class ChangeRequestApproved(ConnectWiseModel): + updated: Annotated[int | None, Field(alias="updated")] = None + + +class ChangeRequestAgingnotifications(ConnectWiseModel): + approved: Annotated[ChangeRequestApproved | None, Field(alias="Approved")] = None + + +class ChangeRequestMembers(ConnectWiseModel): + member_id: Annotated[str | None, Field(alias="memberId")] = None + mail: Annotated[bool | None, Field(alias="mail")] = None + mailsentkey: Annotated[int | None, Field(alias="mailsentkey")] = None + notes: Annotated[str | None, Field(alias="notes")] = None + approvedcreatedtime: Annotated[int | None, Field(alias="approvedcreatedtime")] = None + approval: Annotated[str | bool | None, Field(alias="approval")] = None + + +class ChangeRequestNotifications(ConnectWiseModel): + status: Annotated[str | None, Field(alias="status")] = None + members: Annotated[list[ChangeRequestMembers] | None, Field(alias="members")] = None + template: Annotated[str | None, Field(alias="template")] = None + + +class ChangeRequestConnectwiseServiceTypes(ConnectWiseModel): + s_r_type_rec_id: Annotated[str | None, Field(alias="SR_Type_RecID")] = None + service_type: Annotated[str | None, Field(alias="ServiceType")] = None + s_r_board_rec_id: Annotated[str | None, Field(alias="SR_Board_RecID")] = None + board_name: Annotated[str | None, Field(alias="Board_Name")] = None + inactive_flag: Annotated[str | None, Field(alias="Inactive_Flag")] = None + updated_by: Annotated[str | None, Field(alias="Updated_By")] = None + default_flag: Annotated[str | None, Field(alias="Default_Flag")] = None + + +class ChangeRequestConnectwiseServiceSubtypes(ConnectWiseModel): + s_r_sub_type_rec_id: Annotated[str | None, Field(alias="SR_SubType_RecID")] = None + service_sub_type_desc: Annotated[str | None, Field(alias="ServiceSubTypeDesc")] = None + s_r_board_rec_id: Annotated[str | None, Field(alias="SR_Board_RecID")] = None + board_name: Annotated[str | None, Field(alias="Board_Name")] = None + inactive_flag: Annotated[str | None, Field(alias="Inactive_Flag")] = None + s_r_type_rec_id: Annotated[str | None, Field(alias="SR_Type_RecID")] = None + service_type_desc: Annotated[str | None, Field(alias="ServiceTypeDesc")] = None + updated_by: Annotated[str | None, Field(alias="Updated_By")] = None + + +class ChangeRequestConnectwiseServicePriorities(ConnectWiseModel): + s_r_urgency_rec_id: Annotated[str | None, Field(alias="SR_Urgency_RecID")] = None + service_priority_desc: Annotated[str | None, Field(alias="Service_Priority_Desc")] = None + sort_order: Annotated[str | None, Field(alias="Sort_Order")] = None + color: Annotated[str | None, Field(alias="Color")] = None + default_flag: Annotated[str | None, Field(alias="Default_Flag")] = None + updated_by: Annotated[str | None, Field(alias="Updated_By")] = None + + +class ChangeRequestMsg(ConnectWiseModel): + id: Annotated[str | None, Field(alias="_id")] = None + bundled_tickets: Annotated[list[ChangeRequestBundledtickets] | None, Field(alias="bundledTickets")] = None + project_tickets_arr: Annotated[list | None, Field(alias="projectTicketsArr")] = None + affected_c_i: Annotated[list[ChangeRequestAffectedci] | None, Field(alias="affectedCI")] = None + company_id: Annotated[str | None, Field(alias="companyId")] = None + change_request_name: Annotated[str | None, Field(alias="changeRequestName")] = None + selected_site_details: Annotated[ChangeRequestSelectedsitedetails | None, Field(alias="selectedSiteDetails")] = None + contact_id: Annotated[str | None, Field(alias="contactId")] = None + short_desc: Annotated[str | None, Field(alias="shortDesc")] = None + is_converted_service_ticket: Annotated[bool | None, Field(alias="isConvertedServiceTicket")] = None + from_service_ticket: Annotated[str | None, Field(alias="fromServiceTicket")] = None + priority: Annotated[str | None, Field(alias="priority")] = None + category: Annotated[str | None, Field(alias="Category")] = None + sub_category: Annotated[str | None, Field(alias="subCategory")] = None + created_by: Annotated[str | None, Field(alias="createdBy")] = None + notifications: Annotated[list[ChangeRequestNotifications] | None, Field(alias="notifications")] = None + approval_status: Annotated[str | None, Field(alias="approvalStatus")] = None + number: Annotated[str | None, Field(alias="number")] = None + # TODO - Handle unix timestamps to datetime! + requested_by_date: Annotated[int | datetime | None, Field(alias="requestedByDate")] = None + planned_start_date: Annotated[int | datetime | None, Field(alias="plannedStartDate")] = None + planned_end_date: Annotated[int | datetime | None, Field(alias="plannedEndDate")] = None + work_start: Annotated[int | datetime | None, Field(alias="workStart")] = None + work_end: Annotated[int | datetime | None, Field(alias="workEnd")] = None + enable_schedule: Annotated[bool | None, Field(alias="enableSchedule")] = None + approval_emails: Annotated[list | None, Field(alias="approvalEmails")] = None + tmp_affected_c_i: Annotated[str | None, Field(alias="tmpAffectedCI")] = None + creator_firstname: Annotated[str | None, Field(alias="creator_firstname")] = None + creator_lastname: Annotated[str | None, Field(alias="creator_lastname")] = None + owner_selected: Annotated[ChangeRequestOwnerselected | None, Field(alias="ownerSelected")] = None + member_id: Annotated[str | None, Field(alias="memberId")] = None + contact_selected: Annotated[ChangeRequestContactselected | None, Field(alias="contactSelected")] = None + contact_name: Annotated[str | None, Field(alias="contactName")] = None + company_selected: Annotated[ChangeRequestCompanyselected | None, Field(alias="companySelected")] = None + company_name: Annotated[str | None, Field(alias="companyName")] = None + site_id: Annotated[str | None, Field(alias="siteId")] = None + highest_number: Annotated[int | None, Field(alias="highestNumber")] = None + impact: Annotated[str | None, Field(alias="impact")] = None + likelihood: Annotated[str | None, Field(alias="likelihood")] = None + risk: Annotated[str | None, Field(alias="risk")] = None + change_type_id: Annotated[str | None, Field(alias="changeTypeId")] = None + change_type_name: Annotated[str | None, Field(alias="changeTypeName")] = None + approval_group_name: Annotated[str | None, Field(alias="approvalGroupName")] = None + over_ride_approval_group: Annotated[bool | None, Field(alias="OverRideApprovalGroup")] = None + steps: Annotated[list | None, Field(alias="steps")] = None + requested_by_time: Annotated[str | None, Field(alias="requestedByTime")] = None + planned_star_time: Annotated[str | None, Field(alias="plannedStarTime")] = None + work_start_time: Annotated[str | None, Field(alias="workStartTime")] = None + custom_form: Annotated[list[ChangeRequestCustomform] | None, Field(alias="customForm")] = None + requested_by_time_added: Annotated[bool | None, Field(alias="requestedByTimeAdded")] = None + planned_start_time_added: Annotated[bool | None, Field(alias="plannedStartTimeAdded")] = None + work_start_time_added: Annotated[bool | None, Field(alias="workStartTimeAdded")] = None + planned_end_time_added: Annotated[bool | None, Field(alias="plannedEndTimeAdded")] = None + work_end_time_added: Annotated[bool | None, Field(alias="workEndTimeAdded")] = None + is_approved: Annotated[bool | str | None, Field(alias="isApproved")] = None + risk_token: Annotated[str | None, Field(alias="risk_token")] = None + aging_notifications: Annotated[ChangeRequestAgingnotifications | None, Field(alias="agingNotifications")] = None + created: Annotated[int | None, Field(alias="created")] = None + updated: Annotated[int | None, Field(alias="updated")] = None + partner_id: Annotated[str | None, Field(alias="partnerId")] = None + member_rand_strings: Annotated[dict[str, str] | None, Field(alias="memberRandStrings")] = None + members: Annotated[list[ChangeRequestMembers] | None, Field(alias="members")] = None + approve_encoded: Annotated[str | None, Field(alias="approveEncoded")] = None + emergency_status: Annotated[str | None, Field(alias="emergencyStatus")] = None + planned_starting_date: Annotated[str | None, Field(alias="plannedStartingDate")] = None + reject_encoded: Annotated[str | None, Field(alias="rejectEncoded")] = None + requestedby_date: Annotated[str | None, Field(alias="requestedbyDate")] = None + work_starting_date: Annotated[str | None, Field(alias="workStartingDate")] = None + connectwise_service_types: Annotated[ + list[ChangeRequestConnectwiseServiceTypes] | None, Field(alias="connectwise_service_types") + ] = None + connectwise_service_subtypes: Annotated[ + list[ChangeRequestConnectwiseServiceSubtypes] | None, Field(alias="connectwise_service_subtypes") + ] = None + connectwise_service_priorities: Annotated[ + list[ChangeRequestConnectwiseServicePriorities] | None, Field(alias="connectwise_service_priorities") + ] = None + + +class ChangeRequestObject(ConnectWiseModel): + status: Annotated[str | None, Field(alias="status")] = None + msg: Annotated[ChangeRequestMsg | None, Field(alias="msg")] = None + + +class ChangeRequestGetMsg(ConnectWiseModel): + total: Annotated[int | None, Field(alias="total")] = None + current: Annotated[int | None, Field(alias="current")] = None + data: Annotated[list[ChangeRequestMsg] | None, Field(alias="data")] = None + + +class ChangeRequestGetObject(ConnectWiseModel): + status: Annotated[str | None, Field(alias="status")] = None + msg: Annotated[ChangeRequestGetMsg | None, Field(alias="msg")] = None + + +class ChangeTypeForm(ConnectWiseModel): + component: Annotated[str | None, Field(alias="component")] = None + editable: Annotated[bool | None, Field(alias="editable")] = None + index: Annotated[int | None, Field(alias="index")] = None + label: Annotated[str | None, Field(alias="label")] = None + description: Annotated[str | None, Field(alias="description")] = None + placeholder: Annotated[str | None, Field(alias="placeholder")] = None + options: Annotated[list | None, Field(alias="options")] = None + required: Annotated[bool | None, Field(alias="required")] = None + validation: Annotated[str | None, Field(alias="validation")] = None + + +class ChangeTypeData(ConnectWiseModel): + _id: Annotated[str | None, Field(alias="_id")] = None + steps: Annotated[list | None, Field(alias="steps")] = None + change_type_name: Annotated[str | None, Field(alias="changeTypeName")] = None + approval_group_id: Annotated[str | None, Field(alias="approvalGroupId")] = None + template_id: Annotated[str | None, Field(alias="templateId")] = None + approval_type: Annotated[str | None, Field(alias="approvalType")] = None + over_ride_approval_group: Annotated[bool | None, Field(alias="overRideApprovalGroup")] = None + enable_schedule: Annotated[bool | None, Field(alias="enableSchedule")] = None + approval_group_name: Annotated[str | None, Field(alias="approvalGroupName")] = None + created: Annotated[int | None, Field(alias="created")] = None + updated: Annotated[int | None, Field(alias="updated")] = None + partner_id: Annotated[str | None, Field(alias="partnerId")] = None + form: Annotated[list[ChangeTypeForm] | None, Field(alias="form")] = None + selected_change_types: Annotated[bool | None, Field(alias="SelectedChangeTypes")] = None + + +class ChangeTypeMsg(ConnectWiseModel): + total: Annotated[int | None, Field(alias="total")] = None + current: Annotated[int | None, Field(alias="current")] = None + data: Annotated[list[ChangeTypeData] | None, Field(alias="data")] = None + + +class ChangeTypeObject(ConnectWiseModel): + status: Annotated[str | None, Field(alias="status")] = None + msg: Annotated[ChangeTypeMsg | None, Field(alias="msg")] = None + + +class UserIdMsg(ConnectWiseModel): + id: Annotated[str | None, Field(alias="_id")] = None + user_name: Annotated[str | None, Field(alias="userName")] = None + company: Annotated[str | None, Field(alias="company")] = None + domain: Annotated[str | None, Field(alias="domain")] = None + role: Annotated[str | None, Field(alias="role")] = None + password: Annotated[str | None, Field(alias="password")] = None + status: Annotated[str | None, Field(alias="status")] = None + wizard_complete: Annotated[bool | None, Field(alias="wizardComplete")] = None + direct_ticket_url: Annotated[bool | None, Field(alias="directTicketUrl")] = None + partner_id: Annotated[str | None, Field(alias="partnerId")] = None + api_key: Annotated[str | None, Field(alias="apiKey")] = None + created: Annotated[int | None, Field(alias="created")] = None + updated: Annotated[int | None, Field(alias="updated")] = None + is_agree: Annotated[bool | None, Field(alias="isAgree")] = None + + +class UserIdObject(ConnectWiseModel): + status: Annotated[str | None, Field(alias="status")] = None + msg: Annotated[UserIdMsg | None, Field(alias="msg")] = None + + +class AclRolesAcl(ConnectWiseModel): + administration: Annotated[bool | None, Field(alias="administration")] = None + billingprofile: Annotated[bool | None, Field(alias="billingprofile")] = None + jobs: Annotated[bool | None, Field(alias="jobs")] = None + companies: Annotated[bool | None, Field(alias="companies")] = None + + +class AclRolesData(ConnectWiseModel): + _id: Annotated[str | None, Field(alias="_id")] = None + first_name: Annotated[str | None, Field(alias="First_Name")] = None + last_name: Annotated[str | None, Field(alias="Last_Name")] = None + member_id: Annotated[str | None, Field(alias="Member_ID")] = None + member_rec_id: Annotated[str | None, Field(alias="Member_RecID")] = None + acl_group: Annotated[str | None, Field(alias="aclGroup")] = None + acl: Annotated[AclRolesAcl | None, Field(alias="acl")] = None + billing_unit_rec_id: Annotated[str | None, Field(alias="Billing_Unit_RecID")] = None + created: Annotated[int | None, Field(alias="created")] = None + updated: Annotated[int | None, Field(alias="updated")] = None + partner_id: Annotated[str | None, Field(alias="partnerId")] = None + + +class AclRolesMsg(ConnectWiseModel): + total: Annotated[int | None, Field(alias="total")] = None + current: Annotated[int | None, Field(alias="current")] = None + data: Annotated[list[AclRolesData] | None, Field(alias="data")] = None + + +class AclRolesObject(ConnectWiseModel): + status: Annotated[str | None, Field(alias="status")] = None + msg: Annotated[AclRolesMsg | None, Field(alias="msg")] = None + + +class LoginMsg(ConnectWiseModel): + partner_id: Annotated[str | None, Field(alias="partnerId")] = None + first_name: Annotated[str | None, Field(alias="firstName")] = None + last_name: Annotated[str | None, Field(alias="lastName")] = None + company_name: Annotated[str | None, Field(alias="companyName")] = None + user_name: Annotated[str | None, Field(alias="userName")] = None + concurrent_count: Annotated[int | None, Field(alias="concurrentCount")] = None + role: Annotated[str | None, Field(alias="role")] = None + isloggedin: Annotated[bool | None, Field(alias="isloggedin")] = None + integrator_id: Annotated[str | None, Field(alias="integratorId")] = None + domain: Annotated[str | None, Field(alias="domain")] = None + integrator_company: Annotated[str | None, Field(alias="integratorCompany")] = None + wizard_complete: Annotated[bool | None, Field(alias="wizardComplete")] = None + direct_ticket_url: Annotated[bool | None, Field(alias="directTicketUrl")] = None + rec_id: Annotated[str | None, Field(alias="RecId")] = None + is_agree: Annotated[bool | None, Field(alias="isAgree")] = None + version_code: Annotated[str | None, Field(alias="versionCode")] = None + is_instance_login: Annotated[bool | None, Field(alias="isInstanceLogin")] = None + + +class SettingsOptions(ConnectWiseModel): + hide_closed_cr: Annotated[bool | None, Field(alias="hideClosedCr")] = None + latest_per_format: Annotated[int | None, Field(alias="latestPerFormat")] = None + + +class LoginObject(ConnectWiseModel): + status: Annotated[str | None, Field(alias="status")] = None + msg: Annotated[LoginMsg | None, Field(alias="msg")] = None + + +class SettingsData(ConnectWiseModel): + _id: Annotated[str | None, Field(alias="_id")] = None + options: Annotated[SettingsOptions | None, Field(alias="options")] = None + name: Annotated[str | None, Field(alias="name")] = None + partner_id: Annotated[str | None, Field(alias="partnerId")] = None + created: Annotated[int | None, Field(alias="created")] = None + updated: Annotated[int | None, Field(alias="updated")] = None + + +class SettingsMsg(ConnectWiseModel): + total: Annotated[int | None, Field(alias="total")] = None + current: Annotated[int | None, Field(alias="current")] = None + data: Annotated[list[SettingsData] | None, Field(alias="data")] = None + + +class SettingsObject(ConnectWiseModel): + status: Annotated[str | None, Field(alias="status")] = None + msg: Annotated[SettingsMsg | None, Field(alias="msg")] = None + + +class StatsMsg(ConnectWiseModel): + count: Annotated[int | None, Field(alias="count")] = None + approval_status: Annotated[str | None, Field(alias="approvalStatus")] = None + + +class StatsObject(ConnectWiseModel): + status: Annotated[str | None, Field(alias="status")] = None + msg: Annotated[list[StatsMsg] | None, Field(alias="msg")] = None + + +class TemplateData(ConnectWiseModel): + _id: Annotated[str | None, Field(alias="_id")] = None + template_html: Annotated[str | None, Field(alias="template_html")] = None + img_url: Annotated[str | None, Field(alias="imgUrl")] = None + name: Annotated[str | None, Field(alias="name")] = None + subject: Annotated[str | None, Field(alias="subject")] = None + first_name: Annotated[str | None, Field(alias="firstName")] = None + last_name: Annotated[str | None, Field(alias="lastName")] = None + email_address: Annotated[str | None, Field(alias="emailAddress")] = None + category: Annotated[str | None, Field(alias="category")] = None + created: Annotated[int | None, Field(alias="created")] = None + updated: Annotated[int | None, Field(alias="updated")] = None + partner_id: Annotated[str | None, Field(alias="partnerId")] = None + + +class TemplateMsg(ConnectWiseModel): + total: Annotated[int | None, Field(alias="total")] = None + current: Annotated[int | None, Field(alias="current")] = None + data: Annotated[list[TemplateData] | None, Field(alias="data")] = None + + +class TemplateObject(ConnectWiseModel): + status: Annotated[str | None, Field(alias="status")] = None + msg: Annotated[TemplateMsg | None, Field(alias="msg")] = None + + +class SetSettingsOptions(ConnectWiseModel): + hide_closed_cr: Annotated[bool | None, Field(alias="hideClosedCr")] = None + latest_per_format: Annotated[int | None, Field(alias="latestPerFormat")] = None + + +class SetSettingsMsg(ConnectWiseModel): + id: Annotated[str | None, Field(alias="_id")] = None + options: Annotated[SetSettingsOptions | None, Field(alias="options")] = None + name: Annotated[str | None, Field(alias="name")] = None + partner_id: Annotated[str | None, Field(alias="partnerId")] = None + created: Annotated[int | None, Field(alias="created")] = None + updated: Annotated[int | None, Field(alias="updated")] = None + + +class SetSettingsObject(ConnectWiseModel): + status: Annotated[str | None, Field(alias="status")] = None + msg: Annotated[SetSettingsMsg | None, Field(alias="msg")] = None + + +class ContactsData(ConnectWiseModel): + contact_rec_id: Annotated[str | None, Field(alias="Contact_RecID")] = None + company_rec_id: Annotated[str | None, Field(alias="Company_RecID")] = None + company_id: Annotated[str | None, Field(alias="Company_ID")] = None + first_name: Annotated[str | None, Field(alias="First_Name")] = None + last_name: Annotated[str | None, Field(alias="Last_Name")] = None + default_flag: Annotated[str | None, Field(alias="Default_Flag")] = None + default_phone: Annotated[str | None, Field(alias="Default_Phone")] = None + default_email: Annotated[str | None, Field(alias="Default_Email")] = None + country: Annotated[str | None, Field(alias="Country")] = None + + +class ContactsMsg(ConnectWiseModel): + total: Annotated[int | None, Field(alias="total")] = None + current: Annotated[int | None, Field(alias="current")] = None + data: Annotated[list[ContactsData] | None, Field(alias="data")] = None + + +class ContactsObject(ConnectWiseModel): + status: Annotated[str | None, Field(alias="status")] = None + msg: Annotated[ContactsMsg | None, Field(alias="msg")] = None + + +class ConfigurationData(ConnectWiseModel): + id: Annotated[str | None, Field(alias="Id")] = None + configuration_type_id: Annotated[str | None, Field(alias="ConfigurationTypeId")] = None + configuration_type: Annotated[str | None, Field(alias="ConfigurationType")] = None + status: Annotated[str | None, Field(alias="Status")] = None + configuration_name: Annotated[str | None, Field(alias="ConfigurationName")] = None + installed_by: Annotated[str | None, Field(alias="InstalledBy")] = None + warranty_expiration: Annotated[str | None, Field(alias="WarrantyExpiration")] = None + universal_formatted_warranty_expiration: Annotated[ + str | None, Field(alias="Universal_Formatted_Warranty_Expiration") + ] = None + serial_number: Annotated[str | None, Field(alias="SerialNumber")] = None + model_number: Annotated[str | None, Field(alias="ModelNumber")] = None + tag_number: Annotated[str | None, Field(alias="TagNumber")] = None + contact_name: Annotated[str | None, Field(alias="ContactName")] = None + date_purchased: Annotated[str | None, Field(alias="Date_Purchased")] = None + vendor: Annotated[str | None, Field(alias="Vendor")] = None + date_installed: Annotated[str | None, Field(alias="Date_Installed")] = None + vendor_notes: Annotated[str | None, Field(alias="Vendor_Notes")] = None + + +class ConfigurationMsg(ConnectWiseModel): + total: Annotated[str | None, Field(alias="total")] = None + current: Annotated[int | None, Field(alias="current")] = None + data: Annotated[list[ConfigurationData] | None, Field(alias="data")] = None + + +class ConfigurationObject(ConnectWiseModel): + status: Annotated[str | None, Field(alias="status")] = None + msg: Annotated[ConfigurationMsg | None, Field(alias="msg")] = None diff --git a/src/pyconnectwise/models/manage/__init__.py b/src/pyconnectwise/models/manage/__init__.py index 3120a454a..e92a9ca6e 100644 --- a/src/pyconnectwise/models/manage/__init__.py +++ b/src/pyconnectwise/models/manage/__init__.py @@ -9883,7 +9883,9 @@ class Member(ConnectWiseModel): invoice_screen_default_tab_format: Annotated[ Literal["ShowInvoicingTab", "ShowAgreementInvoicingTab"], Field(alias="invoiceScreenDefaultTabFormat") ] - invoice_time_tab_format: Annotated[Literal["SummaryList", "DetailList"] | None, Field(alias="invoiceTimeTabFormat")] = None + invoice_time_tab_format: Annotated[ + Literal["SummaryList", "DetailList"] | None, Field(alias="invoiceTimeTabFormat") + ] = None invoicing_display_options: Annotated[ Literal["RemainOnInvoicingScreen", "ShowRecentInvoices"], Field(alias="invoicingDisplayOptions") ] diff --git a/src/pyconnectwise/types.py b/src/pyconnectwise/types.py index ecbb5d51c..ed32bfd6a 100644 --- a/src/pyconnectwise/types.py +++ b/src/pyconnectwise/types.py @@ -23,6 +23,14 @@ class ConnectWiseManageRequestParams(TypedDict): columns: NotRequired[str] +class ConnectWiseChangeApprovalRequestParams(TypedDict): + perColConditions: NotRequired[dict[str, str]] + skip: NotRequired[int] + limit: NotRequired[int] + orderBy: NotRequired[list[dict[str, str]]] + condition: NotRequired[str] + + class ConnectWiseAutomateRequestParams(TypedDict): condition: NotRequired[str] customFieldConditions: NotRequired[str] @@ -36,7 +44,12 @@ class ConnectWiseAutomateRequestParams(TypedDict): GenericRequestParams: TypeAlias = dict[str, Literals] -RequestParams: TypeAlias = ConnectWiseManageRequestParams | ConnectWiseAutomateRequestParams | GenericRequestParams +RequestParams: TypeAlias = ( + ConnectWiseManageRequestParams + | ConnectWiseChangeApprovalRequestParams + | ConnectWiseAutomateRequestParams + | GenericRequestParams +) PatchRequestData: TypeAlias = list[Patch] RequestData: TypeAlias = JSON | PatchRequestData RequestMethod: TypeAlias = Literal["GET", "POST", "PUT", "PATCH", "DELETE"] diff --git a/tests/clients/test_change_approvals.py b/tests/clients/test_change_approvals.py new file mode 100644 index 000000000..d6f438b65 --- /dev/null +++ b/tests/clients/test_change_approvals.py @@ -0,0 +1,166 @@ +import os +import pathlib + +# Load the .env file +from dotenv import load_dotenv + +from pyconnectwise.clients.change_request_client import ConnectWiseChangeApprovalClient +from pyconnectwise.types import ConnectWiseChangeApprovalRequestParams + +current_path = pathlib.Path(__file__).parent.resolve() +# In current directory, load the .env file +assert load_dotenv(os.path.join(current_path, ".env")) + + +def change_approval_client_init(): + client = ConnectWiseChangeApprovalClient( + company_name=os.environ["CW_COMPANY_NAME"], + manage_url=os.environ["CW_MANAGE_URL"], + client_id=os.environ["CW_CLIENT_ID"], + member_hash=os.environ["CW_MEMBER_HASH"], + member_id=os.environ["CW_MEMBER_ID"], + login_username=os.environ["CW_LOGIN_USERNAME"], + login_password=os.environ["CW_LOGIN_PASSWORD"], + login_role=os.environ["CW_LOGIN_ROLE"], + login_partner_id=os.environ["CW_LOGIN_PARTNER_ID"], + ) + return client + + +def test_change_approval_client_init(): + client = change_approval_client_init() + assert client.company_name == os.environ["CW_COMPANY_NAME"] + assert client.manage_url == os.environ["CW_MANAGE_URL"] + assert client.client_id == os.environ["CW_CLIENT_ID"] + assert client.member_hash == os.environ["CW_MEMBER_HASH"] + assert client.member_id == os.environ["CW_MEMBER_ID"] + + +def test_get_change_approval(): + # Testing with real data. :D + change_request_id = "66ed7a101336c1045d07502f" + client = change_approval_client_init() + client.auth_login() + change_approval = client.change_request.id(change_request_id).get() + assert change_approval.id == change_request_id + change_approvals = client.change_request.get( + params=ConnectWiseChangeApprovalRequestParams(orderBy=[{"updated": -1}]) + ) + assert len(change_approvals) > 20 + change_approvals = client.change_request.get( + params=ConnectWiseChangeApprovalRequestParams(skip=0, limit=20, orderBy=[{"updated": -1}]) + ) + assert len(change_approvals) == 20 + + +def test_get_user(): + # Testing with real data. :D + test_id = "642bfee39d2d780477bdc662" + client = change_approval_client_init() + client.auth_login() + user_info = client.users.id(test_id).get() + assert user_info.id == test_id + + +def test_get_acl_roles(): + client = change_approval_client_init() + client.auth_login() + obj_data = client.acl_roles.get() + assert obj_data is not None + assert len(obj_data) > 0 + + obj_filter_data = client.acl_roles.get( + params=ConnectWiseChangeApprovalRequestParams(perColConditions={"Member_RecID": client.login_msg.rec_id}) + ) + assert obj_filter_data is not None + assert len(obj_filter_data) > 0 + + +def test_get_settings(): + client = change_approval_client_init() + client.auth_login() + obj_filter_data = client.settings.get() + assert obj_filter_data is not None + assert len(obj_filter_data) > 0 + obj_filter_data = client.settings.get( + params=ConnectWiseChangeApprovalRequestParams(perColConditions={"name": "DefaultSettings"}) + ) + assert obj_filter_data is not None + assert len(obj_filter_data) > 0 + + +def test_get_templates(): + client = change_approval_client_init() + client.auth_login() + obj_filter_data = client.template.get( + params=ConnectWiseChangeApprovalRequestParams(perColConditions={"category": "My Templates"}) + ) + assert obj_filter_data is not None + assert len(obj_filter_data) > 0 + + +def test_get_stats(): + client = change_approval_client_init() + client.auth_login() + # TODO - Figure out what 120-timeDuration means + final_stats = client.get_stats.post(data={"timeDuration": 120}) + assert final_stats is not None + assert len(final_stats) > 0 + + +def test_get_contacts(): + client = change_approval_client_init() + client.auth_login() + final_stats = client.contacts.get(params=ConnectWiseChangeApprovalRequestParams(condition="Company_RecID==2")) + assert final_stats is not None + assert len(final_stats) > 0 + + +def test_get_configuration(): + client = change_approval_client_init() + client.auth_login() + # Yes, the field name is slightly different :facepalm: + final_stats = client.configuration.get(params=ConnectWiseChangeApprovalRequestParams(condition="CompanyRecID==2")) + assert final_stats is not None + assert len(final_stats) > 0 + + +def test_set_default_settings(): + client = change_approval_client_init() + client.auth_login() + # TODO - Make this data object cleaner? + final_stats = client.settings.default_settings.set_settings.post( + data={ + "params": { + "options": { + "hideClosedCr": True, + "latestPerFormat": 196, + } + } + } + ) + assert final_stats is not None + assert final_stats.name == "DefaultSettings" + assert final_stats.options.hide_closed_cr is True + assert final_stats.options.latest_per_format == 196 + # Reset the settings + final_stats = client.settings.default_settings.set_settings.post( + data={ + "params": { + "options": { + "hideClosedCr": False, + "latestPerFormat": 195, + } + } + } + ) + assert final_stats is not None + assert final_stats.name == "DefaultSettings" + assert final_stats.options.hide_closed_cr is False + assert final_stats.options.latest_per_format == 195 + + +def test_login(): + client = change_approval_client_init() + client.auth_login() + assert True