From 84cbed9be431f4f691aecda74176adb657d5071a Mon Sep 17 00:00:00 2001 From: Matthew Judy Date: Wed, 2 Oct 2024 13:44:45 -0400 Subject: [PATCH 01/14] Add additional type-checker support for editable package install. --- pyproject.toml | 4 ++++ src/jamf_pro_sdk/__init__.py | 13 +++++++++++++ src/jamf_pro_sdk/py.typed | 0 3 files changed, 17 insertions(+) create mode 100644 src/jamf_pro_sdk/py.typed diff --git a/pyproject.toml b/pyproject.toml index 7af7371..bd34a83 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -72,6 +72,10 @@ package-dir = {"" = "src"} where = ["src"] +[tool.setuptools.package-data] +"jamf_pro_sdk" = ["py.typed"] + + [tool.setuptools.dynamic] version = {attr = "jamf_pro_sdk.__about__.__version__"} readme = {file = ["README.md"], content-type = "text/markdown"} diff --git a/src/jamf_pro_sdk/__init__.py b/src/jamf_pro_sdk/__init__.py index a5a4385..13a2a15 100644 --- a/src/jamf_pro_sdk/__init__.py +++ b/src/jamf_pro_sdk/__init__.py @@ -8,3 +8,16 @@ ) from .helpers import logger_quick_setup from .models.client import SessionConfig + + +__all__ = [ + "__title__", + "__version__", + "JamfProClient", + "BasicAuthProvider", + "LoadFromAwsSecretsManager", + "LoadFromKeychain", + "PromptForCredentials", + "logger_quick_setup", + "SessionConfig", +] diff --git a/src/jamf_pro_sdk/py.typed b/src/jamf_pro_sdk/py.typed new file mode 100644 index 0000000..e69de29 From e8d2e1a4848c64262637c0f6b13c2e2d550100a6 Mon Sep 17 00:00:00 2001 From: Matthew Judy Date: Wed, 2 Oct 2024 13:59:10 -0400 Subject: [PATCH 02/14] Add ``files`` parameter to ``pro_api_request()``, passed through to underlying request if the HTTP method is``POST``. --- src/jamf_pro_sdk/clients/__init__.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/jamf_pro_sdk/clients/__init__.py b/src/jamf_pro_sdk/clients/__init__.py index daa07de..31d8829 100644 --- a/src/jamf_pro_sdk/clients/__init__.py +++ b/src/jamf_pro_sdk/clients/__init__.py @@ -2,7 +2,7 @@ import logging import tempfile from pathlib import Path -from typing import Any, Callable, Dict, Iterable, Iterator, Optional, Type, Union +from typing import Any, Callable, Dict, Iterable, Iterator, Optional, Type, Union, BinaryIO from urllib.parse import urlunparse import certifi @@ -195,6 +195,7 @@ def pro_api_request( resource_path: str, query_params: Optional[Dict[str, str]] = None, data: Optional[Union[dict, BaseModel]] = None, + files: Optional[dict[str, tuple[str, BinaryIO, str]]] = None, override_headers: Dict[str, str] = None, ) -> requests.Response: """Perform a request to the Pro API. @@ -214,6 +215,10 @@ def pro_api_request( or ``BaseModel`` that is being sent. :type data: dict | BaseModel + :param files: If the request is a ``POST``, a dictionary with a single ``files`` key, + and a tuple containing the filename, file-like object to upload, and mime type. + :type files: Optional[dict[str, tuple[str, BinaryIO, str]]] + :param override_headers: A dictionary of key-value pairs that will be set as headers for the request. You cannot override the ``Authorization`` or ``Content-Type`` headers. @@ -246,6 +251,9 @@ def pro_api_request( else: raise ValueError("'data' must be one of 'dict' or 'BaseModel'") + if files and (method.lower() == "post"): + pro_req["files"] = files + with self.session.request(**pro_req) as pro_resp: logger.info("ProAPIRequest %s %s", method.upper(), resource_path) try: From 2746964243473107bf48c8119e74d93dccff6c2c Mon Sep 17 00:00:00 2001 From: Matthew Judy Date: Wed, 2 Oct 2024 13:59:59 -0400 Subject: [PATCH 03/14] Add ``Package`` model. --- src/jamf_pro_sdk/models/pro/packages.py | 44 +++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 src/jamf_pro_sdk/models/pro/packages.py diff --git a/src/jamf_pro_sdk/models/pro/packages.py b/src/jamf_pro_sdk/models/pro/packages.py new file mode 100644 index 0000000..b90c454 --- /dev/null +++ b/src/jamf_pro_sdk/models/pro/packages.py @@ -0,0 +1,44 @@ +from pydantic import BaseModel, ConfigDict +from typing import Optional + + +class Package(BaseModel): + """Represents a full package record.""" + + model_config = ConfigDict(extra="allow") + + id : Optional[str] + packageName : str + fileName : str + categoryId : str + info : Optional[str] + notes : Optional[str] + priority : int + osRequirements : Optional[str] + fillUserTemplate : bool + indexed : bool + fillExistingUsers : bool + swu : bool + rebootRequired : bool + selfHealNotify : bool + selfHealingAction : Optional[str] + osInstall : bool + serialNumber : Optional[str] + parentPackageId : Optional[str] + basePath : Optional[str] + suppressUpdates : bool + cloudTransferStatus : str + ignoreConflicts : bool + suppressFromDock : bool + suppressEula : bool + suppressRegistration : bool + installLanguage : Optional[str] + md5 : Optional[str] + sha256 : Optional[str] + hashType : Optional[str] + hashValue : Optional[str] + size : Optional[str] + osInstallerVersion : Optional[str] + manifest : Optional[str] + manifestFileName : Optional[str] + format : Optional[str] From e62b913f9d82f969b538b60ea679f3aaaa75838c Mon Sep 17 00:00:00 2001 From: Matthew Judy Date: Wed, 2 Oct 2024 14:00:26 -0400 Subject: [PATCH 04/14] Add ``Packages`` to documentation. --- docs/reference/models_pro.rst | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/reference/models_pro.rst b/docs/reference/models_pro.rst index bb7dbae..0763c09 100644 --- a/docs/reference/models_pro.rst +++ b/docs/reference/models_pro.rst @@ -45,6 +45,16 @@ Computers ComputerContentCaching ComputerGroupMembership +Packages +-------- + +.. currentmodule:: jamf_pro_sdk.models.pro.packages + +.. autosummary:: + :toctree: _autosummary + + Package + JCDS2 ----- From a70ca6f625ac246663c6868dac096eca0b994849 Mon Sep 17 00:00:00 2001 From: Matthew Judy Date: Wed, 2 Oct 2024 14:01:39 -0400 Subject: [PATCH 05/14] Add ``ProApi.get_packages_v1()`` with overloads. --- src/jamf_pro_sdk/clients/pro_api/__init__.py | 94 +++++++++++++++++++- 1 file changed, 93 insertions(+), 1 deletion(-) diff --git a/src/jamf_pro_sdk/clients/pro_api/__init__.py b/src/jamf_pro_sdk/clients/pro_api/__init__.py index 5c70a02..e404a39 100644 --- a/src/jamf_pro_sdk/clients/pro_api/__init__.py +++ b/src/jamf_pro_sdk/clients/pro_api/__init__.py @@ -1,10 +1,11 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Callable, Iterator, List, Union +from typing import TYPE_CHECKING, Optional, Callable, Iterator, List, Union, Literal, overload from uuid import UUID from ...models.pro.api_options import * # noqa: F403 from ...models.pro.computers import Computer +from ...models.pro.packages import Package from ...models.pro.jcds2 import DownloadUrl, File, NewFile from ...models.pro.mdm import ( CustomCommand, @@ -132,6 +133,97 @@ def get_computer_inventory_v1( return paginator(return_generator=return_generator) + @overload + def get_packages_v1( + self, + start_page: int = ..., + end_page: Optional[int] = ..., + page_size: int = ..., + sort_expression: Optional[SortExpression] = ..., + filter_expression: Optional[FilterExpression] = ..., + return_generator: Literal[False] = False, + ) -> List[Package]: ... + + @overload + def get_packages_v1( + self, + start_page: int = ..., + end_page: Optional[int] = ..., + page_size: int = ..., + sort_expression: Optional[SortExpression] = ..., + filter_expression: Optional[FilterExpression] = ..., + return_generator: Literal[True] = True, + ) -> Iterator[Page]: ... + + def get_packages_v1( + self, + start_page: int = 0, + end_page: Optional[int] = None, + page_size: int = 100, + sort_expression: Optional[SortExpression] = None, + filter_expression: Optional[FilterExpression] = None, + return_generator: bool = False, + ) -> Union[List[Package], Iterator[Page]]: + """Returns a list of package records. + + :param start_page: (optional) The page to begin returning results from. See + :class:`Paginator` for more information. + :type start_page: int + + :param end_page: (optional) The page to end returning results at. See :class:`Paginator` for + more information. + :type start_page: int + + :param page_size: (optional) The number of results to include in each requested page. See + :class:`Paginator` for more information. + :type page_size: int + + :param sort_expression: (optional) The sort fields to apply to the request. See the + documentation for :ref:`Pro API Sorting` for more information. + + Allowed sort fields: + + .. autoapioptions:: jamf_pro_sdk.models.pro.api_options.get_computer_inventory_v1_allowed_sort_fields + + :type sort_expression: SortExpression + + :param filter_expression: (optional) The filter expression to apply to the request. See the + documentation for :ref:`Pro API Filtering` for more information. + + Allowed filter fields: + + .. autoapioptions:: jamf_pro_sdk.models.pro.api_options.get_computer_inventory_v1_allowed_filter_fields + + :type filter_expression: FilterExpression + + :param return_generator: If ``True`` a generator is returned to iterate over pages. By + default, the results for all pages will be returned in a single response. + :type return_generator: bool + + :return: List of computers OR a paginator generator. + :rtype: List[~jamf_pro_sdk.models.pro.computer.Computer] | Iterator[Page] + + """ + if sort_expression: + sort_expression.validate(get_packages_v1_allowed_sort_fields) + + if filter_expression: + filter_expression.validate(get_packages_v1_allowed_filter_fields) + + paginator = Paginator( + api_client=self, + resource_path="v1/packages", + return_model=Package, + start_page=start_page, + end_page=end_page, + page_size=page_size, + sort_expression=sort_expression, + filter_expression=filter_expression, + extra_params={"section": ",".join(sections)}, + ) + + return paginator(return_generator=return_generator) + # JCDS APIs def get_jcds_files_v1(self) -> List[File]: From 0beea3c74ed75a2f99b3b4ac83364042469d1fa9 Mon Sep 17 00:00:00 2001 From: Matthew Judy Date: Wed, 2 Oct 2024 14:03:16 -0400 Subject: [PATCH 06/14] Add allowed sort/filter fields. Note: The sort fields are not specified by the documentation; duplicated the documented values for filter. --- src/jamf_pro_sdk/models/pro/api_options.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/jamf_pro_sdk/models/pro/api_options.py b/src/jamf_pro_sdk/models/pro/api_options.py index 0d6f00e..c2d6b6a 100644 --- a/src/jamf_pro_sdk/models/pro/api_options.py +++ b/src/jamf_pro_sdk/models/pro/api_options.py @@ -106,6 +106,26 @@ "purchasing.warrantyDate", ] +get_packages_v1_allowed_sort_fields = [ + "id", + "packageName", + "fileName", + "categoryId", + "info", + "notes", + "manifestFileName", +] + +get_packages_v1_allowed_filter_fields = [ + "id", + "packageName", + "fileName", + "categoryId", + "info", + "notes", + "manifestFileName", +] + get_mdm_commands_v2_allowed_sort_fields = [ "uuid", "clientManagementId", From 04e62624ef455acda103af77821fb47ac90c9111 Mon Sep 17 00:00:00 2001 From: Matthew Judy Date: Sat, 2 Nov 2024 14:24:21 -0400 Subject: [PATCH 07/14] Add overloads for ``get_computer_inventory_v1``. Resolve other type-checking issues. --- src/jamf_pro_sdk/clients/pro_api/__init__.py | 59 ++++++-- .../clients/pro_api/pagination.py | 16 +-- src/jamf_pro_sdk/models/client.py | 12 +- src/jamf_pro_sdk/models/pro/computers.py | 136 +++++++++--------- src/jamf_pro_sdk/models/pro/mdm.py | 2 +- src/jamf_pro_sdk/models/pro/mobile_devices.py | 20 +-- 6 files changed, 140 insertions(+), 105 deletions(-) diff --git a/src/jamf_pro_sdk/clients/pro_api/__init__.py b/src/jamf_pro_sdk/clients/pro_api/__init__.py index e404a39..aa587c3 100644 --- a/src/jamf_pro_sdk/clients/pro_api/__init__.py +++ b/src/jamf_pro_sdk/clients/pro_api/__init__.py @@ -3,7 +3,19 @@ from typing import TYPE_CHECKING, Optional, Callable, Iterator, List, Union, Literal, overload from uuid import UUID -from ...models.pro.api_options import * # noqa: F403 +from ...models.pro.api_options import ( + get_computer_inventory_v1_allowed_sections, + get_computer_inventory_v1_allowed_sort_fields, + get_computer_inventory_v1_allowed_filter_fields, + get_packages_v1_allowed_sort_fields, + get_packages_v1_allowed_filter_fields, + get_mdm_commands_v2_allowed_command_types, + get_mdm_commands_v2_allowed_sort_fields, + get_mdm_commands_v2_allowed_filter_fields, + get_mobile_device_inventory_v2_allowed_sections, + get_mobile_device_inventory_v2_allowed_sort_fields, + get_mobile_device_inventory_v2_allowed_filter_fields, +) from ...models.pro.computers import Computer from ...models.pro.packages import Package from ...models.pro.jcds2 import DownloadUrl, File, NewFile @@ -43,14 +55,38 @@ def __init__( # Computer Inventory APIs + @overload + def get_computer_inventory_v1( + self, + sections: Optional[List[str]] = ..., + start_page: int = ..., + end_page: Optional[int] = ..., + page_size: int = ..., + sort_expression: Optional[SortExpression] = ..., + filter_expression: Optional[FilterExpression] = ..., + return_generator: Literal[False] = False, + ) -> List[Computer]: ... + + @overload def get_computer_inventory_v1( self, - sections: List[str] = None, + sections: Optional[List[str]] = ..., + start_page: int = ..., + end_page: Optional[int] = ..., + page_size: int = ..., + sort_expression: Optional[SortExpression] = ..., + filter_expression: Optional[FilterExpression] = ..., + return_generator: Literal[True] = True, + ) -> Iterator[Page]: ... + + def get_computer_inventory_v1( + self, + sections: Optional[List[str]] = None, start_page: int = 0, - end_page: int = None, + end_page: Optional[int] = None, page_size: int = 100, - sort_expression: SortExpression = None, - filter_expression: FilterExpression = None, + sort_expression: Optional[SortExpression] = None, + filter_expression: Optional[FilterExpression] = None, return_generator: bool = False, ) -> Union[List[Computer], Iterator[Page]]: """Returns a list of computer inventory records. @@ -219,7 +255,6 @@ def get_packages_v1( page_size=page_size, sort_expression=sort_expression, filter_expression=filter_expression, - extra_params={"section": ",".join(sections)}, ) return paginator(return_generator=return_generator) @@ -352,9 +387,9 @@ def get_mdm_commands_v2( self, filter_expression: FilterExpression, start_page: int = 0, - end_page: int = None, + end_page: Optional[int] = None, page_size: int = 100, - sort_expression: SortExpression = None, + sort_expression: Optional[SortExpression] = None, return_generator: bool = False, ) -> Union[List[MdmCommandStatus], Iterator[Page]]: """Returns a list of MDM commands. @@ -427,12 +462,12 @@ def get_mdm_commands_v2( def get_mobile_device_inventory_v2( self, - sections: List[str] = None, + sections: Optional[List[str]] = None, start_page: int = 0, - end_page: int = None, + end_page: Optional[int] = None, page_size: int = 100, - sort_expression: SortExpression = None, - filter_expression: FilterExpression = None, + sort_expression: Optional[SortExpression] = None, + filter_expression: Optional[FilterExpression] = None, return_generator: bool = False, ) -> Union[List[MobileDevice], Iterator[Page]]: """Returns a list of mobile device (iOS and tvOS) inventory records. diff --git a/src/jamf_pro_sdk/clients/pro_api/pagination.py b/src/jamf_pro_sdk/clients/pro_api/pagination.py index d4d8627..dd3d9cd 100644 --- a/src/jamf_pro_sdk/clients/pro_api/pagination.py +++ b/src/jamf_pro_sdk/clients/pro_api/pagination.py @@ -2,7 +2,7 @@ import math from dataclasses import dataclass -from typing import TYPE_CHECKING, Dict, Iterable, Iterator, List, Type, Union +from typing import TYPE_CHECKING, Dict, Iterable, Iterator, List, Type, Optional, Union from pydantic import BaseModel @@ -54,7 +54,7 @@ def __init__(self, name: str): def _return_expression(self, operator: str, value: Union[bool, int, str]) -> FilterExpression: return FilterExpression( filter_expression=f"{self.name}{operator}{value}", - fields=[FilterEntry(name=self.name, op=operator, value=value)], + fields=[FilterEntry(name=self.name, op=operator, value=str(value))], ) def eq(self, value: Union[bool, int, str]) -> FilterExpression: @@ -146,13 +146,13 @@ def __init__( self, api_client: ProApi, resource_path: str, - return_model: Type[BaseModel] = None, + return_model: Type[BaseModel], start_page: int = 0, - end_page: int = None, + end_page: Optional[int] = None, page_size: int = 100, - sort_expression: SortExpression = None, - filter_expression: FilterExpression = None, - extra_params: Dict[str, str] = None, + sort_expression: Optional[SortExpression] = None, + filter_expression: Optional[FilterExpression] = None, + extra_params: Optional[Dict[str, str]] = None, ): """A paginator for the Jamf Pro API. A paginator automatically iterates over an API if multiple unreturned pages are detected in the response. Paginated requests are performed @@ -212,7 +212,7 @@ def __init__( self.extra_params = extra_params def _paginated_request(self, page: int) -> Page: - query_params = {"page": page, "page-size": self.page_size} + query_params: dict = {"page": page, "page-size": self.page_size} if self.sort_expression: query_params["sort"] = str(self.sort_expression) if self.filter_expression: diff --git a/src/jamf_pro_sdk/models/client.py b/src/jamf_pro_sdk/models/client.py index f2f55ac..ce78f9e 100644 --- a/src/jamf_pro_sdk/models/client.py +++ b/src/jamf_pro_sdk/models/client.py @@ -12,8 +12,8 @@ class Schemes(str, Enum): - http: str = "http" - https: str = "https" + http = "http" + https = "https" class SessionConfig(BaseModel): @@ -51,15 +51,15 @@ class SessionConfig(BaseModel): :type scheme: str """ - timeout: Union[int, None] = None + timeout: Optional[int] = None max_retries: int = 0 max_concurrency: int = 5 return_exceptions: bool = True user_agent: str = DEFAULT_USER_AGENT verify: bool = True - cookie: Union[str, Path] = None - ca_cert_bundle: Union[str, Path] = None - scheme: Schemes = "https" + cookie: Optional[Union[str, Path]] = None + ca_cert_bundle: Optional[Union[str, Path]] = None + scheme: Schemes = Schemes.https class AccessToken(BaseModel): diff --git a/src/jamf_pro_sdk/models/pro/computers.py b/src/jamf_pro_sdk/models/pro/computers.py index bb0bedd..4a0727b 100644 --- a/src/jamf_pro_sdk/models/pro/computers.py +++ b/src/jamf_pro_sdk/models/pro/computers.py @@ -11,16 +11,16 @@ class ComputerExtensionAttributeDataType(str, Enum): - STRING: str = "STRING" - INTEGER: str = "INTEGER" - DATE_TIME: str = "DATE_TIME" + STRING = "STRING" + INTEGER = "INTEGER" + DATE_TIME = "DATE_TIME" -class ComputerExtensionAttributeInputType(Enum): - TEXT: str = "TEXT" - POPUP: str = "POPUP" - SCRIPT: str = "SCRIPT" - LDAP: str = "LDAP" +class ComputerExtensionAttributeInputType(str, Enum): + TEXT = "TEXT" + POPUP = "POPUP" + SCRIPT = "SCRIPT" + LDAP = "LDAP" class ComputerExtensionAttribute(BaseModel): @@ -100,24 +100,24 @@ class ComputerGeneral(BaseModel): class ComputerPartitionFileVault2State(str, Enum): - UNKNOWN: str = "UNKNOWN" - UNENCRYPTED: str = "UNENCRYPTED" - INELIGIBLE: str = "INELIGIBLE" - DECRYPTED: str = "DECRYPTED" - DECRYPTING: str = "DECRYPTING" - ENCRYPTED: str = "ENCRYPTED" - ENCRYPTING: str = "ENCRYPTING" - RESTART_NEEDED: str = "RESTART_NEEDED" - OPTIMIZING: str = "OPTIMIZING" - DECRYPTING_PAUSED: str = "DECRYPTING_PAUSED" - ENCRYPTING_PAUSED: str = "ENCRYPTING_PAUSED" + UNKNOWN = "UNKNOWN" + UNENCRYPTED = "UNENCRYPTED" + INELIGIBLE = "INELIGIBLE" + DECRYPTED = "DECRYPTED" + DECRYPTING = "DECRYPTING" + ENCRYPTED = "ENCRYPTED" + ENCRYPTING = "ENCRYPTING" + RESTART_NEEDED = "RESTART_NEEDED" + OPTIMIZING = "OPTIMIZING" + DECRYPTING_PAUSED = "DECRYPTING_PAUSED" + ENCRYPTING_PAUSED = "ENCRYPTING_PAUSED" class IndividualRecoveryKeyValidityStatus(str, Enum): - VALID: str = "VALID" - INVALID: str = "INVALID" - UNKNOWN: str = "UNKNOWN" - NOT_APPLICABLE: str = "NOT_APPLICABLE" + VALID = "VALID" + INVALID = "INVALID" + UNKNOWN = "UNKNOWN" + NOT_APPLICABLE = "NOT_APPLICABLE" class ComputerPartitionEncryption(BaseModel): @@ -180,9 +180,9 @@ class ComputerApplication(BaseModel): class PartitionType(str, Enum): - BOOT: str = "BOOT" - RECOVERY: str = "RECOVERY" - OTHER: str = "OTHER" + BOOT = "BOOT" + RECOVERY = "RECOVERY" + OTHER = "OTHER" class ComputerPartition(BaseModel): @@ -309,16 +309,16 @@ class ComputerHardware(BaseModel): class UserAccountType(str, Enum): - LOCAL: str = "LOCAL" - MOBILE: str = "MOBILE" - UNKNOWN: str = "UNKNOWN" + LOCAL = "LOCAL" + MOBILE = "MOBILE" + UNKNOWN = "UNKNOWN" class AzureActiveDirectoryId(str, Enum): - ACTIVATED: str = "ACTIVATED" - DEACTIVATED: str = "DEACTIVATED" - UNRESPONSIVE: str = "UNRESPONSIVE" - UNKNOWN: str = "UNKNOWN" + ACTIVATED = "ACTIVATED" + DEACTIVATED = "DEACTIVATED" + UNRESPONSIVE = "UNRESPONSIVE" + UNKNOWN = "UNKNOWN" class ComputerLocalUserAccount(BaseModel): @@ -346,16 +346,16 @@ class ComputerLocalUserAccount(BaseModel): class LifecycleStatus(str, Enum): - ACTIVE: str = "ACTIVE" - INACTIVE: str = "INACTIVE" + ACTIVE = "ACTIVE" + INACTIVE = "INACTIVE" class CertificateStatus(str, Enum): - EXPIRING: str = "EXPIRING" - EXPIRED: str = "EXPIRED" - REVOKED: str = "REVOKED" - PENDING_REVOKE: str = "PENDING_REVOKE" - ISSUED: str = "ISSUED" + EXPIRING = "EXPIRING" + EXPIRED = "EXPIRED" + REVOKED = "REVOKED" + PENDING_REVOKE = "PENDING_REVOKE" + ISSUED = "ISSUED" class ComputerCertificate(BaseModel): @@ -422,32 +422,32 @@ class ComputerFont(BaseModel): class SipStatus(str, Enum): - NOT_COLLECTED: str = "NOT_COLLECTED" - NOT_AVAILABLE: str = "NOT_AVAILABLE" - DISABLED: str = "DISABLED" - ENABLED: str = "ENABLED" + NOT_COLLECTED = "NOT_COLLECTED" + NOT_AVAILABLE = "NOT_AVAILABLE" + DISABLED = "DISABLED" + ENABLED = "ENABLED" class GatekeeperStatus(str, Enum): - NOT_COLLECTED: str = "NOT_COLLECTED" - DISABLED: str = "DISABLED" - APP_STORE_AND_IDENTIFIED_DEVELOPERS: str = "APP_STORE_AND_IDENTIFIED_DEVELOPERS" - APP_STORE: str = "APP_STORE" + NOT_COLLECTED = "NOT_COLLECTED" + DISABLED = "DISABLED" + APP_STORE_AND_IDENTIFIED_DEVELOPERS = "APP_STORE_AND_IDENTIFIED_DEVELOPERS" + APP_STORE = "APP_STORE" class SecureBootLevel(str, Enum): - NO_SECURITY: str = "NO_SECURITY" - MEDIUM_SECURITY: str = "MEDIUM_SECURITY" - FULL_SECURITY: str = "FULL_SECURITY" - NOT_SUPPORTED: str = "NOT_SUPPORTED" - UNKNOWN: str = "UNKNOWN" + NO_SECURITY = "NO_SECURITY" + MEDIUM_SECURITY = "MEDIUM_SECURITY" + FULL_SECURITY = "FULL_SECURITY" + NOT_SUPPORTED = "NOT_SUPPORTED" + UNKNOWN = "UNKNOWN" class ExternalBootLevel(str, Enum): - ALLOW_BOOTING_FROM_EXTERNAL_MEDIA: str = "ALLOW_BOOTING_FROM_EXTERNAL_MEDIA" - DISALLOW_BOOTING_FROM_EXTERNAL_MEDIA: str = "DISALLOW_BOOTING_FROM_EXTERNAL_MEDIA" - NOT_SUPPORTED: str = "NOT_SUPPORTED" - UNKNOWN: str = "UNKNOWN" + ALLOW_BOOTING_FROM_EXTERNAL_MEDIA = "ALLOW_BOOTING_FROM_EXTERNAL_MEDIA" + DISALLOW_BOOTING_FROM_EXTERNAL_MEDIA = "DISALLOW_BOOTING_FROM_EXTERNAL_MEDIA" + NOT_SUPPORTED = "NOT_SUPPORTED" + UNKNOWN = "UNKNOWN" class ComputerSecurity(BaseModel): @@ -470,11 +470,11 @@ class ComputerSecurity(BaseModel): class FileVault2Status(str, Enum): - NOT_APPLICABLE: str = "NOT_APPLICABLE" - NOT_ENCRYPTED: str = "NOT_ENCRYPTED" - BOOT_ENCRYPTED: str = "BOOT_ENCRYPTED" - SOME_ENCRYPTED: str = "SOME_ENCRYPTED" - ALL_ENCRYPTED: str = "ALL_ENCRYPTED" + NOT_APPLICABLE = "NOT_APPLICABLE" + NOT_ENCRYPTED = "NOT_ENCRYPTED" + BOOT_ENCRYPTED = "BOOT_ENCRYPTED" + SOME_ENCRYPTED = "SOME_ENCRYPTED" + ALL_ENCRYPTED = "ALL_ENCRYPTED" class ComputerOperatingSystem(BaseModel): @@ -610,15 +610,15 @@ class ComputerContentCachingDataMigrationError(BaseModel): class ComputerContentCachingRegistrationStatus(str, Enum): - CONTENT_CACHING_FAILED: str = "CONTENT_CACHING_FAILED" - CONTENT_CACHING_PENDING: str = "CONTENT_CACHING_PENDING" - CONTENT_CACHING_SUCCEEDED: str = "CONTENT_CACHING_SUCCEEDED" + CONTENT_CACHING_FAILED = "CONTENT_CACHING_FAILED" + CONTENT_CACHING_PENDING = "CONTENT_CACHING_PENDING" + CONTENT_CACHING_SUCCEEDED = "CONTENT_CACHING_SUCCEEDED" class ComputerContentCachingTetheratorStatus(str, Enum): - CONTENT_CACHING_UNKNOWN: str = "CONTENT_CACHING_UNKNOWN" - CONTENT_CACHING_DISABLED: str = "CONTENT_CACHING_DISABLED" - CONTENT_CACHING_ENABLED: str = "CONTENT_CACHING_ENABLED" + CONTENT_CACHING_UNKNOWN = "CONTENT_CACHING_UNKNOWN" + CONTENT_CACHING_DISABLED = "CONTENT_CACHING_DISABLED" + CONTENT_CACHING_ENABLED = "CONTENT_CACHING_ENABLED" class ComputerContentCaching(BaseModel): diff --git a/src/jamf_pro_sdk/models/pro/mdm.py b/src/jamf_pro_sdk/models/pro/mdm.py index 41a4aca..219bdeb 100644 --- a/src/jamf_pro_sdk/models/pro/mdm.py +++ b/src/jamf_pro_sdk/models/pro/mdm.py @@ -207,7 +207,7 @@ class CustomCommand(BaseModel): class SendMdmCommandClientData(BaseModel): - managementId: UUID + managementId: Union[str, UUID] BuiltInCommands = Annotated[ diff --git a/src/jamf_pro_sdk/models/pro/mobile_devices.py b/src/jamf_pro_sdk/models/pro/mobile_devices.py index e79a955..a91f20f 100644 --- a/src/jamf_pro_sdk/models/pro/mobile_devices.py +++ b/src/jamf_pro_sdk/models/pro/mobile_devices.py @@ -10,14 +10,14 @@ class MobileDeviceType(str, Enum): """Not in use: the value of this attribute can be an undocumented state.""" - iOS: str = "iOS" - tvOS: str = "tvOS" + iOS = "iOS" + tvOS = "tvOS" class MobileDeviceExtensionAttributeType(str, Enum): - STRING: str = "STRING" - INTEGER: str = "INTEGER" - DATE: str = "DATE" + STRING = "STRING" + INTEGER = "INTEGER" + DATE = "DATE" class MobileDeviceExtensionAttribute(BaseModel): @@ -125,11 +125,11 @@ class MobileDeviceUserProfile(MobileDeviceProfile): class MobileDeviceOwnershipType(str, Enum): - Institutional: str = "Institutional" - PersonalDeviceProfile: str = "PersonalDeviceProfile" - UserEnrollment: str = "UserEnrollment" - AccountDrivenUserEnrollment: str = "AccountDrivenUserEnrollment" - AccountDrivenDeviceEnrollment: str = "AccountDrivenDeviceEnrollment" + Institutional = "Institutional" + PersonalDeviceProfile = "PersonalDeviceProfile" + UserEnrollment = "UserEnrollment" + AccountDrivenUserEnrollment = "AccountDrivenUserEnrollment" + AccountDrivenDeviceEnrollment = "AccountDrivenDeviceEnrollment" class MobileDeviceEnrollmentMethodPrestage(BaseModel): From 255ca6b9946ad586f45b9db04df2b50bcb3f5027 Mon Sep 17 00:00:00 2001 From: Matthew Judy Date: Mon, 4 Nov 2024 16:41:00 -0500 Subject: [PATCH 08/14] Add basic integration test for packages. --- tests/integration/test_pro_client_packages.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 tests/integration/test_pro_client_packages.py diff --git a/tests/integration/test_pro_client_packages.py b/tests/integration/test_pro_client_packages.py new file mode 100644 index 0000000..d109ff5 --- /dev/null +++ b/tests/integration/test_pro_client_packages.py @@ -0,0 +1,14 @@ +# from jamf_pro_sdk.clients.pro_api.pagination import FilterField, SortField + + +def test_integration_pro_packages_v1_default(jamf_client): + # This test is only valid if the computer inventory is less than the max page size + + # Test at max page size to get full inventory count + result_one_call = jamf_client.pro_api.get_packages_v1() + result_total_count = len(result_one_call) + assert result_total_count > 1 + + # Test paginated response matches full inventory count above + result_paginated = jamf_client.pro_api.get_packages_v1(page_size=10) + assert result_total_count == len(result_paginated) From 347663d4408f758720609dd65abab8c0d9fe9d59 Mon Sep 17 00:00:00 2001 From: Matthew Judy Date: Mon, 4 Nov 2024 17:14:08 -0500 Subject: [PATCH 09/14] Add a few missed optional typing annotations. --- src/jamf_pro_sdk/clients/__init__.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/jamf_pro_sdk/clients/__init__.py b/src/jamf_pro_sdk/clients/__init__.py index 31d8829..9a27294 100644 --- a/src/jamf_pro_sdk/clients/__init__.py +++ b/src/jamf_pro_sdk/clients/__init__.py @@ -27,7 +27,7 @@ def __init__( server: str, credentials: CredentialsProvider, port: int = 443, - session_config: SessionConfig = None, + session_config: Optional[SessionConfig] = None, ): """The base client class for interacting with the Jamf Pro APIs. @@ -138,7 +138,7 @@ def classic_api_request( method: str, resource_path: str, data: Optional[Union[str, ClassicApiModel]] = None, - override_headers: dict = None, + override_headers: Optional[dict] = None, ) -> requests.Response: """Perform a request to the Classic API. @@ -196,7 +196,7 @@ def pro_api_request( query_params: Optional[Dict[str, str]] = None, data: Optional[Union[dict, BaseModel]] = None, files: Optional[dict[str, tuple[str, BinaryIO, str]]] = None, - override_headers: Dict[str, str] = None, + override_headers: Optional[Dict[str, str]] = None, ) -> requests.Response: """Perform a request to the Pro API. @@ -269,7 +269,7 @@ def concurrent_api_requests( handler: Callable, arguments: Iterable[Any], return_model: Optional[Type[BaseModel]] = None, - max_concurrency: int = None, + max_concurrency: Optional[int] = None, return_exceptions: Optional[bool] = None, ) -> Iterator[Union[Any, Exception]]: """An interface for performing concurrent API operations. From 0d21db634ed9521b1b2a7808263d5de481698c58 Mon Sep 17 00:00:00 2001 From: Matthew Judy Date: Mon, 4 Nov 2024 17:25:03 -0500 Subject: [PATCH 10/14] Run ``make format``. --- src/jamf_pro_sdk/__init__.py | 1 - src/jamf_pro_sdk/clients/__init__.py | 2 +- src/jamf_pro_sdk/clients/pro_api/__init__.py | 14 ++-- .../clients/pro_api/pagination.py | 2 +- src/jamf_pro_sdk/models/classic/packages.py | 2 +- src/jamf_pro_sdk/models/pro/packages.py | 73 ++++++++++--------- 6 files changed, 47 insertions(+), 47 deletions(-) diff --git a/src/jamf_pro_sdk/__init__.py b/src/jamf_pro_sdk/__init__.py index 13a2a15..56fe92a 100644 --- a/src/jamf_pro_sdk/__init__.py +++ b/src/jamf_pro_sdk/__init__.py @@ -9,7 +9,6 @@ from .helpers import logger_quick_setup from .models.client import SessionConfig - __all__ = [ "__title__", "__version__", diff --git a/src/jamf_pro_sdk/clients/__init__.py b/src/jamf_pro_sdk/clients/__init__.py index 9a27294..01d74e4 100644 --- a/src/jamf_pro_sdk/clients/__init__.py +++ b/src/jamf_pro_sdk/clients/__init__.py @@ -2,7 +2,7 @@ import logging import tempfile from pathlib import Path -from typing import Any, Callable, Dict, Iterable, Iterator, Optional, Type, Union, BinaryIO +from typing import Any, BinaryIO, Callable, Dict, Iterable, Iterator, Optional, Type, Union from urllib.parse import urlunparse import certifi diff --git a/src/jamf_pro_sdk/clients/pro_api/__init__.py b/src/jamf_pro_sdk/clients/pro_api/__init__.py index aa587c3..f74bf79 100644 --- a/src/jamf_pro_sdk/clients/pro_api/__init__.py +++ b/src/jamf_pro_sdk/clients/pro_api/__init__.py @@ -1,23 +1,22 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Optional, Callable, Iterator, List, Union, Literal, overload +from typing import TYPE_CHECKING, Callable, Iterator, List, Literal, Optional, Union, overload from uuid import UUID from ...models.pro.api_options import ( + get_computer_inventory_v1_allowed_filter_fields, get_computer_inventory_v1_allowed_sections, get_computer_inventory_v1_allowed_sort_fields, - get_computer_inventory_v1_allowed_filter_fields, - get_packages_v1_allowed_sort_fields, - get_packages_v1_allowed_filter_fields, get_mdm_commands_v2_allowed_command_types, - get_mdm_commands_v2_allowed_sort_fields, get_mdm_commands_v2_allowed_filter_fields, + get_mdm_commands_v2_allowed_sort_fields, + get_mobile_device_inventory_v2_allowed_filter_fields, get_mobile_device_inventory_v2_allowed_sections, get_mobile_device_inventory_v2_allowed_sort_fields, - get_mobile_device_inventory_v2_allowed_filter_fields, + get_packages_v1_allowed_filter_fields, + get_packages_v1_allowed_sort_fields, ) from ...models.pro.computers import Computer -from ...models.pro.packages import Package from ...models.pro.jcds2 import DownloadUrl, File, NewFile from ...models.pro.mdm import ( CustomCommand, @@ -34,6 +33,7 @@ ShutDownDeviceCommand, ) from ...models.pro.mobile_devices import MobileDevice +from ...models.pro.packages import Package from .pagination import Paginator if TYPE_CHECKING: diff --git a/src/jamf_pro_sdk/clients/pro_api/pagination.py b/src/jamf_pro_sdk/clients/pro_api/pagination.py index dd3d9cd..2ac046c 100644 --- a/src/jamf_pro_sdk/clients/pro_api/pagination.py +++ b/src/jamf_pro_sdk/clients/pro_api/pagination.py @@ -2,7 +2,7 @@ import math from dataclasses import dataclass -from typing import TYPE_CHECKING, Dict, Iterable, Iterator, List, Type, Optional, Union +from typing import TYPE_CHECKING, Dict, Iterable, Iterator, List, Optional, Type, Union from pydantic import BaseModel diff --git a/src/jamf_pro_sdk/models/classic/packages.py b/src/jamf_pro_sdk/models/classic/packages.py index 035d312..195b050 100644 --- a/src/jamf_pro_sdk/models/classic/packages.py +++ b/src/jamf_pro_sdk/models/classic/packages.py @@ -5,7 +5,7 @@ from .. import BaseModel from . import ClassicApiModel -_XML_ARRAY_ITEM_NAMES = {} +_XML_ARRAY_ITEM_NAMES: dict = {} class ClassicPackageItem(BaseModel): diff --git a/src/jamf_pro_sdk/models/pro/packages.py b/src/jamf_pro_sdk/models/pro/packages.py index b90c454..3b38efc 100644 --- a/src/jamf_pro_sdk/models/pro/packages.py +++ b/src/jamf_pro_sdk/models/pro/packages.py @@ -1,44 +1,45 @@ -from pydantic import BaseModel, ConfigDict from typing import Optional +from pydantic import BaseModel, ConfigDict + class Package(BaseModel): """Represents a full package record.""" model_config = ConfigDict(extra="allow") - id : Optional[str] - packageName : str - fileName : str - categoryId : str - info : Optional[str] - notes : Optional[str] - priority : int - osRequirements : Optional[str] - fillUserTemplate : bool - indexed : bool - fillExistingUsers : bool - swu : bool - rebootRequired : bool - selfHealNotify : bool - selfHealingAction : Optional[str] - osInstall : bool - serialNumber : Optional[str] - parentPackageId : Optional[str] - basePath : Optional[str] - suppressUpdates : bool - cloudTransferStatus : str - ignoreConflicts : bool - suppressFromDock : bool - suppressEula : bool - suppressRegistration : bool - installLanguage : Optional[str] - md5 : Optional[str] - sha256 : Optional[str] - hashType : Optional[str] - hashValue : Optional[str] - size : Optional[str] - osInstallerVersion : Optional[str] - manifest : Optional[str] - manifestFileName : Optional[str] - format : Optional[str] + id: Optional[str] + packageName: str + fileName: str + categoryId: str + info: Optional[str] + notes: Optional[str] + priority: int + osRequirements: Optional[str] + fillUserTemplate: bool + indexed: bool + fillExistingUsers: bool + swu: bool + rebootRequired: bool + selfHealNotify: bool + selfHealingAction: Optional[str] + osInstall: bool + serialNumber: Optional[str] + parentPackageId: Optional[str] + basePath: Optional[str] + suppressUpdates: bool + cloudTransferStatus: str + ignoreConflicts: bool + suppressFromDock: bool + suppressEula: bool + suppressRegistration: bool + installLanguage: Optional[str] + md5: Optional[str] + sha256: Optional[str] + hashType: Optional[str] + hashValue: Optional[str] + size: Optional[str] + osInstallerVersion: Optional[str] + manifest: Optional[str] + manifestFileName: Optional[str] + format: Optional[str] From 6894a1612e85f04a9371e985b6dc8b3f57595bf0 Mon Sep 17 00:00:00 2001 From: Matthew Judy Date: Tue, 5 Nov 2024 14:08:35 -0500 Subject: [PATCH 11/14] Add overloads for ``get_mdm_commands_v2()`` and ``get_mobile_device_inventory_v2``. Update ``get_packages_v1()`` documentation to correct entity name. Update ``get_mdm_commands_v2()`` documentation to correct entity name. Insert section heading comments for consistency in ``pro_api/__init__.py`` --- src/jamf_pro_sdk/clients/pro_api/__init__.py | 60 ++++++++++++++++++-- 1 file changed, 55 insertions(+), 5 deletions(-) diff --git a/src/jamf_pro_sdk/clients/pro_api/__init__.py b/src/jamf_pro_sdk/clients/pro_api/__init__.py index f74bf79..9de1da3 100644 --- a/src/jamf_pro_sdk/clients/pro_api/__init__.py +++ b/src/jamf_pro_sdk/clients/pro_api/__init__.py @@ -169,6 +169,8 @@ def get_computer_inventory_v1( return paginator(return_generator=return_generator) + # Package APIs + @overload def get_packages_v1( self, @@ -219,7 +221,7 @@ def get_packages_v1( Allowed sort fields: - .. autoapioptions:: jamf_pro_sdk.models.pro.api_options.get_computer_inventory_v1_allowed_sort_fields + .. autoapioptions:: jamf_pro_sdk.models.pro.api_options.get_packages_v1_allowed_sort_fields :type sort_expression: SortExpression @@ -228,7 +230,7 @@ def get_packages_v1( Allowed filter fields: - .. autoapioptions:: jamf_pro_sdk.models.pro.api_options.get_computer_inventory_v1_allowed_filter_fields + .. autoapioptions:: jamf_pro_sdk.models.pro.api_options.get_packages_v1_allowed_filter_fields :type filter_expression: FilterExpression @@ -236,8 +238,8 @@ def get_packages_v1( default, the results for all pages will be returned in a single response. :type return_generator: bool - :return: List of computers OR a paginator generator. - :rtype: List[~jamf_pro_sdk.models.pro.computer.Computer] | Iterator[Page] + :return: List of packages OR a paginator generator. + :rtype: List[~jamf_pro_sdk.models.pro.packages.package] | Iterator[Page] """ if sort_expression: @@ -383,6 +385,28 @@ def send_mdm_command_preview( resp = self.api_request(method="post", resource_path="preview/mdm/commands", data=data) return [SendMdmCommandResponse(**i) for i in resp.json()] + @overload + def get_mdm_commands_v2( + self, + filter_expression: FilterExpression, + start_page: int = ..., + end_page: Optional[int] = ..., + page_size: int = ..., + sort_expression: Optional[SortExpression] = ..., + return_generator: Literal[False] = False, + ) -> List[MdmCommandStatus]: ... + + @overload + def get_mdm_commands_v2( + self, + filter_expression: FilterExpression, + start_page: int = ..., + end_page: Optional[int] = ..., + page_size: int = ..., + sort_expression: Optional[SortExpression] = ..., + return_generator: Literal[True] = True, + ) -> Iterator[Page]: ... + def get_mdm_commands_v2( self, filter_expression: FilterExpression, @@ -429,7 +453,7 @@ def get_mdm_commands_v2( the results for all pages will be returned in a single response. :type return_generator: bool - :return: List of computers OR a paginator generator. + :return: List of MDM commands OR a paginator generator. :rtype: List[~jamf_pro_sdk.models.pro.mdm.MdmCommand] | Iterator[Page] """ @@ -460,6 +484,32 @@ def get_mdm_commands_v2( return paginator(return_generator=return_generator) + # Mobile Device Inventory APIs + + @overload + def get_mobile_device_inventory_v2( + self, + sections: Optional[List[str]] = ..., + start_page: int = ..., + end_page: Optional[int] = ..., + page_size: int = ..., + sort_expression: Optional[SortExpression] = ..., + filter_expression: Optional[FilterExpression] = ..., + return_generator: Literal[False] = False, + ) -> List[MobileDevice]: ... + + @overload + def get_mobile_device_inventory_v2( + self, + sections: Optional[List[str]] = ..., + start_page: int = ..., + end_page: Optional[int] = ..., + page_size: int = ..., + sort_expression: Optional[SortExpression] = ..., + filter_expression: Optional[FilterExpression] = ..., + return_generator: Literal[True] = True, + ) -> Iterator[Page]: ... + def get_mobile_device_inventory_v2( self, sections: Optional[List[str]] = None, From 650af61c854ffb00b9a0e8c190254e56064619d3 Mon Sep 17 00:00:00 2001 From: Matthew Judy Date: Tue, 5 Nov 2024 14:34:27 -0500 Subject: [PATCH 12/14] Update contribution guide to specify the use of ``@overload`` and ``Optional[]`` type hinting. --- docs/contributors/index.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/contributors/index.rst b/docs/contributors/index.rst index 8717356..bb9cc2f 100644 --- a/docs/contributors/index.rst +++ b/docs/contributors/index.rst @@ -96,6 +96,8 @@ Any Jamf Pro API added to the clients must have the following elements code comp * The API method has been added to the appropriate client, has a complete docstring, and has an interface in-line with other methods of that client. * The API must have matching and complete Pydantic models. +* Provide ``@overload`` interfaces for API methods with dynamic return types (e.g. ``Union[List[Computer], Iterator[Page]]``) which be determined from the value of an argument. +* If the value of an argument or variable can be ``None``, inform the type checker by wrapping its type with ``Optional[]``. * Unless your code is covered by another automated test you will need to add tests to ensure coverage. The SDK references in the documentation automatically include all public method on the clients and no documentation changes may be required as a part of the contribution. From e30715603d8c2433e5d2cabd9d5c5e252d30117f Mon Sep 17 00:00:00 2001 From: Matthew Judy Date: Wed, 6 Nov 2024 18:34:52 -0500 Subject: [PATCH 13/14] Clarify ``Optional`` use case. --- docs/contributors/index.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/contributors/index.rst b/docs/contributors/index.rst index bb9cc2f..e47879b 100644 --- a/docs/contributors/index.rst +++ b/docs/contributors/index.rst @@ -96,8 +96,8 @@ Any Jamf Pro API added to the clients must have the following elements code comp * The API method has been added to the appropriate client, has a complete docstring, and has an interface in-line with other methods of that client. * The API must have matching and complete Pydantic models. -* Provide ``@overload`` interfaces for API methods with dynamic return types (e.g. ``Union[List[Computer], Iterator[Page]]``) which be determined from the value of an argument. -* If the value of an argument or variable can be ``None``, inform the type checker by wrapping its type with ``Optional[]``. +* Provide ``@overload`` interfaces for API methods with dynamic return types (e.g. ``Union[list[Computer], Iterator[Page]]``) which be determined from the value of an argument. +* If the value of a variable, method argument, or return parameter can be ``None``, inform the type checker by wrapping its type with ``Optional[]``, e.g. ``description: Optional[str] = None`` * Unless your code is covered by another automated test you will need to add tests to ensure coverage. The SDK references in the documentation automatically include all public method on the clients and no documentation changes may be required as a part of the contribution. From 33e09452677e53838113a5905578be34d4fe1a5e Mon Sep 17 00:00:00 2001 From: Matthew Judy Date: Wed, 6 Nov 2024 18:37:24 -0500 Subject: [PATCH 14/14] Import custom ``BaseModel`` instead of Pydantic default. --- src/jamf_pro_sdk/models/pro/packages.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/jamf_pro_sdk/models/pro/packages.py b/src/jamf_pro_sdk/models/pro/packages.py index 3b38efc..be0b99f 100644 --- a/src/jamf_pro_sdk/models/pro/packages.py +++ b/src/jamf_pro_sdk/models/pro/packages.py @@ -1,6 +1,8 @@ from typing import Optional -from pydantic import BaseModel, ConfigDict +from pydantic import ConfigDict + +from .. import BaseModel class Package(BaseModel):