diff --git a/docs/contributors/index.rst b/docs/contributors/index.rst index 8717356..e47879b 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 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. 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 ----- 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..56fe92a 100644 --- a/src/jamf_pro_sdk/__init__.py +++ b/src/jamf_pro_sdk/__init__.py @@ -8,3 +8,15 @@ ) 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/clients/__init__.py b/src/jamf_pro_sdk/clients/__init__.py index daa07de..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 +from typing import Any, BinaryIO, Callable, Dict, Iterable, Iterator, Optional, Type, Union from urllib.parse import urlunparse import certifi @@ -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. @@ -195,7 +195,8 @@ def pro_api_request( resource_path: str, query_params: Optional[Dict[str, str]] = None, data: Optional[Union[dict, BaseModel]] = None, - override_headers: Dict[str, str] = None, + files: Optional[dict[str, tuple[str, BinaryIO, str]]] = None, + override_headers: Optional[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: @@ -261,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. diff --git a/src/jamf_pro_sdk/clients/pro_api/__init__.py b/src/jamf_pro_sdk/clients/pro_api/__init__.py index 5c70a02..9de1da3 100644 --- a/src/jamf_pro_sdk/clients/pro_api/__init__.py +++ b/src/jamf_pro_sdk/clients/pro_api/__init__.py @@ -1,9 +1,21 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Callable, Iterator, List, Union +from typing import TYPE_CHECKING, Callable, Iterator, List, Literal, Optional, Union, overload from uuid import UUID -from ...models.pro.api_options import * # noqa: F403 +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_mdm_commands_v2_allowed_command_types, + 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_packages_v1_allowed_filter_fields, + get_packages_v1_allowed_sort_fields, +) from ...models.pro.computers import Computer from ...models.pro.jcds2 import DownloadUrl, File, NewFile from ...models.pro.mdm import ( @@ -21,6 +33,7 @@ ShutDownDeviceCommand, ) from ...models.pro.mobile_devices import MobileDevice +from ...models.pro.packages import Package from .pagination import Paginator if TYPE_CHECKING: @@ -42,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: 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: 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[Computer], Iterator[Page]]: """Returns a list of computer inventory records. @@ -132,6 +169,98 @@ def get_computer_inventory_v1( return paginator(return_generator=return_generator) + # Package APIs + + @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_packages_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_packages_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 packages OR a paginator generator. + :rtype: List[~jamf_pro_sdk.models.pro.packages.package] | 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, + ) + + return paginator(return_generator=return_generator) + # JCDS APIs def get_jcds_files_v1(self) -> List[File]: @@ -256,13 +385,35 @@ 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, 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. @@ -302,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] """ @@ -333,14 +484,40 @@ 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: 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..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, Union +from typing import TYPE_CHECKING, Dict, Iterable, Iterator, List, Optional, Type, 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/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/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/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", 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): 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..be0b99f --- /dev/null +++ b/src/jamf_pro_sdk/models/pro/packages.py @@ -0,0 +1,47 @@ +from typing import Optional + +from pydantic import ConfigDict + +from .. import BaseModel + + +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] diff --git a/src/jamf_pro_sdk/py.typed b/src/jamf_pro_sdk/py.typed new file mode 100644 index 0000000..e69de29 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)