From 90aa4f357de4fbe1f329403013a9a869df2a7852 Mon Sep 17 00:00:00 2001 From: Bryson Tyrrell Date: Fri, 29 Dec 2023 11:34:35 -0600 Subject: [PATCH 01/19] Pydantic 1 -> 2 Update all classic computer models. Classic computer model tests pass. --- pyproject.toml | 6 +- src/jamf_pro_sdk/clients/classic_api.py | 1 - src/jamf_pro_sdk/models/classic/__init__.py | 85 ++-- src/jamf_pro_sdk/models/classic/computers.py | 479 ++++++++++-------- tests/models/test_models_classic_computers.py | 14 +- 5 files changed, 321 insertions(+), 264 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 84c7323..645c815 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,12 +2,12 @@ name = "jamf-pro-sdk" dynamic = ["readme", "version"] description = "Jamf Pro SDK for Python" -keywords = ["jamf", "jamf pro", "jss", "jps"] +keywords = ["jamf", "pro", "jss", "jps"] license = {text = "MIT"} requires-python = ">=3.9, <4" dependencies = [ "requests>=2.28.1,<3", - "pydantic>=1.10.4,<2", + "pydantic>=2,<3", "dicttoxml>=1.7.16,<2", "defusedxml" ] @@ -43,7 +43,7 @@ dev = [ "boto3>=1.26.45,<2", "keyring>=23.13.1", "polyfactory>=2.1.1,<3", - "black", + "black[d]", "ruff", "coverage[toml]", "pytest >= 6", diff --git a/src/jamf_pro_sdk/clients/classic_api.py b/src/jamf_pro_sdk/clients/classic_api.py index 2ebf9e7..1c2c04e 100644 --- a/src/jamf_pro_sdk/clients/classic_api.py +++ b/src/jamf_pro_sdk/clients/classic_api.py @@ -25,7 +25,6 @@ if TYPE_CHECKING: import requests - VALID_COMPUTER_SUBSETS = ( "general", "location", diff --git a/src/jamf_pro_sdk/models/classic/__init__.py b/src/jamf_pro_sdk/models/classic/__init__.py index 2156dea..0decdb1 100644 --- a/src/jamf_pro_sdk/models/classic/__init__.py +++ b/src/jamf_pro_sdk/models/classic/__init__.py @@ -2,7 +2,7 @@ from typing import Any, Dict, Iterable, Optional, Set import dicttoxml -from pydantic import Extra +from pydantic import ConfigDict from .. import BaseModel @@ -52,6 +52,10 @@ def remove_fields(data: Any, values_to_remove: Iterable = None): class ClassicApiModel(BaseModel): """The base model used for Classic API models.""" + model_config = ConfigDict( + extra="allow", json_encoders={datetime: convert_datetime_to_jamf_iso} + ) + _xml_root_name: str _xml_array_item_names: Dict[str, str] _xml_write_fields: Optional[Set[str]] = None @@ -69,7 +73,7 @@ def xml(self, exclude_none: bool = True, exclude_read_only: bool = False) -> str :rtype: str """ data = remove_fields( - self.dict( + self.model_dump( include=self._xml_write_fields if exclude_read_only else None, exclude_none=exclude_none, ) @@ -83,57 +87,54 @@ def xml(self, exclude_none: bool = True, exclude_read_only: bool = False) -> str return_bytes=False, ) - class Config: - extra = Extra.allow - json_encoders = { - # custom output conversion for datetime - datetime: convert_datetime_to_jamf_iso - } - class ClassicDeviceLocation(BaseModel): """Device user assignment information.""" - username: Optional[str] - realname: Optional[str] - real_name: Optional[str] - email_address: Optional[str] - position: Optional[str] - phone: Optional[str] - phone_number: Optional[str] - department: Optional[str] - building: Optional[str] - room: Optional[str] + model_config = ConfigDict(extra="allow") + + username: Optional[str] = None + realname: Optional[str] = None + real_name: Optional[str] = None + email_address: Optional[str] = None + position: Optional[str] = None + phone: Optional[str] = None + phone_number: Optional[str] = None + department: Optional[str] = None + building: Optional[str] = None + room: Optional[str] = None class ClassicDevicePurchasing(BaseModel): """Device purchase information (normally populated by GSX).""" - is_purchased: Optional[bool] - is_leased: Optional[bool] - po_number: Optional[str] - vendor: Optional[str] - applecare_id: Optional[str] - purchase_price: Optional[str] - purchasing_account: Optional[str] - po_date: Optional[str] - po_date_epoch: Optional[int] - po_date_utc: Optional[str] - warranty_expires: Optional[str] - warranty_expires_epoch: Optional[int] - warranty_expires_utc: Optional[str] - lease_expires: Optional[str] - lease_expires_epoch: Optional[int] - lease_expires_utc: Optional[str] - life_expectancy: Optional[int] - purchasing_contact: Optional[str] - os_applecare_id: Optional[str] - os_maintenance_expires: Optional[str] - attachments: Optional[list] # Deprecated? + model_config = ConfigDict(extra="allow") + + is_purchased: Optional[bool] = None + is_leased: Optional[bool] = None + po_number: Optional[str] = None + vendor: Optional[str] = None + applecare_id: Optional[str] = None + purchase_price: Optional[str] = None + purchasing_account: Optional[str] = None + po_date: Optional[str] = None + po_date_epoch: Optional[int] = None + po_date_utc: Optional[str] = None + warranty_expires: Optional[str] = None + warranty_expires_epoch: Optional[int] = None + warranty_expires_utc: Optional[str] = None + lease_expires: Optional[str] = None + lease_expires_epoch: Optional[int] = None + lease_expires_utc: Optional[str] = None + life_expectancy: Optional[int] = None + purchasing_contact: Optional[str] = None + os_applecare_id: Optional[str] = None + os_maintenance_expires: Optional[str] = None + attachments: Optional[list] = None # Deprecated? class ClassicSite(BaseModel): """Site assignment information.""" - id: Optional[int] - name: Optional[str] + id: Optional[int] = None + name: Optional[str] = None diff --git a/src/jamf_pro_sdk/models/classic/computers.py b/src/jamf_pro_sdk/models/classic/computers.py index f83cddd..e5f6c55 100644 --- a/src/jamf_pro_sdk/models/classic/computers.py +++ b/src/jamf_pro_sdk/models/classic/computers.py @@ -1,10 +1,15 @@ from datetime import datetime from typing import Any, List, Optional, Union -from pydantic import Extra, Field +from pydantic import ConfigDict, Field from .. import BaseModel -from . import ClassicApiModel, ClassicDeviceLocation, ClassicDevicePurchasing, ClassicSite +from . import ( + ClassicApiModel, + ClassicDeviceLocation, + ClassicDevicePurchasing, + ClassicSite, +) _XML_ARRAY_ITEM_NAMES = { # Computer @@ -36,273 +41,309 @@ # Computer.General Models -class ClassicComputerGeneralRemoteManagement(BaseModel, extra=Extra.allow): +class ClassicComputerGeneralRemoteManagement(BaseModel): """Computer nested model: computer.general.remote_management - :class:`str` management_password: This attribute is only used in POST/PUT operations - :class:`str` management_password_sha256: This attribute is read-only """ - managed: Optional[bool] - management_username: Optional[str] - management_password: Optional[str] - management_password_sha256: Optional[str] + model_config = ConfigDict(extra="allow") + managed: Optional[bool] = None + management_username: Optional[str] = None + management_password: Optional[str] = None + management_password_sha256: Optional[str] = None -class ClassicComputerGeneralMdmCapableUsers(BaseModel, extra=Extra.allow): + +class ClassicComputerGeneralMdmCapableUsers(BaseModel): """Computer nested model: computer.general.mdm_capable_users""" - mdm_capable_user: Optional[str] + model_config = ConfigDict(extra="allow") + + mdm_capable_user: Optional[str] = None -class ClassicComputerGeneralManagementStatus(BaseModel, extra=Extra.allow): +class ClassicComputerGeneralManagementStatus(BaseModel): """Computer nested model: computer.general.management_status""" - enrolled_via_dep: Optional[bool] - user_approved_enrollment: Optional[bool] - user_approved_mdm: Optional[bool] + model_config = ConfigDict(extra="allow") + + enrolled_via_dep: Optional[bool] = None + user_approved_enrollment: Optional[bool] = None + user_approved_mdm: Optional[bool] = None -class ClassicComputerGeneral(BaseModel, extra=Extra.allow): +class ClassicComputerGeneral(BaseModel): """Computer nested model: computer.general""" - id: Optional[int] - name: Optional[str] - mac_address: Optional[str] - network_adapter_type: Optional[str] - alt_mac_address: Optional[str] - alt_network_adapter_type: Optional[str] - ip_address: Optional[str] - last_reported_ip: Optional[str] - serial_number: Optional[str] - udid: Optional[str] - jamf_version: Optional[str] - platform: Optional[str] - barcode_1: Optional[str] - barcode_2: Optional[str] - asset_tag: Optional[str] - remote_management: Optional[ClassicComputerGeneralRemoteManagement] - supervised: Optional[bool] - mdm_capable: Optional[bool] - mdm_capable_users: Optional[Union[dict, ClassicComputerGeneralMdmCapableUsers]] - management_status: Optional[ClassicComputerGeneralManagementStatus] - report_date: Optional[str] - report_date_epoch: Optional[int] - report_date_utc: Union[Optional[datetime], Optional[str]] - last_contact_time: Optional[str] - last_contact_time_epoch: Optional[int] - last_contact_time_utc: Optional[str] - initial_entry_date: Optional[str] - initial_entry_date_epoch: Optional[int] - initial_entry_date_utc: Union[Optional[datetime], Optional[str]] - last_cloud_backup_date_epoch: Optional[int] - last_cloud_backup_date_utc: Union[Optional[datetime], Optional[str]] - last_enrolled_date_epoch: Optional[int] - last_enrolled_date_utc: Union[Optional[datetime], Optional[str]] - mdm_profile_expiration_epoch: Optional[int] - mdm_profile_expiration_utc: Union[Optional[datetime], Optional[str]] - distribution_point: Optional[str] - sus: Optional[str] - site: Optional[ClassicSite] - itunes_store_account_is_active: Optional[bool] + model_config = ConfigDict(extra="allow") + + id: Optional[int] = None + name: Optional[str] = None + mac_address: Optional[str] = None + network_adapter_type: Optional[str] = None + alt_mac_address: Optional[str] = None + alt_network_adapter_type: Optional[str] = None + ip_address: Optional[str] = None + last_reported_ip: Optional[str] = None + serial_number: Optional[str] = None + udid: Optional[str] = None + jamf_version: Optional[str] = None + platform: Optional[str] = None + barcode_1: Optional[str] = None + barcode_2: Optional[str] = None + asset_tag: Optional[str] = None + remote_management: Optional[ClassicComputerGeneralRemoteManagement] = None + supervised: Optional[bool] = None + mdm_capable: Optional[bool] = None + mdm_capable_users: Optional[ + Union[dict, ClassicComputerGeneralMdmCapableUsers] + ] = None + management_status: Optional[ClassicComputerGeneralManagementStatus] = None + report_date: Optional[str] = None + report_date_epoch: Optional[int] = None + report_date_utc: Union[Optional[datetime], Optional[str]] = None + last_contact_time: Optional[str] = None + last_contact_time_epoch: Optional[int] = None + last_contact_time_utc: Optional[str] = None + initial_entry_date: Optional[str] = None + initial_entry_date_epoch: Optional[int] = None + initial_entry_date_utc: Union[Optional[datetime], Optional[str]] = None + last_cloud_backup_date_epoch: Optional[int] = None + last_cloud_backup_date_utc: Union[Optional[datetime], Optional[str]] = None + last_enrolled_date_epoch: Optional[int] = None + last_enrolled_date_utc: Union[Optional[datetime], Optional[str]] = None + mdm_profile_expiration_epoch: Optional[int] = None + mdm_profile_expiration_utc: Union[Optional[datetime], Optional[str]] = None + distribution_point: Optional[str] = None + sus: Optional[str] = None + site: Optional[ClassicSite] = None + itunes_store_account_is_active: Optional[bool] = None # Computer.Hardware Models -class ClassicComputerHardwareStorageDevicePartition(BaseModel, extra=Extra.allow): +class ClassicComputerHardwareStorageDevicePartition(BaseModel): """Computer nested model: computer.hardware.storage.partitions""" - name: Optional[str] - size: Optional[int] - type: Optional[str] - partition_capacity_mb: Optional[int] - percentage_full: Optional[int] - available_mb: Optional[int] - filevault_status: Optional[str] - filevault_percent: Optional[int] - filevault2_status: Optional[str] - filevault2_percent: Optional[int] - boot_drive_available_mb: Optional[int] - lvgUUID: Optional[str] - lvUUID: Optional[str] - pvUUID: Optional[str] - - -class ClassicComputerHardwareStorageDevice(BaseModel, extra=Extra.allow): + model_config = ConfigDict(extra="allow") + + name: Optional[str] = None + size: Optional[int] = None + type: Optional[str] = None + partition_capacity_mb: Optional[int] = None + percentage_full: Optional[int] = None + available_mb: Optional[int] = None + filevault_status: Optional[str] = None + filevault_percent: Optional[int] = None + filevault2_status: Optional[str] = None + filevault2_percent: Optional[int] = None + boot_drive_available_mb: Optional[int] = None + lvgUUID: Optional[str] = None + lvUUID: Optional[str] = None + pvUUID: Optional[str] = None + + +class ClassicComputerHardwareStorageDevice(BaseModel): """Computer nested model: computer.hardware.storage""" - disk: Optional[str] - model: Optional[str] - revision: Optional[str] - serial_number: Optional[str] - size: Optional[int] - drive_capacity_mb: Optional[int] - connection_type: Optional[str] - smart_status: Optional[str] - partitions: Optional[List[ClassicComputerHardwareStorageDevicePartition]] + model_config = ConfigDict(extra="allow") + disk: Optional[str] = None + model: Optional[str] = None + revision: Optional[str] = None + serial_number: Optional[str] = None + size: Optional[int] = None + drive_capacity_mb: Optional[int] = None + connection_type: Optional[str] = None + smart_status: Optional[str] = None + partitions: Optional[List[ClassicComputerHardwareStorageDevicePartition]] = None -class ClassicComputerHardwareMappedPrinter(BaseModel, extra=Extra.allow): + +class ClassicComputerHardwareMappedPrinter(BaseModel): """Computer nested model: computer.hardware.mapped_printers""" - name: Optional[str] - uri: Optional[str] - type: Optional[str] - location: Optional[str] + model_config = ConfigDict(extra="allow") + + name: Optional[str] = None + uri: Optional[str] = None + type: Optional[str] = None + location: Optional[str] = None class ClassicComputerHardware(ClassicApiModel): """Computer nested model: computer.hardware""" + model_config = ConfigDict(extra="allow") + _xml_root_name = "hardware" _xml_array_item_names = _XML_ARRAY_ITEM_NAMES - make: Optional[str] - model: Optional[str] - model_identifier: Optional[str] - os_name: Optional[str] - os_version: Optional[str] - os_build: Optional[str] - software_update_device_id: Optional[str] - active_directory_status: Optional[str] - service_pack: Optional[str] - processor_type: Optional[str] - is_apple_silicon: Optional[bool] - processor_architecture: Optional[str] - processor_speed: Optional[int] - processor_speed_mhz: Optional[int] - number_processors: Optional[int] - number_cores: Optional[int] - total_ram: Optional[int] - total_ram_mb: Optional[int] - boot_rom: Optional[str] - bus_speed: Optional[int] - bus_speed_mhz: Optional[int] - battery_capacity: Optional[int] - cache_size: Optional[int] - cache_size_kb: Optional[int] - available_ram_slots: Optional[int] - optical_drive: Optional[str] - nic_speed: Optional[str] - smc_version: Optional[str] - ble_capable: Optional[bool] - supports_ios_app_installs: Optional[bool] - sip_status: Optional[str] - gatekeeper_status: Optional[str] - xprotect_version: Optional[str] - institutional_recovery_key: Optional[str] - disk_encryption_configuration: Optional[str] - filevault2_users: Optional[List[str]] - storage: Optional[List[ClassicComputerHardwareStorageDevice]] - mapped_printers: Optional[List[ClassicComputerHardwareMappedPrinter]] + make: Optional[str] = None + model: Optional[str] = None + model_identifier: Optional[str] = None + os_name: Optional[str] = None + os_version: Optional[str] = None + os_build: Optional[str] = None + software_update_device_id: Optional[str] = None + active_directory_status: Optional[str] = None + service_pack: Optional[str] = None + processor_type: Optional[str] = None + is_apple_silicon: Optional[bool] = None + processor_architecture: Optional[str] = None + processor_speed: Optional[int] = None + processor_speed_mhz: Optional[int] = None + number_processors: Optional[int] = None + number_cores: Optional[int] = None + total_ram: Optional[int] = None + total_ram_mb: Optional[int] = None + boot_rom: Optional[str] = None + bus_speed: Optional[int] = None + bus_speed_mhz: Optional[int] = None + battery_capacity: Optional[int] = None + cache_size: Optional[int] = None + cache_size_kb: Optional[int] = None + available_ram_slots: Optional[int] = None + optical_drive: Optional[str] = None + nic_speed: Optional[str] = None + smc_version: Optional[str] = None + ble_capable: Optional[bool] = None + supports_ios_app_installs: Optional[bool] = None + sip_status: Optional[str] = None + gatekeeper_status: Optional[str] = None + xprotect_version: Optional[str] = None + institutional_recovery_key: Optional[str] = None + disk_encryption_configuration: Optional[str] = None + filevault2_users: Optional[List[str]] = None + storage: Optional[List[ClassicComputerHardwareStorageDevice]] = None + mapped_printers: Optional[List[ClassicComputerHardwareMappedPrinter]] = None # Computer.Certificate Models -class ClassicComputerCertificate(BaseModel, extra=Extra.allow): +class ClassicComputerCertificate(BaseModel): """Computer nested model: computer.certificates""" - common_name: Optional[str] - identity: Optional[bool] - expires_utc: Optional[str] - expires_epoch: Optional[int] - name: Optional[str] + model_config = ConfigDict(extra="allow") + + common_name: Optional[str] = None + identity: Optional[bool] = None + expires_utc: Optional[str] = None + expires_epoch: Optional[int] = None + name: Optional[str] = None # Computer.Security Models -class ClassicComputerSecurity(BaseModel, extra=Extra.allow): +class ClassicComputerSecurity(BaseModel): """Computer nested model: computer.security""" - activation_lock: Optional[bool] - recovery_lock_enabled: Optional[bool] - secure_boot_level: Optional[str] - external_boot_level: Optional[str] - firewall_enabled: Optional[bool] + model_config = ConfigDict(extra="allow") + + activation_lock: Optional[bool] = None + recovery_lock_enabled: Optional[bool] = None + secure_boot_level: Optional[str] = None + external_boot_level: Optional[str] = None + firewall_enabled: Optional[bool] = None # Computer.Software Models -class ClassicComputerSoftwareAvailableUpdate(BaseModel, extra=Extra.allow): +class ClassicComputerSoftwareAvailableUpdate(BaseModel): """Computer nested model: computer.software.available_updates""" - name: Optional[str] - package_name: Optional[str] - version: Optional[str] + model_config = ConfigDict(extra="allow") + + name: Optional[str] = None + package_name: Optional[str] = None + version: Optional[str] = None -class ClassicComputerSoftwareItem(BaseModel, extra=Extra.allow): +class ClassicComputerSoftwareItem(BaseModel): """Computer nested model: computer.software.applications, computer.software.fonts, computer.software.plugins """ - name: Optional[str] - path: Optional[str] - version: Optional[str] - bundle_id: Optional[str] + model_config = ConfigDict(extra="allow") + name: Optional[str] = None + path: Optional[str] = None + version: Optional[str] = None + bundle_id: Optional[str] = None -class ClassicComputerSoftware(BaseModel, extra=Extra.allow): # Lots of assumptions in this object + +class ClassicComputerSoftware(BaseModel): # Lots of assumptions in this object """Computer nested model: computer.software""" - unix_executables: Optional[List[str]] - licensed_software: Optional[List[str]] - installed_by_casper: Optional[List[str]] - installed_by_installer_swu: Optional[List[str]] - cached_by_casper: Optional[List[str]] - available_software_updates: Optional[List[str]] - available_updates: Union[Optional[List[ClassicComputerSoftwareAvailableUpdate]], Optional[dict]] - running_services: Optional[List[str]] - applications: Optional[List[ClassicComputerSoftwareItem]] - fonts: Optional[List[ClassicComputerSoftwareItem]] - plugins: Optional[List[ClassicComputerSoftwareItem]] + model_config = ConfigDict(extra="allow") + + unix_executables: Optional[List[str]] = None + licensed_software: Optional[List[str]] = None + installed_by_casper: Optional[List[str]] = None + installed_by_installer_swu: Optional[List[str]] = None + cached_by_casper: Optional[List[str]] = None + available_software_updates: Optional[List[str]] = None + available_updates: Union[ + Optional[List[ClassicComputerSoftwareAvailableUpdate]], Optional[dict] + ] = None + running_services: Optional[List[str]] = None + applications: Optional[List[ClassicComputerSoftwareItem]] = None + fonts: Optional[List[ClassicComputerSoftwareItem]] = None + plugins: Optional[List[ClassicComputerSoftwareItem]] = None # Computer.ExtensionAttributes Models -class ClassicComputerExtensionAttribute(BaseModel, extra=Extra.allow): +class ClassicComputerExtensionAttribute(BaseModel): """Computer nested model: computer.extension_attributes""" - id: Optional[int] - name: Optional[str] - type: Optional[str] - multi_value: Optional[bool] - value: Optional[str] + model_config = ConfigDict(extra="allow") + + id: Optional[int] = None + name: Optional[str] = None + type: Optional[str] = None + multi_value: Optional[bool] = None + value: Optional[str] = None # Computer GroupsAccounts Models -class ClassicComputerGroupsAccountsLocalAccount(BaseModel, extra=Extra.allow): +class ClassicComputerGroupsAccountsLocalAccount(BaseModel): """Computer nested model: computer.groups_accounts.local_accounts""" - name: Optional[str] - realname: Optional[str] - uid: Optional[str] - home: Optional[str] - home_size: Optional[str] - home_size_mb: Optional[int] - administrator: Optional[bool] - filevault_enabled: Optional[bool] + model_config = ConfigDict(extra="allow") + + name: Optional[str] = None + realname: Optional[str] = None + uid: Optional[str] = None + home: Optional[str] = None + home_size: Optional[str] = None + home_size_mb: Optional[int] = None + administrator: Optional[bool] = None + filevault_enabled: Optional[bool] = None -class ClassicComputerGroupsAccountsUserInventoriesUser(BaseModel, extra=Extra.allow): +class ClassicComputerGroupsAccountsUserInventoriesUser(BaseModel): """Computer nested model: computer.groups_accounts.user_inventories.user""" - username: Optional[str] - password_history_depth: Optional[str] - password_min_length: Optional[str] - password_max_age: Optional[str] - password_min_complex_characters: Optional[str] - password_require_alphanumeric: Optional[str] + model_config = ConfigDict(extra="allow") + + username: Optional[str] = None + password_history_depth: Optional[str] = None + password_min_length: Optional[str] = None + password_max_age: Optional[str] = None + password_min_complex_characters: Optional[str] = None + password_require_alphanumeric: Optional[str] = None -class ClassicComputerGroupsAccountsUserInventories(BaseModel, extra=Extra.allow): +class ClassicComputerGroupsAccountsUserInventories(BaseModel): """Computer nested model: computer.groups_accounts.user_inventories There is a bug with this API resource! @@ -328,37 +369,43 @@ class ClassicComputerGroupsAccountsUserInventories(BaseModel, extra=Extra.allow) TODO: Accurate data can only be obtained using an XML response """ - disable_automatic_login: Optional[bool] + model_config = ConfigDict(extra="allow") + + disable_automatic_login: Optional[bool] = None user: Union[ Optional[ClassicComputerGroupsAccountsUserInventoriesUser], Optional[List[ClassicComputerGroupsAccountsUserInventoriesUser]], - ] + ] = None -class ClassicComputerGroupsAccounts(BaseModel, extra=Extra.allow): +class ClassicComputerGroupsAccounts(BaseModel): """Computer nested model: computer.groups_accounts""" - computer_group_memberships: Optional[List[str]] - local_accounts: Optional[List[ClassicComputerGroupsAccountsLocalAccount]] - user_inventories: Optional[ClassicComputerGroupsAccountsUserInventories] + model_config = ConfigDict(extra="allow") + + computer_group_memberships: Optional[List[str]] = None + local_accounts: Optional[List[ClassicComputerGroupsAccountsLocalAccount]] = None + user_inventories: Optional[ClassicComputerGroupsAccountsUserInventories] = None # Computer.ConfigurationProfiles Models -class ClassicComputerConfigurationProfile(BaseModel, extra=Extra.allow): +class ClassicComputerConfigurationProfile(BaseModel): """Computer nested model: computer.configuration_profiles""" - id: Optional[int] - name: Optional[str] - uuid: Optional[str] - is_removable: Optional[bool] + model_config = ConfigDict(extra="allow") + + id: Optional[int] = None + name: Optional[str] = None + uuid: Optional[str] = None + is_removable: Optional[bool] = None # Computer Models -class ClassicComputersItem(BaseModel, extra=Extra.allow): +class ClassicComputersItem(BaseModel): """Represents a computer record returned by the :meth:`~jamf_pro_sdk.clients.classic_api.ClassicApi.list_computers` operation. @@ -366,18 +413,20 @@ class ClassicComputersItem(BaseModel, extra=Extra.allow): populated. """ + model_config = ConfigDict(extra="allow") + id: int name: str - managed: Optional[bool] - username: Optional[str] - model: Optional[str] - department: Optional[str] - building: Optional[str] - mac_address: Optional[str] - udid: Optional[str] - serial_number: Optional[str] - report_date_utc: Union[Optional[datetime], Optional[str]] - report_date_epoch: Optional[int] + managed: Optional[bool] = None + username: Optional[str] = None + model: Optional[str] = None + department: Optional[str] = None + building: Optional[str] = None + mac_address: Optional[str] = None + udid: Optional[str] = None + serial_number: Optional[str] = None + report_date_utc: Union[Optional[datetime], Optional[str]] = None + report_date_epoch: Optional[int] = None class ClassicComputer(ClassicApiModel): @@ -390,23 +439,29 @@ class ClassicComputer(ClassicApiModel): operation. """ + model_config = ConfigDict(extra="allow") + _xml_root_name = "computer" _xml_array_item_names = _XML_ARRAY_ITEM_NAMES _xml_write_fields = {"general", "location", "extension_attributes"} - general: Optional[ClassicComputerGeneral] = Field(default_factory=ClassicComputerGeneral) - location: Optional[ClassicDeviceLocation] = Field(default_factory=ClassicDeviceLocation) - purchasing: Optional[ClassicDevicePurchasing] + general: Optional[ClassicComputerGeneral] = Field( + default_factory=ClassicComputerGeneral + ) + location: Optional[ClassicDeviceLocation] = Field( + default_factory=ClassicDeviceLocation + ) + purchasing: Optional[ClassicDevicePurchasing] = None # Peripherals are a deprecated feature of Jamf Pro - peripherals: Optional[Any] - hardware: Optional[ClassicComputerHardware] - certificates: Optional[List[ClassicComputerCertificate]] - security: Optional[ClassicComputerSecurity] - software: Optional[ClassicComputerSoftware] + peripherals: Optional[Any] = None + hardware: Optional[ClassicComputerHardware] = None + certificates: Optional[List[ClassicComputerCertificate]] = None + security: Optional[ClassicComputerSecurity] = None + software: Optional[ClassicComputerSoftware] = None extension_attributes: Optional[List[ClassicComputerExtensionAttribute]] = Field( default_factory=list ) - groups_accounts: Optional[ClassicComputerGroupsAccounts] + groups_accounts: Optional[ClassicComputerGroupsAccounts] = None # iPhones in Computer inventory is a deprecated feature of Jamf Pro - iphones: Optional[Any] - configuration_profiles: Optional[List[ClassicComputerConfigurationProfile]] + iphones: Optional[Any] = None + configuration_profiles: Optional[List[ClassicComputerConfigurationProfile]] = None diff --git a/tests/models/test_models_classic_computers.py b/tests/models/test_models_classic_computers.py index fa950f1..13afec6 100644 --- a/tests/models/test_models_classic_computers.py +++ b/tests/models/test_models_classic_computers.py @@ -250,7 +250,7 @@ def test_computer_model_parsing(): """Verify select attributes across the Computer model.""" - computer = ClassicComputer(**COMPUTER_JSON["computer"]) + computer = ClassicComputer.model_validate(COMPUTER_JSON["computer"]) assert computer.general is not None # mypy assert computer.general.id == 123 @@ -311,7 +311,9 @@ def test_computer_model_parsing(): def test_computer_model_construct_from_dict(): - computer = ClassicComputer(**{"general": {"id": 123}, "location": {"username": "oscar"}}) + computer = ClassicComputer( + **{"general": {"id": 123}, "location": {"username": "oscar"}} + ) assert computer.general is not None # mypy assert computer.general.id == 123 @@ -319,7 +321,7 @@ def test_computer_model_construct_from_dict(): assert computer.location is not None # mypy assert computer.location.username == "oscar" - computer_dict = computer.dict(exclude_none=True) + computer_dict = computer.model_dump(exclude_none=True) assert computer_dict["general"]["id"] == 123 assert computer_dict["location"]["username"] == "oscar" @@ -342,15 +344,15 @@ def test_computer_model_construct_attrs(): assert computer.extension_attributes[0].id == 1 assert computer.extension_attributes[0].value == "foo" - computer_dict = computer.dict(exclude_none=True) + computer_dict = computer.model_dump(exclude_none=True) assert computer_dict["general"]["id"] == 123 assert computer_dict["extension_attributes"][0] == {"id": 1, "value": "foo"} def test_computer_model_json_output_matches_input(): - computer = ClassicComputer(**COMPUTER_JSON["computer"]) - serialized_output = json.loads(computer.json(exclude_none=True)) + computer = ClassicComputer.model_validate(COMPUTER_JSON["computer"]) + serialized_output = json.loads(computer.model_dump_json(exclude_none=True)) diff = DeepDiff(COMPUTER_JSON["computer"], serialized_output, ignore_order=True) From 73e01be017e59db76a23097f04b276d8dda1a6dd Mon Sep 17 00:00:00 2001 From: Bryson Tyrrell Date: Fri, 29 Dec 2023 11:40:03 -0600 Subject: [PATCH 02/19] Update all classic category models. Classic category model tests pass. --- src/jamf_pro_sdk/models/classic/categories.py | 8 ++++++-- tests/models/test_models_classic_categories.py | 6 +++--- tests/models/test_models_classic_computers.py | 4 ++-- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/jamf_pro_sdk/models/classic/categories.py b/src/jamf_pro_sdk/models/classic/categories.py index 6e31084..c4ca3b0 100644 --- a/src/jamf_pro_sdk/models/classic/categories.py +++ b/src/jamf_pro_sdk/models/classic/categories.py @@ -1,6 +1,6 @@ from typing import Optional -from pydantic import Extra +from pydantic import ConfigDict from .. import BaseModel from . import ClassicApiModel @@ -8,11 +8,13 @@ _XML_ARRAY_ITEM_NAMES = {} -class ClassicCategoriesItem(BaseModel, extra=Extra.allow): +class ClassicCategoriesItem(BaseModel): """Represents a category record returned by the :meth:`~jamf_pro_sdk.clients.classic_api.ClassicApi.list_all_categories` operation. """ + model_config = ConfigDict(extra="allow") + id: Optional[int] name: Optional[str] @@ -27,6 +29,8 @@ class ClassicCategory(ClassicApiModel): passing to the API operation. """ + model_config = ConfigDict(extra="allow") + _xml_root_name = "category" _xml_array_item_names = _XML_ARRAY_ITEM_NAMES _xml_write_fields = {"name", "priority"} diff --git a/tests/models/test_models_classic_categories.py b/tests/models/test_models_classic_categories.py index 9bca7fd..6401fd8 100644 --- a/tests/models/test_models_classic_categories.py +++ b/tests/models/test_models_classic_categories.py @@ -8,7 +8,7 @@ def test_category_model_parsings(): """Verify select attributes across the ComputerGroup model.""" - category = ClassicCategory(**CATEGORY_JSON["category"]) + category = ClassicCategory.model_validate(CATEGORY_JSON["category"]) assert category is not None # mypy assert category.name == "Test Category" @@ -17,8 +17,8 @@ def test_category_model_parsings(): def test_category_model_json_output_matches_input(): - category = ClassicCategory(**CATEGORY_JSON["category"]) - serialized_output = json.loads(category.json(exclude_none=True)) + category = ClassicCategory.model_validate(CATEGORY_JSON["category"]) + serialized_output = json.loads(category.model_dump_json(exclude_none=True)) diff = DeepDiff(CATEGORY_JSON["category"], serialized_output, ignore_order=True) diff --git a/tests/models/test_models_classic_computers.py b/tests/models/test_models_classic_computers.py index 13afec6..e79b5e9 100644 --- a/tests/models/test_models_classic_computers.py +++ b/tests/models/test_models_classic_computers.py @@ -311,8 +311,8 @@ def test_computer_model_parsing(): def test_computer_model_construct_from_dict(): - computer = ClassicComputer( - **{"general": {"id": 123}, "location": {"username": "oscar"}} + computer = ClassicComputer.model_validate( + {"general": {"id": 123}, "location": {"username": "oscar"}} ) assert computer.general is not None # mypy From 4cecac9bcec35ed4e510bd7832086b712adad939 Mon Sep 17 00:00:00 2001 From: Bryson Tyrrell Date: Fri, 29 Dec 2023 11:40:58 -0600 Subject: [PATCH 03/19] Optionals must have default value of None --- src/jamf_pro_sdk/models/classic/categories.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/jamf_pro_sdk/models/classic/categories.py b/src/jamf_pro_sdk/models/classic/categories.py index c4ca3b0..64bb666 100644 --- a/src/jamf_pro_sdk/models/classic/categories.py +++ b/src/jamf_pro_sdk/models/classic/categories.py @@ -15,8 +15,8 @@ class ClassicCategoriesItem(BaseModel): model_config = ConfigDict(extra="allow") - id: Optional[int] - name: Optional[str] + id: Optional[int] = None + name: Optional[str] = None class ClassicCategory(ClassicApiModel): @@ -35,6 +35,6 @@ class ClassicCategory(ClassicApiModel): _xml_array_item_names = _XML_ARRAY_ITEM_NAMES _xml_write_fields = {"name", "priority"} - id: Optional[int] - name: Optional[str] - priority: Optional[int] + id: Optional[int] = None + name: Optional[str] = None + priority: Optional[int] = None From 92daee891cec1649fb3517ed091b24fbcee33b21 Mon Sep 17 00:00:00 2001 From: Bryson Tyrrell Date: Fri, 29 Dec 2023 11:43:56 -0600 Subject: [PATCH 04/19] Update all classic computer group models. Classic computer group model tests pass. --- .../models/classic/computer_groups.py | 34 +++++++++++-------- .../test_models_classic_computer_groups.py | 12 ++++--- 2 files changed, 27 insertions(+), 19 deletions(-) diff --git a/src/jamf_pro_sdk/models/classic/computer_groups.py b/src/jamf_pro_sdk/models/classic/computer_groups.py index bd9fb71..b4faf7e 100644 --- a/src/jamf_pro_sdk/models/classic/computer_groups.py +++ b/src/jamf_pro_sdk/models/classic/computer_groups.py @@ -1,6 +1,6 @@ from typing import List, Optional -from pydantic import Extra +from pydantic import ConfigDict from .. import BaseModel from . import ClassicApiModel, ClassicSite @@ -24,16 +24,18 @@ # is_smart: bool -class ClassicComputerGroupMember(BaseModel, extra=Extra.allow): +class ClassicComputerGroupMember(BaseModel): """ComputerGroup nested model: computer_group.computers, computer_group.computer_additions, computer_group.computer_deletions """ - id: Optional[int] - name: Optional[str] - mac_address: Optional[str] - alt_mac_address: Optional[str] - serial_number: Optional[str] + model_config = ConfigDict(extra="allow") + + id: Optional[int] = None + name: Optional[str] = None + mac_address: Optional[str] = None + alt_mac_address: Optional[str] = None + serial_number: Optional[str] = None class ClassicComputerGroup(ClassicApiModel): @@ -52,16 +54,18 @@ class ClassicComputerGroup(ClassicApiModel): operation. """ + model_config = ConfigDict(extra="allow") + _xml_root_name = "computer_group" _xml_array_item_names = _XML_ARRAY_ITEM_NAMES _xml_write_fields = {"name", "is_smart", "site", "criteria"} - id: Optional[int] - name: Optional[str] - is_smart: Optional[bool] - site: Optional[ClassicSite] - criteria: Optional[List[ClassicCriterion]] - computers: Optional[List[ClassicComputerGroupMember]] + id: Optional[int] = None + name: Optional[str] = None + is_smart: Optional[bool] = None + site: Optional[ClassicSite] = None + criteria: Optional[List[ClassicCriterion]] = None + computers: Optional[List[ClassicComputerGroupMember]] = None class ClassicComputerGroupMembershipUpdate(ClassicApiModel): @@ -73,5 +77,5 @@ class ClassicComputerGroupMembershipUpdate(ClassicApiModel): _xml_root_name = "computer_group" _xml_array_item_names = _XML_ARRAY_ITEM_NAMES - computer_additions: Optional[List[ClassicComputerGroupMember]] - computer_deletions: Optional[List[ClassicComputerGroupMember]] + computer_additions: Optional[List[ClassicComputerGroupMember]] = None + computer_deletions: Optional[List[ClassicComputerGroupMember]] = None diff --git a/tests/models/test_models_classic_computer_groups.py b/tests/models/test_models_classic_computer_groups.py index 75efad4..efa6dbc 100644 --- a/tests/models/test_models_classic_computer_groups.py +++ b/tests/models/test_models_classic_computer_groups.py @@ -51,7 +51,7 @@ def test_computer_group_model_parsing(): """Verify select attributes across the ComputerGroup model.""" - group = ClassicComputerGroup(**COMPUTER_GROUP_JSON["computer_group"]) + group = ClassicComputerGroup.model_validate(COMPUTER_GROUP_JSON["computer_group"]) assert group is not None # mypy assert group.criteria is not None # mypy @@ -79,9 +79,13 @@ def test_computer_group_model_parsing(): def test_computer_model_json_output_matches_input(): - computer = ClassicComputerGroup(**COMPUTER_GROUP_JSON["computer_group"]) - serialized_output = json.loads(computer.json(exclude_none=True)) + computer = ClassicComputerGroup.model_validate( + COMPUTER_GROUP_JSON["computer_group"] + ) + serialized_output = json.loads(computer.model_dump_json(exclude_none=True)) - diff = DeepDiff(COMPUTER_GROUP_JSON["computer_group"], serialized_output, ignore_order=True) + diff = DeepDiff( + COMPUTER_GROUP_JSON["computer_group"], serialized_output, ignore_order=True + ) assert not diff From da8b51addd62ea957f3caaaaad0cec4ab2f739ea Mon Sep 17 00:00:00 2001 From: Bryson Tyrrell Date: Fri, 29 Dec 2023 11:46:58 -0600 Subject: [PATCH 05/19] Update all classic network segment models. Classic network segment model tests pass. --- .../models/classic/network_segments.py | 40 ++++++++++--------- ...> test_models_classic_network_segments.py} | 14 +++++-- 2 files changed, 32 insertions(+), 22 deletions(-) rename tests/models/{test_models_classic_network_segments => test_models_classic_network_segments.py} (75%) diff --git a/src/jamf_pro_sdk/models/classic/network_segments.py b/src/jamf_pro_sdk/models/classic/network_segments.py index 10b122c..3e479a5 100644 --- a/src/jamf_pro_sdk/models/classic/network_segments.py +++ b/src/jamf_pro_sdk/models/classic/network_segments.py @@ -1,6 +1,6 @@ from typing import Optional -from pydantic import Extra +from pydantic import ConfigDict from .. import BaseModel from . import ClassicApiModel @@ -8,15 +8,17 @@ _XML_ARRAY_ITEM_NAMES = {} -class ClassicNetworkSegmentItem(BaseModel, extra=Extra.allow): +class ClassicNetworkSegmentItem(BaseModel): """Represents a network_segment record returned by the :meth:`~jamf_pro_sdk.clients.classic_api.ClassicApi.list_network_segments` operation. """ - id: Optional[int] - name: Optional[str] - starting_address: Optional[str] - ending_address: Optional[str] + model_config = ConfigDict(extra="allow") + + id: Optional[int] = None + name: Optional[str] = None + starting_address: Optional[str] = None + ending_address: Optional[str] = None class ClassicNetworkSegment(ClassicApiModel): @@ -24,6 +26,8 @@ class ClassicNetworkSegment(ClassicApiModel): :meth:`~jamf_pro_sdk.clients.classic_api.ClassicApi.get_network_segment_by_id` operation. """ + model_config = ConfigDict(extra="allow") + _xml_root_name = "network_segment" _xml_array_item_names = _XML_ARRAY_ITEM_NAMES _xml_write_fields = { @@ -40,15 +44,15 @@ class ClassicNetworkSegment(ClassicApiModel): "override_departments", } - id: Optional[int] - name: Optional[str] - starting_address: Optional[str] - ending_address: Optional[str] - distribution_server: Optional[str] - distribution_point: Optional[str] - url: Optional[str] - swu_server: Optional[str] - building: Optional[str] - department: Optional[str] - override_buildings: Optional[bool] - override_departments: Optional[bool] + id: Optional[int] = None + name: Optional[str] = None + starting_address: Optional[str] = None + ending_address: Optional[str] = None + distribution_server: Optional[str] = None + distribution_point: Optional[str] = None + url: Optional[str] = None + swu_server: Optional[str] = None + building: Optional[str] = None + department: Optional[str] = None + override_buildings: Optional[bool] = None + override_departments: Optional[bool] = None diff --git a/tests/models/test_models_classic_network_segments b/tests/models/test_models_classic_network_segments.py similarity index 75% rename from tests/models/test_models_classic_network_segments rename to tests/models/test_models_classic_network_segments.py index 40cd9b0..9f0fcc5 100644 --- a/tests/models/test_models_classic_network_segments +++ b/tests/models/test_models_classic_network_segments.py @@ -19,7 +19,9 @@ def test_network_segment_model_parsings(): """Verify select attributes across the NetworkSegment model.""" - network_segment = ClassicNetworkSegment(**NETWORK_SEGMENT_JSON["network_segment"]) + network_segment = ClassicNetworkSegment.model_validate( + NETWORK_SEGMENT_JSON["network_segment"] + ) assert network_segment is not None # mypy assert network_segment.name == "Test Network" @@ -36,9 +38,13 @@ def test_network_segment_model_parsings(): def test_network_segment_model_json_output_matches_input(): - network_segment = ClassicNetworkSegment(**NETWORK_SEGMENT_JSON["network_segment"]) - serialized_output = json.loads(network_segment.json(exclude_none=True)) + network_segment = ClassicNetworkSegment.model_validate( + NETWORK_SEGMENT_JSON["network_segment"] + ) + serialized_output = json.loads(network_segment.model_dump_json(exclude_none=True)) - diff = DeepDiff(NETWORK_SEGMENT_JSON["network_segment"], serialized_output, ignore_order=True) + diff = DeepDiff( + NETWORK_SEGMENT_JSON["network_segment"], serialized_output, ignore_order=True + ) assert not diff From bceb05f7d4b7a7e1020b63ea1463523769238d1d Mon Sep 17 00:00:00 2001 From: Bryson Tyrrell Date: Fri, 29 Dec 2023 11:50:12 -0600 Subject: [PATCH 06/19] Update remaining classic models --- .../classic/advanced_computer_searches.py | 38 ++++++++------ src/jamf_pro_sdk/models/classic/criteria.py | 6 ++- src/jamf_pro_sdk/models/classic/packages.py | 52 ++++++++++--------- 3 files changed, 55 insertions(+), 41 deletions(-) diff --git a/src/jamf_pro_sdk/models/classic/advanced_computer_searches.py b/src/jamf_pro_sdk/models/classic/advanced_computer_searches.py index 214a5ca..e7b93df 100644 --- a/src/jamf_pro_sdk/models/classic/advanced_computer_searches.py +++ b/src/jamf_pro_sdk/models/classic/advanced_computer_searches.py @@ -1,6 +1,6 @@ from typing import List, Optional -from pydantic import Extra +from pydantic import ConfigDict from .. import BaseModel from . import ClassicApiModel, ClassicSite @@ -13,7 +13,7 @@ } -class ClassicAdvancedComputerSearchDisplayField(BaseModel, extra=Extra.allow): +class ClassicAdvancedComputerSearchDisplayField(BaseModel): """ClassicAdvancedComputerSearch nested model: advanced_computer_search.display_fields. Display fields are additional data that are returned with the results of an advanced search. @@ -21,19 +21,23 @@ class ClassicAdvancedComputerSearchDisplayField(BaseModel, extra=Extra.allow): Jamf Pro UI for the supported names. """ - name: Optional[str] + model_config = ConfigDict(extra="allow") + name: Optional[str] = None -class ClassicAdvancedComputerSearchResult(BaseModel, extra=Extra.allow): + +class ClassicAdvancedComputerSearchResult(BaseModel): """ClassicAdvancedComputerSearch nested model: advanced_computer_search.computers. In addition to the ``id``, ``name``, and ``udid`` fields, any defined display fields will also appear with their values from the inventory record. """ - id: Optional[int] - name: Optional[str] - udid: Optional[str] + model_config = ConfigDict(extra="allow") + + id: Optional[int] = None + name: Optional[str] = None + udid: Optional[str] = None class ClassicAdvancedComputerSearchesItem(ClassicApiModel): @@ -42,8 +46,10 @@ class ClassicAdvancedComputerSearchesItem(ClassicApiModel): operation. """ - id: Optional[int] - name: Optional[str] + model_config = ConfigDict(extra="allow") + + id: Optional[int] = None + name: Optional[str] = None class ClassicAdvancedComputerSearch(ClassicApiModel): @@ -58,13 +64,15 @@ class ClassicAdvancedComputerSearch(ClassicApiModel): operation. """ + model_config = ConfigDict(extra="allow") + _xml_root_name = "advanced_computer_search" _xml_array_item_names = _XML_ARRAY_ITEM_NAMES _xml_write_fields = {"name", "site", "criteria", "display_fields"} - id: Optional[int] - name: Optional[str] - site: Optional[ClassicSite] - criteria: Optional[List[ClassicCriterion]] - display_fields: Optional[List[ClassicAdvancedComputerSearchDisplayField]] - computers: Optional[List[ClassicAdvancedComputerSearchResult]] + id: Optional[int] = None + name: Optional[str] = None + site: Optional[ClassicSite] = None + criteria: Optional[List[ClassicCriterion]] = None + display_fields: Optional[List[ClassicAdvancedComputerSearchDisplayField]] = None + computers: Optional[List[ClassicAdvancedComputerSearchResult]] = None diff --git a/src/jamf_pro_sdk/models/classic/criteria.py b/src/jamf_pro_sdk/models/classic/criteria.py index e33a9e8..cf747ec 100644 --- a/src/jamf_pro_sdk/models/classic/criteria.py +++ b/src/jamf_pro_sdk/models/classic/criteria.py @@ -1,6 +1,6 @@ from enum import Enum -from pydantic import BaseModel, Extra +from pydantic import BaseModel, ConfigDict class ClassicCriterionAndOr(str, Enum): @@ -38,9 +38,11 @@ class ClassicCriterionSearchType(str, Enum): less_than_or_equal: str = "less than or equal" -class ClassicCriterion(BaseModel, extra=Extra.allow): +class ClassicCriterion(BaseModel): """Classic API criterion. Used by Smart Groups and Advanced Searches.""" + model_config = ConfigDict(extra="allow") + name: str priority: int and_or: ClassicCriterionAndOr diff --git a/src/jamf_pro_sdk/models/classic/packages.py b/src/jamf_pro_sdk/models/classic/packages.py index 9e9d0d4..035d312 100644 --- a/src/jamf_pro_sdk/models/classic/packages.py +++ b/src/jamf_pro_sdk/models/classic/packages.py @@ -1,6 +1,6 @@ from typing import Optional, Union -from pydantic import Extra +from pydantic import ConfigDict from .. import BaseModel from . import ClassicApiModel @@ -8,13 +8,15 @@ _XML_ARRAY_ITEM_NAMES = {} -class ClassicPackageItem(BaseModel, extra=Extra.allow): +class ClassicPackageItem(BaseModel): """Represents a package record returned by the :meth:`~jamf_pro_sdk.clients.classic_api.ClassicApi.list_packages` operation. """ - id: Optional[int] - name: Optional[str] + model_config = ConfigDict(extra="allow") + + id: Optional[int] = None + name: Optional[str] = None class ClassicPackage(ClassicApiModel): @@ -28,6 +30,8 @@ class ClassicPackage(ClassicApiModel): pasting to the API operation. """ + model_config = ConfigDict(extra="allow") + _xml_root_name = "package" _xml_array_item_names = _XML_ARRAY_ITEM_NAMES _xml_write_fields = { @@ -42,23 +46,23 @@ class ClassicPackage(ClassicApiModel): "install_if_reported_available", } - id: Optional[int] - name: Optional[str] - category: Optional[str] - filename: Optional[str] - info: Optional[str] - notes: Optional[str] - priority: Optional[int] - reboot_required: Optional[bool] - fill_user_template: Optional[bool] - fill_existing_users: Optional[bool] - allow_uninstalled: Optional[bool] - os_requirements: Optional[str] - required_processor: Optional[str] - hash_type: Optional[str] - hash_value: Optional[str] - switch_with_package: Optional[str] - install_if_reported_available: Optional[bool] - reinstall_option: Optional[str] - triggering_files: Optional[Union[dict, str]] - send_notification: Optional[bool] + id: Optional[int] = None + name: Optional[str] = None + category: Optional[str] = None + filename: Optional[str] = None + info: Optional[str] = None + notes: Optional[str] = None + priority: Optional[int] = None + reboot_required: Optional[bool] = None + fill_user_template: Optional[bool] = None + fill_existing_users: Optional[bool] = None + allow_uninstalled: Optional[bool] = None + os_requirements: Optional[str] = None + required_processor: Optional[str] = None + hash_type: Optional[str] = None + hash_value: Optional[str] = None + switch_with_package: Optional[str] = None + install_if_reported_available: Optional[bool] = None + reinstall_option: Optional[str] = None + triggering_files: Optional[Union[dict, str]] = None + send_notification: Optional[bool] = None From 7ce79fe9e51f343ec048799df63c4b6d9d31d07f Mon Sep 17 00:00:00 2001 From: Bryson Tyrrell Date: Fri, 29 Dec 2023 11:55:30 -0600 Subject: [PATCH 07/19] Update webhook and client models --- src/jamf_pro_sdk/models/client.py | 2 +- src/jamf_pro_sdk/models/webhooks/webhooks.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/jamf_pro_sdk/models/client.py b/src/jamf_pro_sdk/models/client.py index a61528f..f2f55ac 100644 --- a/src/jamf_pro_sdk/models/client.py +++ b/src/jamf_pro_sdk/models/client.py @@ -83,7 +83,7 @@ class AccessToken(BaseModel): type: str = "" token: str = "" expires: datetime = EPOCH_DATETIME - scope: Optional[List[str]] + scope: Optional[List[str]] = None def __str__(self): return self.token diff --git a/src/jamf_pro_sdk/models/webhooks/webhooks.py b/src/jamf_pro_sdk/models/webhooks/webhooks.py index a53192e..1a13419 100644 --- a/src/jamf_pro_sdk/models/webhooks/webhooks.py +++ b/src/jamf_pro_sdk/models/webhooks/webhooks.py @@ -1,11 +1,11 @@ from ipaddress import IPv4Address from typing import Literal, Optional, Union -from pydantic import BaseModel, Extra +from pydantic import BaseModel, ConfigDict -class WebhookModel(BaseModel, extra=Extra.allow): - pass +class WebhookModel(BaseModel): + model_config = ConfigDict(extra="allow") class WebhookData(WebhookModel): From 8d1d88cf4a8a01441549cb8178e30f2fd7a1cff4 Mon Sep 17 00:00:00 2001 From: Bryson Tyrrell Date: Fri, 29 Dec 2023 11:58:32 -0600 Subject: [PATCH 08/19] Updated Pro computer models --- src/jamf_pro_sdk/models/pro/computers.py | 776 +++++++++++++---------- 1 file changed, 429 insertions(+), 347 deletions(-) diff --git a/src/jamf_pro_sdk/models/pro/computers.py b/src/jamf_pro_sdk/models/pro/computers.py index 4c25ec4..f22d41c 100644 --- a/src/jamf_pro_sdk/models/pro/computers.py +++ b/src/jamf_pro_sdk/models/pro/computers.py @@ -2,7 +2,7 @@ from enum import Enum from typing import List, Optional -from pydantic import Extra, Field +from pydantic import ConfigDict, Field from .. import BaseModel from . import V1Site @@ -23,67 +23,77 @@ class ComputerExtensionAttributeInputType(Enum): LDAP: str = "LDAP" -class ComputerExtensionAttribute(BaseModel, extra=Extra.allow): - definitionId: Optional[str] - name: Optional[str] - description: Optional[str] - enabled: Optional[bool] - multiValue: Optional[bool] - values: Optional[List[str]] - dataType: Optional[ComputerExtensionAttributeDataType] - options: Optional[List[str]] - inputType: Optional[ComputerExtensionAttributeInputType] +class ComputerExtensionAttribute(BaseModel): + model_config = ConfigDict(extra="allow") + + definitionId: Optional[str] = None + name: Optional[str] = None + description: Optional[str] = None + enabled: Optional[bool] = None + multiValue: Optional[bool] = None + values: Optional[List[str]] = None + dataType: Optional[ComputerExtensionAttributeDataType] = None + options: Optional[List[str]] = None + inputType: Optional[ComputerExtensionAttributeInputType] = None # Computer General Models -class ComputerRemoteManagement(BaseModel, extra=Extra.allow): - managed: Optional[bool] - managementUsername: Optional[str] - managementPassword: Optional[str] +class ComputerRemoteManagement(BaseModel): + model_config = ConfigDict(extra="allow") + + managed: Optional[bool] = None + managementUsername: Optional[str] = None + managementPassword: Optional[str] = None + + +class ComputerMdmCapability(BaseModel): + model_config = ConfigDict(extra="allow") + + capable: Optional[bool] = None + capableUsers: Optional[List[str]] = None -class ComputerMdmCapability(BaseModel, extra=Extra.allow): - capable: Optional[bool] - capableUsers: Optional[List[str]] +class EnrollmentMethod(BaseModel): + model_config = ConfigDict(extra="allow") + id: Optional[str] = None + objectName: Optional[str] = None + objectType: Optional[str] = None -class EnrollmentMethod(BaseModel, extra=Extra.allow): - id: Optional[str] - objectName: Optional[str] - objectType: Optional[str] +class ComputerGeneral(BaseModel): + model_config = ConfigDict(extra="allow") -class ComputerGeneral(BaseModel, extra=Extra.allow): - name: Optional[str] - lastIpAddress: Optional[str] - lastReportedIp: Optional[str] - jamfBinaryVersion: Optional[str] - platform: Optional[str] - barcode1: Optional[str] - barcode2: Optional[str] - assetTag: Optional[str] + name: Optional[str] = None + lastIpAddress: Optional[str] = None + lastReportedIp: Optional[str] = None + jamfBinaryVersion: Optional[str] = None + platform: Optional[str] = None + barcode1: Optional[str] = None + barcode2: Optional[str] = None + assetTag: Optional[str] = None remoteManagement: Optional[ComputerRemoteManagement] = Field( default_factory=ComputerRemoteManagement ) - supervised: Optional[bool] - mdmCapable: Optional[ComputerMdmCapability] - reportDate: Optional[datetime] - lastContactTime: Optional[datetime] - lastCloudBackupDate: Optional[datetime] - lastEnrolledDate: Optional[datetime] - mdmProfileExpiration: Optional[datetime] - initialEntryDate: Optional[date] # 2018-10-31 - distributionPoint: Optional[str] - enrollmentMethod: Optional[EnrollmentMethod] + supervised: Optional[bool] = None + mdmCapable: Optional[ComputerMdmCapability] = None + reportDate: Optional[datetime] = None + lastContactTime: Optional[datetime] = None + lastCloudBackupDate: Optional[datetime] = None + lastEnrolledDate: Optional[datetime] = None + mdmProfileExpiration: Optional[datetime] = None + initialEntryDate: Optional[date] = None # 2018-10-31 + distributionPoint: Optional[str] = None + enrollmentMethod: Optional[EnrollmentMethod] = None site: Optional[V1Site] = Field(default_factory=V1Site) - itunesStoreAccountActive: Optional[bool] - enrolledViaAutomatedDeviceEnrollment: Optional[bool] - userApprovedMdm: Optional[bool] - declarativeDeviceManagementEnabled: Optional[bool] - extensionAttributes: Optional[List[ComputerExtensionAttribute]] - managementId: Optional[str] + itunesStoreAccountActive: Optional[bool] = None + enrolledViaAutomatedDeviceEnrollment: Optional[bool] = None + userApprovedMdm: Optional[bool] = None + declarativeDeviceManagementEnabled: Optional[bool] = None + extensionAttributes: Optional[List[ComputerExtensionAttribute]] = None + managementId: Optional[str] = None # Computer Disk Encryption Models @@ -110,52 +120,62 @@ class IndividualRecoveryKeyValidityStatus(str, Enum): NOT_APPLICABLE: str = "NOT_APPLICABLE" -class ComputerPartitionEncryption(BaseModel, extra=Extra.allow): - partitionName: Optional[str] - partitionFileVault2State: Optional[ComputerPartitionFileVault2State] - partitionFileVault2Percent: Optional[int] +class ComputerPartitionEncryption(BaseModel): + model_config = ConfigDict(extra="allow") + partitionName: Optional[str] = None + partitionFileVault2State: Optional[ComputerPartitionFileVault2State] = None + partitionFileVault2Percent: Optional[int] = None -class ComputerDiskEncryption(BaseModel, extra=Extra.allow): - bootPartitionEncryptionDetails: Optional[ComputerPartitionEncryption] - individualRecoveryKeyValidityStatus: Optional[IndividualRecoveryKeyValidityStatus] - institutionalRecoveryKeyPresent: Optional[bool] - diskEncryptionConfigurationName: Optional[str] - fileVault2EnabledUserNames: Optional[List[str]] - fileVault2EligibilityMessage: Optional[str] + +class ComputerDiskEncryption(BaseModel): + model_config = ConfigDict(extra="allow") + + bootPartitionEncryptionDetails: Optional[ComputerPartitionEncryption] = None + individualRecoveryKeyValidityStatus: Optional[ + IndividualRecoveryKeyValidityStatus + ] = None + institutionalRecoveryKeyPresent: Optional[bool] = None + diskEncryptionConfigurationName: Optional[str] = None + fileVault2EnabledUserNames: Optional[List[str]] = None + fileVault2EligibilityMessage: Optional[str] = None # Computer Purchase Model -class ComputerPurchase(BaseModel, extra=Extra.allow): - leased: Optional[bool] - purchased: Optional[bool] - poNumber: Optional[str] - poDate: Optional[date] - vendor: Optional[str] - warrantyDate: Optional[date] - appleCareId: Optional[str] - leaseDate: Optional[date] - purchasePrice: Optional[str] - lifeExpectancy: Optional[int] - purchasingAccount: Optional[str] - purchasingContact: Optional[str] - extensionAttributes: Optional[List[ComputerExtensionAttribute]] +class ComputerPurchase(BaseModel): + model_config = ConfigDict(extra="allow") + + leased: Optional[bool] = None + purchased: Optional[bool] = None + poNumber: Optional[str] = None + poDate: Optional[date] = None + vendor: Optional[str] = None + warrantyDate: Optional[date] = None + appleCareId: Optional[str] = None + leaseDate: Optional[date] = None + purchasePrice: Optional[str] = None + lifeExpectancy: Optional[int] = None + purchasingAccount: Optional[str] = None + purchasingContact: Optional[str] = None + extensionAttributes: Optional[List[ComputerExtensionAttribute]] = None # Computer Application Model -class ComputerApplication(BaseModel, extra=Extra.allow): - name: Optional[str] - path: Optional[str] - version: Optional[str] - macAppStore: Optional[bool] - sizeMegabytes: Optional[int] - bundleId: Optional[str] - updateAvailable: Optional[bool] - externalVersionId: Optional[str] +class ComputerApplication(BaseModel): + model_config = ConfigDict(extra="allow") + + name: Optional[str] = None + path: Optional[str] = None + version: Optional[str] = None + macAppStore: Optional[bool] = None + sizeMegabytes: Optional[int] = None + bundleId: Optional[str] = None + updateAvailable: Optional[bool] = None + externalVersionId: Optional[str] = None # Computer Storage Models @@ -167,108 +187,124 @@ class PartitionType(str, Enum): OTHER: str = "OTHER" -class ComputerPartition(BaseModel, extra=Extra.allow): - name: Optional[str] - sizeMegabytes: Optional[int] - availableMegabytes: Optional[int] - partitionType: Optional[PartitionType] - percentUsed: Optional[int] - fileVault2State: Optional[ComputerPartitionFileVault2State] - fileVault2ProgressPercent: Optional[int] - lvmManaged: Optional[bool] - - -class ComputerDisk(BaseModel, extra=Extra.allow): - id: Optional[str] - device: Optional[str] - model: Optional[str] - revision: Optional[str] - serialNumber: Optional[str] - sizeMegabytes: Optional[int] - smartStatus: Optional[str] - type: Optional[str] +class ComputerPartition(BaseModel): + model_config = ConfigDict(extra="allow") + + name: Optional[str] = None + sizeMegabytes: Optional[int] = None + availableMegabytes: Optional[int] = None + partitionType: Optional[PartitionType] = None + percentUsed: Optional[int] = None + fileVault2State: Optional[ComputerPartitionFileVault2State] = None + fileVault2ProgressPercent: Optional[int] = None + lvmManaged: Optional[bool] = None + + +class ComputerDisk(BaseModel): + model_config = ConfigDict(extra="allow") + + id: Optional[str] = None + device: Optional[str] = None + model: Optional[str] = None + revision: Optional[str] = None + serialNumber: Optional[str] = None + sizeMegabytes: Optional[int] = None + smartStatus: Optional[str] = None + type: Optional[str] = None partitions: Optional[List[ComputerPartition]] = None -class ComputerStorage(BaseModel, extra=Extra.allow): - bootDriveAvailableSpaceMegabytes: Optional[int] - disks: Optional[List[ComputerDisk]] +class ComputerStorage(BaseModel): + model_config = ConfigDict(extra="allow") + + bootDriveAvailableSpaceMegabytes: Optional[int] = None + disks: Optional[List[ComputerDisk]] = None # Computer User and Location Model -class ComputerUserAndLocation(BaseModel, extra=Extra.allow): - username: Optional[str] - realname: Optional[str] - email: Optional[str] - position: Optional[str] - phone: Optional[str] - departmentId: Optional[str] - buildingId: Optional[str] - room: Optional[str] - extensionAttributes: Optional[List[ComputerExtensionAttribute]] +class ComputerUserAndLocation(BaseModel): + model_config = ConfigDict(extra="allow") + + username: Optional[str] = None + realname: Optional[str] = None + email: Optional[str] = None + position: Optional[str] = None + phone: Optional[str] = None + departmentId: Optional[str] = None + buildingId: Optional[str] = None + room: Optional[str] = None + extensionAttributes: Optional[List[ComputerExtensionAttribute]] = None # Computer Configuration Profile Model -class ComputerConfigurationProfile(BaseModel, extra=Extra.allow): - id: Optional[str] - username: Optional[str] - lastInstalled: Optional[datetime] - removable: Optional[bool] - displayName: Optional[str] - profileIdentifier: Optional[str] +class ComputerConfigurationProfile(BaseModel): + model_config = ConfigDict(extra="allow") + + id: Optional[str] = None + username: Optional[str] = None + lastInstalled: Optional[datetime] = None + removable: Optional[bool] = None + displayName: Optional[str] = None + profileIdentifier: Optional[str] = None # Computer Printer Model -class ComputerPrinter(BaseModel, extra=Extra.allow): - name: Optional[str] - type: Optional[str] - uri: Optional[str] - location: Optional[str] +class ComputerPrinter(BaseModel): + model_config = ConfigDict(extra="allow") + + name: Optional[str] = None + type: Optional[str] = None + uri: Optional[str] = None + location: Optional[str] = None # Computer Service Model -class ComputerService(BaseModel, extra=Extra.allow): - name: Optional[str] +class ComputerService(BaseModel): + model_config = ConfigDict(extra="allow") + + name: Optional[str] = None # Computer Hardware Models -class ComputerHardware(BaseModel, extra=Extra.allow): - make: Optional[str] - model: Optional[str] - modelIdentifier: Optional[str] - serialNumber: Optional[str] - processorSpeedMhz: Optional[int] - processorCount: Optional[int] - coreCount: Optional[int] - processorType: Optional[str] - processorArchitecture: Optional[str] - busSpeedMhz: Optional[int] - cacheSizeKilobytes: Optional[int] - networkAdapterType: Optional[str] - macAddress: Optional[str] - altNetworkAdapterType: Optional[str] - altMacAddress: Optional[str] - totalRamMegabytes: Optional[int] - openRamSlots: Optional[int] - batteryCapacityPercent: Optional[int] - smcVersion: Optional[str] - nicSpeed: Optional[str] - opticalDrive: Optional[str] - bootRom: Optional[str] - bleCapable: Optional[bool] - supportsIosAppInstalls: Optional[bool] - appleSilicon: Optional[bool] - extensionAttributes: Optional[List[ComputerExtensionAttribute]] +class ComputerHardware(BaseModel): + model_config = ConfigDict(extra="allow") + + make: Optional[str] = None + model: Optional[str] = None + modelIdentifier: Optional[str] = None + serialNumber: Optional[str] = None + processorSpeedMhz: Optional[int] = None + processorCount: Optional[int] = None + coreCount: Optional[int] = None + processorType: Optional[str] = None + processorArchitecture: Optional[str] = None + busSpeedMhz: Optional[int] = None + cacheSizeKilobytes: Optional[int] = None + networkAdapterType: Optional[str] = None + macAddress: Optional[str] = None + altNetworkAdapterType: Optional[str] = None + altMacAddress: Optional[str] = None + totalRamMegabytes: Optional[int] = None + openRamSlots: Optional[int] = None + batteryCapacityPercent: Optional[int] = None + smcVersion: Optional[str] = None + nicSpeed: Optional[str] = None + opticalDrive: Optional[str] = None + bootRom: Optional[str] = None + bleCapable: Optional[bool] = None + supportsIosAppInstalls: Optional[bool] = None + appleSilicon: Optional[bool] = None + extensionAttributes: Optional[List[ComputerExtensionAttribute]] = None # Computer Local User Account Models @@ -287,23 +323,25 @@ class AzureActiveDirectoryId(str, Enum): UNKNOWN: str = "UNKNOWN" -class ComputerLocalUserAccount(BaseModel, extra=Extra.allow): - uid: Optional[str] - username: Optional[str] - fullName: Optional[str] - admin: Optional[bool] - homeDirectory: Optional[str] - homeDirectorySizeMb: Optional[int] - fileVault2Enabled: Optional[bool] - userAccountType: Optional[UserAccountType] - passwordMinLength: Optional[int] - passwordMaxAge: Optional[int] - passwordMinComplexCharacters: Optional[int] - passwordHistoryDepth: Optional[int] - passwordRequireAlphanumeric: Optional[bool] - computerAzureActiveDirectoryId: Optional[str] - userAzureActiveDirectoryId: Optional[str] - azureActiveDirectoryId: Optional[AzureActiveDirectoryId] +class ComputerLocalUserAccount(BaseModel): + model_config = ConfigDict(extra="allow") + + uid: Optional[str] = None + username: Optional[str] = None + fullName: Optional[str] = None + admin: Optional[bool] = None + homeDirectory: Optional[str] = None + homeDirectorySizeMb: Optional[int] = None + fileVault2Enabled: Optional[bool] = None + userAccountType: Optional[UserAccountType] = None + passwordMinLength: Optional[int] = None + passwordMaxAge: Optional[int] = None + passwordMinComplexCharacters: Optional[int] = None + passwordHistoryDepth: Optional[int] = None + passwordRequireAlphanumeric: Optional[bool] = None + computerAzureActiveDirectoryId: Optional[str] = None + userAzureActiveDirectoryId: Optional[str] = None + azureActiveDirectoryId: Optional[AzureActiveDirectoryId] = None # Computer Certificate Models @@ -322,54 +360,64 @@ class CertificateStatus(str, Enum): ISSUED: str = "ISSUED" -class ComputerCertificate(BaseModel, extra=Extra.allow): - commonName: Optional[str] - identity: Optional[bool] - expirationDate: Optional[datetime] - username: Optional[str] - lifecycleStatus: Optional[LifecycleStatus] - certificateStatus: Optional[CertificateStatus] - subjectName: Optional[str] - serialNumber: Optional[str] - sha1Fingerprint: Optional[str] - issuedDate: Optional[str] +class ComputerCertificate(BaseModel): + model_config = ConfigDict(extra="allow") + + commonName: Optional[str] = None + identity: Optional[bool] = None + expirationDate: Optional[datetime] = None + username: Optional[str] = None + lifecycleStatus: Optional[LifecycleStatus] = None + certificateStatus: Optional[CertificateStatus] = None + subjectName: Optional[str] = None + serialNumber: Optional[str] = None + sha1Fingerprint: Optional[str] = None + issuedDate: Optional[str] = None # Computer Attachment Model -class ComputerAttachment(BaseModel, extra=Extra.allow): - id: Optional[str] - name: Optional[str] - fileType: Optional[str] - sizeBytes: Optional[int] +class ComputerAttachment(BaseModel): + model_config = ConfigDict(extra="allow") + + id: Optional[str] = None + name: Optional[str] = None + fileType: Optional[str] = None + sizeBytes: Optional[int] = None # Computer Plugin Model -class ComputerPlugin(BaseModel, extra=Extra.allow): - name: Optional[str] - version: Optional[str] - path: Optional[str] +class ComputerPlugin(BaseModel): + model_config = ConfigDict(extra="allow") + + name: Optional[str] = None + version: Optional[str] = None + path: Optional[str] = None # Computer Package Receipt Model -class ComputerPackageReceipts(BaseModel, extra=Extra.allow): - installedByJamfPro: Optional[List[str]] - installedByInstallerSwu: Optional[List[str]] - cached: Optional[List[str]] +class ComputerPackageReceipts(BaseModel): + model_config = ConfigDict(extra="allow") + + installedByJamfPro: Optional[List[str]] = None + installedByInstallerSwu: Optional[List[str]] = None + cached: Optional[List[str]] = None # Computer Font Model -class ComputerFont(BaseModel, extra=Extra.allow): - name: Optional[str] - version: Optional[str] - path: Optional[str] +class ComputerFont(BaseModel): + model_config = ConfigDict(extra="allow") + + name: Optional[str] = None + version: Optional[str] = None + path: Optional[str] = None # Computer Security Models @@ -404,18 +452,20 @@ class ExternalBootLevel(str, Enum): UNKNOWN: str = "UNKNOWN" -class ComputerSecurity(BaseModel, extra=Extra.allow): - sipStatus: Optional[SipStatus] - gatekeeperStatus: Optional[GatekeeperStatus] - xprotectVersion: Optional[str] - autoLoginDisabled: Optional[bool] - remoteDesktopEnabled: Optional[bool] - activationLockEnabled: Optional[bool] - recoveryLockEnabled: Optional[bool] - firewallEnabled: Optional[bool] - secureBootLevel: Optional[SecureBootLevel] - externalBootLevel: Optional[ExternalBootLevel] - bootstrapTokenAllowed: Optional[bool] +class ComputerSecurity(BaseModel): + model_config = ConfigDict(extra="allow") + + sipStatus: Optional[SipStatus] = None + gatekeeperStatus: Optional[GatekeeperStatus] = None + xprotectVersion: Optional[str] = None + autoLoginDisabled: Optional[bool] = None + remoteDesktopEnabled: Optional[bool] = None + activationLockEnabled: Optional[bool] = None + recoveryLockEnabled: Optional[bool] = None + firewallEnabled: Optional[bool] = None + secureBootLevel: Optional[SecureBootLevel] = None + externalBootLevel: Optional[ExternalBootLevel] = None + bootstrapTokenAllowed: Optional[bool] = None # Computer Operating System Models @@ -429,110 +479,136 @@ class FileVault2Status(str, Enum): ALL_ENCRYPTED: str = "ALL_ENCRYPTED" -class ComputerOperatingSystem(BaseModel, extra=Extra.allow): - name: Optional[str] - version: Optional[str] - build: Optional[str] - activeDirectoryStatus: Optional[str] - fileVault2Status: Optional[FileVault2Status] - softwareUpdateDeviceId: Optional[str] - extensionAttributes: Optional[List[ComputerExtensionAttribute]] +class ComputerOperatingSystem(BaseModel): + model_config = ConfigDict(extra="allow") + + name: Optional[str] = None + version: Optional[str] = None + build: Optional[str] = None + activeDirectoryStatus: Optional[str] = None + fileVault2Status: Optional[FileVault2Status] = None + softwareUpdateDeviceId: Optional[str] = None + extensionAttributes: Optional[List[ComputerExtensionAttribute]] = None # Computer Licensed Software Model -class ComputerLicensedSoftware(BaseModel, extra=Extra.allow): - id: Optional[str] - name: Optional[str] +class ComputerLicensedSoftware(BaseModel): + model_config = ConfigDict(extra="allow") + + id: Optional[str] = None + name: Optional[str] = None # Computer iBeacon Model -class ComputeriBeacon(BaseModel, extra=Extra.allow): - name: Optional[str] +class ComputeriBeacon(BaseModel): + model_config = ConfigDict(extra="allow") + + name: Optional[str] = None # Computer Software Update Model -class ComputerSoftwareUpdate(BaseModel, extra=Extra.allow): - name: Optional[str] - version: Optional[str] - packageName: Optional[str] +class ComputerSoftwareUpdate(BaseModel): + model_config = ConfigDict(extra="allow") + + name: Optional[str] = None + version: Optional[str] = None + packageName: Optional[str] = None # Computer Content Caching Models -class ComputerContentCachingParentAlert(BaseModel, extra=Extra.allow): - contentCachingParentAlertId: Optional[str] - addresses: Optional[List[str]] - className: Optional[str] - postDate: Optional[datetime] +class ComputerContentCachingParentAlert(BaseModel): + model_config = ConfigDict(extra="allow") + + contentCachingParentAlertId: Optional[str] = None + addresses: Optional[List[str]] = None + className: Optional[str] = None + postDate: Optional[datetime] = None -class ComputerContentCachingParentCapabilities(BaseModel, extra=Extra.allow): - contentCachingParentCapabilitiesId: Optional[str] - imports: Optional[bool] - namespaces: Optional[bool] - personalContent: Optional[bool] - queryParameters: Optional[bool] - sharedContent: Optional[bool] - prioritization: Optional[bool] +class ComputerContentCachingParentCapabilities(BaseModel): + model_config = ConfigDict(extra="allow") + contentCachingParentCapabilitiesId: Optional[str] = None + imports: Optional[bool] = None + namespaces: Optional[bool] = None + personalContent: Optional[bool] = None + queryParameters: Optional[bool] = None + sharedContent: Optional[bool] = None + prioritization: Optional[bool] = None -class ComputerContentCachingParentLocalNetwork(BaseModel, extra=Extra.allow): - contentCachingParentLocalNetworkId: Optional[str] - speed: Optional[int] - wired: Optional[bool] +class ComputerContentCachingParentLocalNetwork(BaseModel): + model_config = ConfigDict(extra="allow") -class ComputerContentCachingParentDetails(BaseModel, extra=Extra.allow): - contentCachingParentDetailsId: Optional[str] - acPower: Optional[bool] - cacheSizeBytes: Optional[int] - capabilities: Optional[ComputerContentCachingParentCapabilities] - portable: Optional[bool] - localNetwork: Optional[List[ComputerContentCachingParentLocalNetwork]] + contentCachingParentLocalNetworkId: Optional[str] = None + speed: Optional[int] = None + wired: Optional[bool] = None -class ComputerContentCachingParent(BaseModel, extra=Extra.allow): - contentCachingParentId: Optional[str] - address: Optional[str] - alerts: Optional[ComputerContentCachingParentAlert] - details: Optional[ComputerContentCachingParentDetails] - guid: Optional[str] - healthy: Optional[bool] - port: Optional[int] - version: Optional[str] +class ComputerContentCachingParentDetails(BaseModel): + model_config = ConfigDict(extra="allow") + contentCachingParentDetailsId: Optional[str] = None + acPower: Optional[bool] = None + cacheSizeBytes: Optional[int] = None + capabilities: Optional[ComputerContentCachingParentCapabilities] = None + portable: Optional[bool] = None + localNetwork: Optional[List[ComputerContentCachingParentLocalNetwork]] = None -class ComputerContentCachingAlert(BaseModel, extra=Extra.allow): - cacheBytesLimit: Optional[int] - className: Optional[str] - pathPreventingAccess: Optional[str] - postDate: Optional[datetime] - reservedVolumeBytes: Optional[int] - resource: Optional[str] +class ComputerContentCachingParent(BaseModel): + model_config = ConfigDict(extra="allow") -class ComputerContentCachingCacheDetail(BaseModel, extra=Extra.allow): - computerContentCachingCacheDetailsId: Optional[str] - categoryName: Optional[str] - diskSpaceBytesUsed: Optional[int] + contentCachingParentId: Optional[str] = None + address: Optional[str] = None + alerts: Optional[ComputerContentCachingParentAlert] = None + details: Optional[ComputerContentCachingParentDetails] = None + guid: Optional[str] = None + healthy: Optional[bool] = None + port: Optional[int] = None + version: Optional[str] = None -class ComputerContentCachingDataMigrationErrorUserInfo(BaseModel, extra=Extra.allow): - key: Optional[str] - value: Optional[str] +class ComputerContentCachingAlert(BaseModel): + model_config = ConfigDict(extra="allow") + cacheBytesLimit: Optional[int] = None + className: Optional[str] = None + pathPreventingAccess: Optional[str] = None + postDate: Optional[datetime] = None + reservedVolumeBytes: Optional[int] = None + resource: Optional[str] = None -class ComputerContentCachingDataMigrationError(BaseModel, extra=Extra.allow): - code: Optional[int] - domain: Optional[str] - userInfo: Optional[List[ComputerContentCachingDataMigrationErrorUserInfo]] + +class ComputerContentCachingCacheDetail(BaseModel): + model_config = ConfigDict(extra="allow") + + computerContentCachingCacheDetailsId: Optional[str] = None + categoryName: Optional[str] = None + diskSpaceBytesUsed: Optional[int] = None + + +class ComputerContentCachingDataMigrationErrorUserInfo(BaseModel): + model_config = ConfigDict(extra="allow") + + key: Optional[str] = None + value: Optional[str] = None + + +class ComputerContentCachingDataMigrationError(BaseModel): + model_config = ConfigDict(extra="allow") + + code: Optional[int] = None + domain: Optional[str] = None + userInfo: Optional[List[ComputerContentCachingDataMigrationErrorUserInfo]] = None class ComputerContentCachingRegistrationStatus(str, Enum): @@ -547,86 +623,92 @@ class ComputerContentCachingTetheratorStatus(str, Enum): CONTENT_CACHING_ENABLED: str = "CONTENT_CACHING_ENABLED" -class ComputerContentCaching(BaseModel, extra=Extra.allow): - computerContentCachingInformationId: Optional[str] - parents: Optional[List[ComputerContentCachingParent]] - alerts: Optional[List[ComputerContentCachingAlert]] - activated: Optional[bool] - active: Optional[bool] - actualCacheBytesUsed: Optional[int] - cacheDetails: Optional[List[ComputerContentCachingCacheDetail]] - cacheBytesFree: Optional[int] - cacheBytesLimit: Optional[int] - cacheStatus: Optional[str] - cacheBytesUsed: Optional[int] - dataMigrationCompleted: Optional[bool] - dataMigrationProgressPercentage: Optional[int] - dataMigrationError: Optional[ComputerContentCachingDataMigrationError] - maxCachePressureLast1HourPercentage: Optional[int] - personalCacheBytesFree: Optional[int] - personalCacheBytesLimit: Optional[int] - personalCacheBytesUsed: Optional[int] - port: Optional[int] - publicAddress: Optional[str] - registrationError: Optional[str] - registrationResponseCode: Optional[int] - registrationStarted: Optional[datetime] - registrationStatus: Optional[ComputerContentCachingRegistrationStatus] - restrictedMedia: Optional[bool] - serverGuid: Optional[str] - startupStatus: Optional[str] - tetheratorStatus: Optional[ComputerContentCachingTetheratorStatus] - totalBytesAreSince: Optional[datetime] - totalBytesDropped: Optional[int] - totalBytesImported: Optional[int] - totalBytesReturnedToChildren: Optional[int] - totalBytesReturnedToClients: Optional[int] - totalBytesReturnedToPeers: Optional[int] - totalBytesStoredFromOrigin: Optional[int] - totalBytesStoredFromParents: Optional[int] - totalBytesStoredFromPeers: Optional[int] +class ComputerContentCaching(BaseModel): + model_config = ConfigDict(extra="allow") + + computerContentCachingInformationId: Optional[str] = None + parents: Optional[List[ComputerContentCachingParent]] = None + alerts: Optional[List[ComputerContentCachingAlert]] = None + activated: Optional[bool] = None + active: Optional[bool] = None + actualCacheBytesUsed: Optional[int] = None + cacheDetails: Optional[List[ComputerContentCachingCacheDetail]] = None + cacheBytesFree: Optional[int] = None + cacheBytesLimit: Optional[int] = None + cacheStatus: Optional[str] = None + cacheBytesUsed: Optional[int] = None + dataMigrationCompleted: Optional[bool] = None + dataMigrationProgressPercentage: Optional[int] = None + dataMigrationError: Optional[ComputerContentCachingDataMigrationError] = None + maxCachePressureLast1HourPercentage: Optional[int] = None + personalCacheBytesFree: Optional[int] = None + personalCacheBytesLimit: Optional[int] = None + personalCacheBytesUsed: Optional[int] = None + port: Optional[int] = None + publicAddress: Optional[str] = None + registrationError: Optional[str] = None + registrationResponseCode: Optional[int] = None + registrationStarted: Optional[datetime] = None + registrationStatus: Optional[ComputerContentCachingRegistrationStatus] = None + restrictedMedia: Optional[bool] = None + serverGuid: Optional[str] = None + startupStatus: Optional[str] = None + tetheratorStatus: Optional[ComputerContentCachingTetheratorStatus] = None + totalBytesAreSince: Optional[datetime] = None + totalBytesDropped: Optional[int] = None + totalBytesImported: Optional[int] = None + totalBytesReturnedToChildren: Optional[int] = None + totalBytesReturnedToClients: Optional[int] = None + totalBytesReturnedToPeers: Optional[int] = None + totalBytesStoredFromOrigin: Optional[int] = None + totalBytesStoredFromParents: Optional[int] = None + totalBytesStoredFromPeers: Optional[int] = None # Computer Group Membership Model -class ComputerGroupMembership(BaseModel, extra=Extra.allow): - groupId: Optional[str] - groupName: Optional[str] - smartGroup: Optional[bool] +class ComputerGroupMembership(BaseModel): + model_config = ConfigDict(extra="allow") + + groupId: Optional[str] = None + groupName: Optional[str] = None + smartGroup: Optional[bool] = None # Computer Inventory Model -class Computer(BaseModel, extra=Extra.allow): +class Computer(BaseModel): """Represents a full computer inventory record.""" - id: Optional[str] - udid: Optional[str] + model_config = ConfigDict(extra="allow") + + id: Optional[str] = None + udid: Optional[str] = None general: Optional[ComputerGeneral] = Field(default_factory=ComputerGeneral) - diskEncryption: Optional[ComputerDiskEncryption] - purchasing: Optional[ComputerPurchase] - applications: Optional[List[ComputerApplication]] - storage: Optional[ComputerStorage] + diskEncryption: Optional[ComputerDiskEncryption] = None + purchasing: Optional[ComputerPurchase] = None + applications: Optional[List[ComputerApplication]] = None + storage: Optional[ComputerStorage] = None userAndLocation: Optional[ComputerUserAndLocation] = Field( default_factory=ComputerUserAndLocation ) - configurationProfiles: Optional[List[ComputerConfigurationProfile]] - printers: Optional[List[ComputerPrinter]] - services: Optional[List[ComputerService]] - hardware: Optional[ComputerHardware] - localUserAccounts: Optional[List[ComputerLocalUserAccount]] - certificates: Optional[List[ComputerCertificate]] - attachments: Optional[List[ComputerAttachment]] - plugins: Optional[List[ComputerPlugin]] - packageReceipts: Optional[ComputerPackageReceipts] - fonts: Optional[List[ComputerFont]] - security: Optional[ComputerSecurity] - operatingSystem: Optional[ComputerOperatingSystem] - licensedSoftware: Optional[List[ComputerLicensedSoftware]] - ibeacons: Optional[List[ComputeriBeacon]] - softwareUpdates: Optional[List[ComputerSoftwareUpdate]] - extensionAttributes: Optional[List[ComputerExtensionAttribute]] - contentCaching: Optional[ComputerContentCaching] - groupMemberships: Optional[List[ComputerGroupMembership]] + configurationProfiles: Optional[List[ComputerConfigurationProfile]] = None + printers: Optional[List[ComputerPrinter]] = None + services: Optional[List[ComputerService]] = None + hardware: Optional[ComputerHardware] = None + localUserAccounts: Optional[List[ComputerLocalUserAccount]] = None + certificates: Optional[List[ComputerCertificate]] = None + attachments: Optional[List[ComputerAttachment]] = None + plugins: Optional[List[ComputerPlugin]] = None + packageReceipts: Optional[ComputerPackageReceipts] = None + fonts: Optional[List[ComputerFont]] = None + security: Optional[ComputerSecurity] = None + operatingSystem: Optional[ComputerOperatingSystem] = None + licensedSoftware: Optional[List[ComputerLicensedSoftware]] = None + ibeacons: Optional[List[ComputeriBeacon]] = None + softwareUpdates: Optional[List[ComputerSoftwareUpdate]] = None + extensionAttributes: Optional[List[ComputerExtensionAttribute]] = None + contentCaching: Optional[ComputerContentCaching] = None + groupMemberships: Optional[List[ComputerGroupMembership]] = None From 8cd441e8fdcb7498114a18924fde3a8e2c1825c0 Mon Sep 17 00:00:00 2001 From: Bryson Tyrrell Date: Fri, 29 Dec 2023 12:11:28 -0600 Subject: [PATCH 09/19] Updated Pro jcds2 and mdm models --- src/jamf_pro_sdk/models/pro/jcds2.py | 14 ++++++++--- src/jamf_pro_sdk/models/pro/mdm.py | 37 +++++++++++++++++++--------- 2 files changed, 35 insertions(+), 16 deletions(-) diff --git a/src/jamf_pro_sdk/models/pro/jcds2.py b/src/jamf_pro_sdk/models/pro/jcds2.py index 9072055..ebbcfcf 100644 --- a/src/jamf_pro_sdk/models/pro/jcds2.py +++ b/src/jamf_pro_sdk/models/pro/jcds2.py @@ -1,10 +1,12 @@ from datetime import datetime from uuid import UUID -from pydantic import BaseModel, Extra +from pydantic import BaseModel, ConfigDict -class NewFile(BaseModel, extra=Extra.allow): +class NewFile(BaseModel): + model_config = ConfigDict(extra="allow") + accessKeyID: str secretAccessKey: str sessionToken: str @@ -15,7 +17,9 @@ class NewFile(BaseModel, extra=Extra.allow): uuid: UUID -class File(BaseModel, extra=Extra.allow): +class File(BaseModel): + model_config = ConfigDict(extra="allow") + region: str fileName: str length: int @@ -23,5 +27,7 @@ class File(BaseModel, extra=Extra.allow): sha3: str -class DownloadUrl(BaseModel, extra=Extra.allow): +class DownloadUrl(BaseModel): + model_config = ConfigDict(extra="allow") + uri: str diff --git a/src/jamf_pro_sdk/models/pro/mdm.py b/src/jamf_pro_sdk/models/pro/mdm.py index e7459c8..41a4aca 100644 --- a/src/jamf_pro_sdk/models/pro/mdm.py +++ b/src/jamf_pro_sdk/models/pro/mdm.py @@ -3,7 +3,7 @@ from typing import Annotated, List, Literal, Optional, Union from uuid import UUID -from pydantic import BaseModel, Extra, Field, constr +from pydantic import BaseModel, ConfigDict, Field, StringConstraints from .api_options import get_mdm_commands_v2_allowed_command_types @@ -70,6 +70,9 @@ class EraseDeviceCommandReturnToService(BaseModel): wifiProfileData: str +EraseDeviceCommandPin = Annotated[str, StringConstraints(min_length=6, max_length=6)] + + class EraseDeviceCommand(BaseModel): """MDM command to remotely wipe a device. Optionally, set the ``returnToService`` property to automatically connect to a wireless network at Setup Assistant. @@ -93,11 +96,11 @@ class EraseDeviceCommand(BaseModel): """ commandType: Literal["ERASE_DEVICE"] = "ERASE_DEVICE" - preserveDataPlan: Optional[bool] - disallowProximitySetup: Optional[bool] - pin: Optional[constr(min_length=6, max_length=6)] - obliterationBehavior: Optional[EraseDeviceCommandObliterationBehavior] - returnToService: Optional[EraseDeviceCommandReturnToService] + preserveDataPlan: Optional[bool] = None + disallowProximitySetup: Optional[bool] = None + pin: Optional[EraseDeviceCommandPin] = None + obliterationBehavior: Optional[EraseDeviceCommandObliterationBehavior] = None + returnToService: Optional[EraseDeviceCommandReturnToService] = None # Log Out User @@ -192,9 +195,11 @@ class ShutDownDeviceCommand(BaseModel): # Custom Command -class CustomCommand(BaseModel, extra=Extra.allow): +class CustomCommand(BaseModel): """A free form model for new commands not yet supported by the SDK.""" + model_config = ConfigDict(extra="allow") + commandType: str @@ -226,16 +231,20 @@ class SendMdmCommand(BaseModel): # MDM Command Responses -class SendMdmCommandResponse(BaseModel, extra=Extra.allow): +class SendMdmCommandResponse(BaseModel): + model_config = ConfigDict(extra="allow") + id: str href: str -class RenewMdmProfileResponse(BaseModel, extra=Extra.allow): +class RenewMdmProfileResponse(BaseModel): """This response model flattens the normal API JSON response from a nested ``udidsNotProcessed.uuids`` array to just ``udidsNotProcessed``. """ + model_config = ConfigDict(extra="allow") + udidsNotProcessed: Optional[List[UUID]] @@ -250,7 +259,9 @@ class MdmCommandStatusClientTypes(str, Enum): MOBILE_DEVICE_USER = "MOBILE_DEVICE_USER" -class MdmCommandStatusClient(BaseModel, extra=Extra.allow): +class MdmCommandStatusClient(BaseModel): + model_config = ConfigDict(extra="allow") + managementId: UUID clientType: MdmCommandStatusClientTypes @@ -268,11 +279,13 @@ class MdmCommandStatusStates(str, Enum): ) -class MdmCommandStatus(BaseModel, extra=Extra.allow): +class MdmCommandStatus(BaseModel): + model_config = ConfigDict(extra="allow") + uuid: UUID client: MdmCommandStatusClient commandState: MdmCommandStatusStates commandType: MdmCommandStatusTypes dateSent: datetime dateCompleted: datetime - profileId: Optional[int] + profileId: Optional[int] = None From bcbfdde8493c8b29933d4faf00e332ad017e7ce3 Mon Sep 17 00:00:00 2001 From: Bryson Tyrrell Date: Fri, 29 Dec 2023 12:21:47 -0600 Subject: [PATCH 10/19] Suppress warning for Pydantic 'model_' namespace conflict --- src/jamf_pro_sdk/models/classic/computers.py | 4 +++- tests/models/test_models_classic_computers.py | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/jamf_pro_sdk/models/classic/computers.py b/src/jamf_pro_sdk/models/classic/computers.py index e5f6c55..db53c02 100644 --- a/src/jamf_pro_sdk/models/classic/computers.py +++ b/src/jamf_pro_sdk/models/classic/computers.py @@ -176,7 +176,9 @@ class ClassicComputerHardwareMappedPrinter(BaseModel): class ClassicComputerHardware(ClassicApiModel): """Computer nested model: computer.hardware""" - model_config = ConfigDict(extra="allow") + model_config = ConfigDict(extra="allow", protected_namespaces=()) + # The 'model_identifier' attribute conflicts with Pydantic's protect 'model_' namespace + # Overriding 'protected_namespaces' for hardware suppresses the warning _xml_root_name = "hardware" _xml_array_item_names = _XML_ARRAY_ITEM_NAMES diff --git a/tests/models/test_models_classic_computers.py b/tests/models/test_models_classic_computers.py index e79b5e9..f216a0f 100644 --- a/tests/models/test_models_classic_computers.py +++ b/tests/models/test_models_classic_computers.py @@ -266,6 +266,7 @@ def test_computer_model_parsing(): assert computer.hardware is not None # mypy assert computer.hardware.model == "MacBook Pro (14-inch, 2021)" + assert computer.hardware.model_identifier == "MacBookPro18,3" assert computer.hardware.filevault2_users is not None # mypy assert computer.hardware.filevault2_users[0] == "admin" From bf9d339bc99d503bf6827cf94d6f83b00ac72442 Mon Sep 17 00:00:00 2001 From: Bryson Tyrrell Date: Fri, 29 Dec 2023 13:19:28 -0600 Subject: [PATCH 11/19] Move all existing tests to a 'unit' tests subpackage --- Makefile | 5 ++++- tests/{conftest.py => unit/__init__.py} | 0 tests/unit/models/__init__.py | 0 tests/{ => unit}/models/test_models_classic_categories.py | 0 .../{ => unit}/models/test_models_classic_computer_groups.py | 0 tests/{ => unit}/models/test_models_classic_computers.py | 2 +- .../models/test_models_classic_network_segments.py | 0 tests/{ => unit}/models/test_models_classic_utils.py | 0 tests/{ => unit}/test_pro_api_expressions.py | 0 tests/{ => unit}/utils.py | 0 10 files changed, 5 insertions(+), 2 deletions(-) rename tests/{conftest.py => unit/__init__.py} (100%) create mode 100644 tests/unit/models/__init__.py rename tests/{ => unit}/models/test_models_classic_categories.py (100%) rename tests/{ => unit}/models/test_models_classic_computer_groups.py (100%) rename tests/{ => unit}/models/test_models_classic_computers.py (99%) rename tests/{ => unit}/models/test_models_classic_network_segments.py (100%) rename tests/{ => unit}/models/test_models_classic_utils.py (100%) rename tests/{ => unit}/test_pro_api_expressions.py (100%) rename tests/{ => unit}/utils.py (100%) diff --git a/Makefile b/Makefile index aa5ee72..f07658b 100644 --- a/Makefile +++ b/Makefile @@ -11,7 +11,10 @@ clean: rm -rf build/ dist/ src/*.egg-info **/__pycache__ .coverage .pytest_cache/ .ruff_cache/ test: - pytest + pytest tests/unit + +test-all: + pytest tests lint: black --check src tests diff --git a/tests/conftest.py b/tests/unit/__init__.py similarity index 100% rename from tests/conftest.py rename to tests/unit/__init__.py diff --git a/tests/unit/models/__init__.py b/tests/unit/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/models/test_models_classic_categories.py b/tests/unit/models/test_models_classic_categories.py similarity index 100% rename from tests/models/test_models_classic_categories.py rename to tests/unit/models/test_models_classic_categories.py diff --git a/tests/models/test_models_classic_computer_groups.py b/tests/unit/models/test_models_classic_computer_groups.py similarity index 100% rename from tests/models/test_models_classic_computer_groups.py rename to tests/unit/models/test_models_classic_computer_groups.py diff --git a/tests/models/test_models_classic_computers.py b/tests/unit/models/test_models_classic_computers.py similarity index 99% rename from tests/models/test_models_classic_computers.py rename to tests/unit/models/test_models_classic_computers.py index f216a0f..2751806 100644 --- a/tests/models/test_models_classic_computers.py +++ b/tests/unit/models/test_models_classic_computers.py @@ -7,7 +7,7 @@ ClassicComputerGeneralRemoteManagement, ClassicComputerGroupsAccountsUserInventoriesUser, ) -from tests import utils +from tests.unit import utils COMPUTER_JSON = { "computer": { diff --git a/tests/models/test_models_classic_network_segments.py b/tests/unit/models/test_models_classic_network_segments.py similarity index 100% rename from tests/models/test_models_classic_network_segments.py rename to tests/unit/models/test_models_classic_network_segments.py diff --git a/tests/models/test_models_classic_utils.py b/tests/unit/models/test_models_classic_utils.py similarity index 100% rename from tests/models/test_models_classic_utils.py rename to tests/unit/models/test_models_classic_utils.py diff --git a/tests/test_pro_api_expressions.py b/tests/unit/test_pro_api_expressions.py similarity index 100% rename from tests/test_pro_api_expressions.py rename to tests/unit/test_pro_api_expressions.py diff --git a/tests/utils.py b/tests/unit/utils.py similarity index 100% rename from tests/utils.py rename to tests/unit/utils.py From 74a67dc7b4fe0fff9c657ba5bb01fd126530bccf Mon Sep 17 00:00:00 2001 From: Bryson Tyrrell Date: Fri, 29 Dec 2023 14:17:10 -0600 Subject: [PATCH 12/19] Start integration tests against Jamf dummy JSS --- Makefile | 2 +- .../clients/pro_api/pagination.py | 14 +++++--- tests/integration/__init__.py | 0 tests/integration/conftest.py | 32 +++++++++++++++++++ tests/integration/test_client_auth.py | 9 ++++++ .../integration/test_pro_client_computers.py | 14 ++++++++ tests/unit/conftest.py | 0 7 files changed, 66 insertions(+), 5 deletions(-) create mode 100644 tests/integration/__init__.py create mode 100644 tests/integration/conftest.py create mode 100644 tests/integration/test_client_auth.py create mode 100644 tests/integration/test_pro_client_computers.py create mode 100644 tests/unit/conftest.py diff --git a/Makefile b/Makefile index f07658b..153e7d0 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ SHELL := /bin/bash .PHONY: docs install: - python3 -m pip install --editable '.[dev]' + python3 -m pip install --upgrade --editable '.[dev]' uninstall: python3 -m pip uninstall -y -r <(python3 -m pip freeze) diff --git a/src/jamf_pro_sdk/clients/pro_api/pagination.py b/src/jamf_pro_sdk/clients/pro_api/pagination.py index f879944..cd65994 100644 --- a/src/jamf_pro_sdk/clients/pro_api/pagination.py +++ b/src/jamf_pro_sdk/clients/pro_api/pagination.py @@ -51,7 +51,9 @@ class FilterField: def __init__(self, name: str): self.name = name - def _return_expression(self, operator: str, value: Union[bool, int, str]) -> FilterExpression: + 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)], @@ -112,7 +114,9 @@ def __and__(self, expression: SortExpression) -> SortExpression: def validate(self, allowed_fields: List[str]): if not all([i in allowed_fields for i in self.fields]): - raise ValueError(f"A field is not in allowed sort fields: {', '.join(allowed_fields)}") + raise ValueError( + f"A field is not in allowed sort fields: {', '.join(allowed_fields)}" + ) class SortField: @@ -120,7 +124,9 @@ def __init__(self, field: str): self.field = field def _return_expression(self, order: str) -> SortExpression: - return SortExpression(sort_expression=f"{self.field}:{order}", fields=[self.field]) + return SortExpression( + sort_expression=f"{self.field}:{order}", fields=[self.field] + ) def asc(self) -> SortExpression: return self._return_expression("asc") @@ -249,7 +255,7 @@ def _request(self) -> Iterator[Page]: [ {"page": i} for i in range( - self.start_page, + self.start_page + 1, math.ceil((total_count - results_count) / self.page_size) + 1, ) ], diff --git a/tests/integration/__init__.py b/tests/integration/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py new file mode 100644 index 0000000..26c141b --- /dev/null +++ b/tests/integration/conftest.py @@ -0,0 +1,32 @@ +import logging + +import pytest + +from jamf_pro_sdk import ( + JamfProClient, + BasicAuthProvider, + SessionConfig, + logger_quick_setup, +) + +JAMF_PRO_HOST = "dummy.jamfcloud.com" +JAMF_PRO_USERNAME = "demo" +JAMF_PRO_PASS = "tryitout" + +logger_quick_setup(logging.DEBUG) + + +@pytest.fixture(scope="module") +def jamf_client(): + client = JamfProClient( + server=JAMF_PRO_HOST, + credentials=BasicAuthProvider( + username=JAMF_PRO_USERNAME, password=JAMF_PRO_PASS + ), + session_config=SessionConfig(timeout=30), + ) + + # Retrieve an access token immediately after init + client.get_access_token() + + return client diff --git a/tests/integration/test_client_auth.py b/tests/integration/test_client_auth.py new file mode 100644 index 0000000..b6c2e7d --- /dev/null +++ b/tests/integration/test_client_auth.py @@ -0,0 +1,9 @@ +import pytest + +from jamf_pro_sdk.models.client import AccessToken + + +def test_integration_basic_auth(jamf_client): + current_token = jamf_client.get_access_token() + assert isinstance(current_token, AccessToken) + assert not current_token.is_expired diff --git a/tests/integration/test_pro_client_computers.py b/tests/integration/test_pro_client_computers.py new file mode 100644 index 0000000..5d4c4ec --- /dev/null +++ b/tests/integration/test_pro_client_computers.py @@ -0,0 +1,14 @@ +from jamf_pro_sdk.clients.pro_api.pagination import FilterField, SortField + + +def test_integration_pro_computer_inventory_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_computer_inventory_v1(page_size=2000) + 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_computer_inventory_v1(page_size=10) + assert result_total_count == len(result_paginated) diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py new file mode 100644 index 0000000..e69de29 From bf3b3f9f7f3f1a637687177d2cc91d5a729998bc Mon Sep 17 00:00:00 2001 From: Bryson Tyrrell Date: Fri, 29 Dec 2023 15:52:00 -0600 Subject: [PATCH 13/19] Replace deprecated Pydantic calls. Fix formatting. --- src/jamf_pro_sdk/clients/__init__.py | 9 ++++--- .../clients/pro_api/pagination.py | 14 +++------- src/jamf_pro_sdk/clients/webhooks.py | 26 +++++++++---------- src/jamf_pro_sdk/models/classic/__init__.py | 4 +-- src/jamf_pro_sdk/models/classic/computers.py | 12 +++------ src/jamf_pro_sdk/models/pro/computers.py | 4 +-- 6 files changed, 27 insertions(+), 42 deletions(-) diff --git a/src/jamf_pro_sdk/clients/__init__.py b/src/jamf_pro_sdk/clients/__init__.py index d76b853..daa07de 100644 --- a/src/jamf_pro_sdk/clients/__init__.py +++ b/src/jamf_pro_sdk/clients/__init__.py @@ -8,13 +8,12 @@ import certifi import requests import requests.adapters -from pydantic import BaseModel, parse_obj_as +from pydantic import BaseModel from requests.utils import cookiejar_from_dict from ..clients.classic_api import ClassicApi from ..clients.jcds2 import JCDS2 from ..clients.pro_api import ProApi -from ..models import BaseModel from ..models.classic import ClassicApiModel from ..models.client import SessionConfig from .auth import CredentialsProvider @@ -242,8 +241,10 @@ def pro_api_request( pro_req["headers"]["Content-Type"] = "application/json" if isinstance(data, dict): pro_req["json"] = data + elif isinstance(data, BaseModel): + pro_req["data"] = data.model_dump_json(exclude_none=True) else: - pro_req["data"] = data.json(exclude_none=True) + raise ValueError("'data' must be one of 'dict' or 'BaseModel'") with self.session.request(**pro_req) as pro_resp: logger.info("ProAPIRequest %s %s", method.upper(), resource_path) @@ -323,7 +324,7 @@ def concurrent_api_requests( if hasattr(return_model, "_xml_root_name") else response.json() ) - yield parse_obj_as(return_model, response_data) + yield return_model.model_validate(response_data) else: yield response except Exception as err: diff --git a/src/jamf_pro_sdk/clients/pro_api/pagination.py b/src/jamf_pro_sdk/clients/pro_api/pagination.py index cd65994..2f889aa 100644 --- a/src/jamf_pro_sdk/clients/pro_api/pagination.py +++ b/src/jamf_pro_sdk/clients/pro_api/pagination.py @@ -51,9 +51,7 @@ class FilterField: def __init__(self, name: str): self.name = name - def _return_expression( - self, operator: str, value: Union[bool, int, str] - ) -> FilterExpression: + 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)], @@ -114,9 +112,7 @@ def __and__(self, expression: SortExpression) -> SortExpression: def validate(self, allowed_fields: List[str]): if not all([i in allowed_fields for i in self.fields]): - raise ValueError( - f"A field is not in allowed sort fields: {', '.join(allowed_fields)}" - ) + raise ValueError(f"A field is not in allowed sort fields: {', '.join(allowed_fields)}") class SortField: @@ -124,9 +120,7 @@ def __init__(self, field: str): self.field = field def _return_expression(self, order: str) -> SortExpression: - return SortExpression( - sort_expression=f"{self.field}:{order}", fields=[self.field] - ) + return SortExpression(sort_expression=f"{self.field}:{order}", fields=[self.field]) def asc(self) -> SortExpression: return self._return_expression("asc") @@ -234,7 +228,7 @@ def _paginated_request(self, page: int) -> Page: page=page, page_count=len(response["results"]), total_count=response["totalCount"], - results=[self.return_model.parse_obj(i) for i in response["results"]] + results=[self.return_model.model_validate(i) for i in response["results"]] if self.return_model else response["results"], ) diff --git a/src/jamf_pro_sdk/clients/webhooks.py b/src/jamf_pro_sdk/clients/webhooks.py index 7ca0bbb..b38cb48 100644 --- a/src/jamf_pro_sdk/clients/webhooks.py +++ b/src/jamf_pro_sdk/clients/webhooks.py @@ -55,7 +55,7 @@ def _batch( yield generator.build() def send_webhook(self, webhook: webhooks.WebhookModel) -> requests.Response: - """Send a single webhook in a HTTP POST request to the configured URL. + """Send a single webhook in an HTTP POST request to the configured URL. :param webhook: The webhook object that will be serialized to JSON. :type webhook: ~webhooks.WebhookModel @@ -64,7 +64,7 @@ def send_webhook(self, webhook: webhooks.WebhookModel) -> requests.Response: :rtype: requests.Response """ response = self.session.post( - self.url, headers={"Content-Type": "application/json"}, data=webhook.json() + self.url, headers={"Content-Type": "application/json"}, data=webhook.model_dump_json() ) return response @@ -141,24 +141,24 @@ def _load_webhook_generators(): if not inspect.isclass(cls) or not issubclass(cls, webhooks.WebhookModel): continue - attrs = {"__set_as_default_factory_for_type__": True, "__faker__": Faker()} + attrs: dict = {"__set_as_default_factory_for_type__": True, "__faker__": Faker()} if issubclass(cls, webhooks.WebhookData): attrs["eventTimestamp"] = Use(epoch) elif issubclass(cls, webhooks.WebhookModel): - if "macAddress" in cls.__fields__: + if "macAddress" in cls.model_fields: attrs["macAddress"] = attrs["__faker__"].mac_address - if "alternateMacAddress" in cls.__fields__: + if "alternateMacAddress" in cls.model_fields: attrs["alternateMacAddress"] = attrs["__faker__"].mac_address - if "wifiMacAddress" in cls.__fields__: + if "wifiMacAddress" in cls.model_fields: attrs["wifiMacAddress"] = attrs["__faker__"].mac_address - if "bluetoothMacAddress" in cls.__fields__: + if "bluetoothMacAddress" in cls.model_fields: attrs["bluetoothMacAddress"] = attrs["__faker__"].mac_address - if "udid" in cls.__fields__: + if "udid" in cls.model_fields: attrs["udid"] = Use(udid) - if "serialNumber" in cls.__fields__: + if "serialNumber" in cls.model_fields: attrs["serialNumber"] = Use(serial_number) # TODO: Fields that are specific to iOS/iPadOS devices @@ -167,13 +167,13 @@ def _load_webhook_generators(): # if "imei" in cls.__fields__: # kwargs["imei"] = Use(imei) - if "realName" in cls.__fields__: + if "realName" in cls.model_fields: attrs["realName"] = attrs["__faker__"].name - if "username" in cls.__fields__: + if "username" in cls.model_fields: attrs["username"] = attrs["__faker__"].user_name - if "emailAddress" in cls.__fields__: + if "emailAddress" in cls.model_fields: attrs["emailAddress"] = attrs["__faker__"].ascii_safe_email - if "phone" in cls.__fields__: + if "phone" in cls.model_fields: attrs["phone"] = attrs["__faker__"].phone_number w = get_webhook_generator(cls, **attrs) diff --git a/src/jamf_pro_sdk/models/classic/__init__.py b/src/jamf_pro_sdk/models/classic/__init__.py index 0decdb1..7ba8e9f 100644 --- a/src/jamf_pro_sdk/models/classic/__init__.py +++ b/src/jamf_pro_sdk/models/classic/__init__.py @@ -52,9 +52,7 @@ def remove_fields(data: Any, values_to_remove: Iterable = None): class ClassicApiModel(BaseModel): """The base model used for Classic API models.""" - model_config = ConfigDict( - extra="allow", json_encoders={datetime: convert_datetime_to_jamf_iso} - ) + model_config = ConfigDict(extra="allow", json_encoders={datetime: convert_datetime_to_jamf_iso}) _xml_root_name: str _xml_array_item_names: Dict[str, str] diff --git a/src/jamf_pro_sdk/models/classic/computers.py b/src/jamf_pro_sdk/models/classic/computers.py index db53c02..9eaaba1 100644 --- a/src/jamf_pro_sdk/models/classic/computers.py +++ b/src/jamf_pro_sdk/models/classic/computers.py @@ -97,9 +97,7 @@ class ClassicComputerGeneral(BaseModel): remote_management: Optional[ClassicComputerGeneralRemoteManagement] = None supervised: Optional[bool] = None mdm_capable: Optional[bool] = None - mdm_capable_users: Optional[ - Union[dict, ClassicComputerGeneralMdmCapableUsers] - ] = None + mdm_capable_users: Optional[Union[dict, ClassicComputerGeneralMdmCapableUsers]] = None management_status: Optional[ClassicComputerGeneralManagementStatus] = None report_date: Optional[str] = None report_date_epoch: Optional[int] = None @@ -447,12 +445,8 @@ class ClassicComputer(ClassicApiModel): _xml_array_item_names = _XML_ARRAY_ITEM_NAMES _xml_write_fields = {"general", "location", "extension_attributes"} - general: Optional[ClassicComputerGeneral] = Field( - default_factory=ClassicComputerGeneral - ) - location: Optional[ClassicDeviceLocation] = Field( - default_factory=ClassicDeviceLocation - ) + general: Optional[ClassicComputerGeneral] = Field(default_factory=ClassicComputerGeneral) + location: Optional[ClassicDeviceLocation] = Field(default_factory=ClassicDeviceLocation) purchasing: Optional[ClassicDevicePurchasing] = None # Peripherals are a deprecated feature of Jamf Pro peripherals: Optional[Any] = None diff --git a/src/jamf_pro_sdk/models/pro/computers.py b/src/jamf_pro_sdk/models/pro/computers.py index f22d41c..bb0bedd 100644 --- a/src/jamf_pro_sdk/models/pro/computers.py +++ b/src/jamf_pro_sdk/models/pro/computers.py @@ -132,9 +132,7 @@ class ComputerDiskEncryption(BaseModel): model_config = ConfigDict(extra="allow") bootPartitionEncryptionDetails: Optional[ComputerPartitionEncryption] = None - individualRecoveryKeyValidityStatus: Optional[ - IndividualRecoveryKeyValidityStatus - ] = None + individualRecoveryKeyValidityStatus: Optional[IndividualRecoveryKeyValidityStatus] = None institutionalRecoveryKeyPresent: Optional[bool] = None diskEncryptionConfigurationName: Optional[str] = None fileVault2EnabledUserNames: Optional[List[str]] = None From 8c938ae3224f6e6ec80532088ef44b5ac73e111d Mon Sep 17 00:00:00 2001 From: Bryson Tyrrell Date: Fri, 29 Dec 2023 15:52:46 -0600 Subject: [PATCH 14/19] Fix formatting. --- tests/integration/conftest.py | 14 +++++++------- tests/integration/test_client_auth.py | 2 -- tests/integration/test_pro_client_computers.py | 2 +- .../models/test_models_classic_computer_groups.py | 8 ++------ tests/unit/models/test_models_classic_computers.py | 1 + .../models/test_models_classic_network_segments.py | 12 +++--------- 6 files changed, 14 insertions(+), 25 deletions(-) diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 26c141b..ec392c5 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -1,18 +1,20 @@ import logging +import os import pytest from jamf_pro_sdk import ( - JamfProClient, BasicAuthProvider, + JamfProClient, SessionConfig, logger_quick_setup, ) -JAMF_PRO_HOST = "dummy.jamfcloud.com" -JAMF_PRO_USERNAME = "demo" -JAMF_PRO_PASS = "tryitout" +JAMF_PRO_HOST = os.getenv("JAMF_PRO_HOST", "dummy.jamfcloud.com") +JAMF_PRO_USERNAME = os.getenv("JAMF_PRO_USERNAME", "demo") +JAMF_PRO_PASS = os.getenv("JAMF_PRO_PASS", "tryitout") +# Run pytest with '-s' to view logging output logger_quick_setup(logging.DEBUG) @@ -20,9 +22,7 @@ def jamf_client(): client = JamfProClient( server=JAMF_PRO_HOST, - credentials=BasicAuthProvider( - username=JAMF_PRO_USERNAME, password=JAMF_PRO_PASS - ), + credentials=BasicAuthProvider(username=JAMF_PRO_USERNAME, password=JAMF_PRO_PASS), session_config=SessionConfig(timeout=30), ) diff --git a/tests/integration/test_client_auth.py b/tests/integration/test_client_auth.py index b6c2e7d..bf4640f 100644 --- a/tests/integration/test_client_auth.py +++ b/tests/integration/test_client_auth.py @@ -1,5 +1,3 @@ -import pytest - from jamf_pro_sdk.models.client import AccessToken diff --git a/tests/integration/test_pro_client_computers.py b/tests/integration/test_pro_client_computers.py index 5d4c4ec..9e64b91 100644 --- a/tests/integration/test_pro_client_computers.py +++ b/tests/integration/test_pro_client_computers.py @@ -1,4 +1,4 @@ -from jamf_pro_sdk.clients.pro_api.pagination import FilterField, SortField +# from jamf_pro_sdk.clients.pro_api.pagination import FilterField, SortField def test_integration_pro_computer_inventory_v1_default(jamf_client): diff --git a/tests/unit/models/test_models_classic_computer_groups.py b/tests/unit/models/test_models_classic_computer_groups.py index efa6dbc..3f34f87 100644 --- a/tests/unit/models/test_models_classic_computer_groups.py +++ b/tests/unit/models/test_models_classic_computer_groups.py @@ -79,13 +79,9 @@ def test_computer_group_model_parsing(): def test_computer_model_json_output_matches_input(): - computer = ClassicComputerGroup.model_validate( - COMPUTER_GROUP_JSON["computer_group"] - ) + computer = ClassicComputerGroup.model_validate(COMPUTER_GROUP_JSON["computer_group"]) serialized_output = json.loads(computer.model_dump_json(exclude_none=True)) - diff = DeepDiff( - COMPUTER_GROUP_JSON["computer_group"], serialized_output, ignore_order=True - ) + diff = DeepDiff(COMPUTER_GROUP_JSON["computer_group"], serialized_output, ignore_order=True) assert not diff diff --git a/tests/unit/models/test_models_classic_computers.py b/tests/unit/models/test_models_classic_computers.py index 2751806..d50addb 100644 --- a/tests/unit/models/test_models_classic_computers.py +++ b/tests/unit/models/test_models_classic_computers.py @@ -7,6 +7,7 @@ ClassicComputerGeneralRemoteManagement, ClassicComputerGroupsAccountsUserInventoriesUser, ) + from tests.unit import utils COMPUTER_JSON = { diff --git a/tests/unit/models/test_models_classic_network_segments.py b/tests/unit/models/test_models_classic_network_segments.py index 9f0fcc5..23f05ea 100644 --- a/tests/unit/models/test_models_classic_network_segments.py +++ b/tests/unit/models/test_models_classic_network_segments.py @@ -19,9 +19,7 @@ def test_network_segment_model_parsings(): """Verify select attributes across the NetworkSegment model.""" - network_segment = ClassicNetworkSegment.model_validate( - NETWORK_SEGMENT_JSON["network_segment"] - ) + network_segment = ClassicNetworkSegment.model_validate(NETWORK_SEGMENT_JSON["network_segment"]) assert network_segment is not None # mypy assert network_segment.name == "Test Network" @@ -38,13 +36,9 @@ def test_network_segment_model_parsings(): def test_network_segment_model_json_output_matches_input(): - network_segment = ClassicNetworkSegment.model_validate( - NETWORK_SEGMENT_JSON["network_segment"] - ) + network_segment = ClassicNetworkSegment.model_validate(NETWORK_SEGMENT_JSON["network_segment"]) serialized_output = json.loads(network_segment.model_dump_json(exclude_none=True)) - diff = DeepDiff( - NETWORK_SEGMENT_JSON["network_segment"], serialized_output, ignore_order=True - ) + diff = DeepDiff(NETWORK_SEGMENT_JSON["network_segment"], serialized_output, ignore_order=True) assert not diff From 73d0b579b4ccb3c3445f0da804c58f67567bb2b5 Mon Sep 17 00:00:00 2001 From: Bryson Tyrrell Date: Fri, 29 Dec 2023 15:53:04 -0600 Subject: [PATCH 15/19] Tests for webhook generators. --- tests/unit/test_webhooks_faker.py | 190 ++++++++++++++++++++++++++++++ 1 file changed, 190 insertions(+) create mode 100644 tests/unit/test_webhooks_faker.py diff --git a/tests/unit/test_webhooks_faker.py b/tests/unit/test_webhooks_faker.py new file mode 100644 index 0000000..701d88a --- /dev/null +++ b/tests/unit/test_webhooks_faker.py @@ -0,0 +1,190 @@ +import re +from ipaddress import IPv4Address + +from jamf_pro_sdk.clients.webhooks import get_webhook_generator +from jamf_pro_sdk.models.webhooks import ( + ComputerAdded, + ComputerCheckIn, + ComputerInventoryCompleted, + ComputerPolicyFinished, + ComputerPushCapabilityChanged, + DeviceAddedToDep, + JssShutdown, + JssStartup, + MobileDeviceCheckIn, + MobileDeviceEnrolled, + MobileDevicePushSent, + MobileDeviceUnEnrolled, + PushSent, + RestApiOperation, + SmartGroupComputerMembershipChange, + SmartGroupMobileDeviceMembershipChange, + SmartGroupUserMembershipChange, +) +from jamf_pro_sdk.models.webhooks.webhooks import ComputerEvent + +MAC_REGEX = re.compile(r"^(?:[0-9A-Fa-f]{2}:){5}(?:[0-9A-Fa-f]{2})$") +SERIAL_REGEX = re.compile(r"^[0-9A-Za-z]{10}$") +# Only assert for uppercase UUIDs +UUID_REGEX = re.compile(r"^[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}$") + + +def test_webhooks_computer_added(): + generator = get_webhook_generator(ComputerAdded) + computer = generator.build() + + assert isinstance(computer, ComputerAdded) + assert computer.webhook.webhookEvent == "ComputerAdded" + + # These fields will not be re-tested for any other events that share the same type + assert isinstance(computer.event.ipAddress, IPv4Address) + assert isinstance(computer.event.reportedIpAddress, IPv4Address) + assert MAC_REGEX.match(computer.event.alternateMacAddress) + assert MAC_REGEX.match(computer.event.macAddress) + assert SERIAL_REGEX.match(computer.event.serialNumber) + assert UUID_REGEX.match(computer.event.udid) + + +def test_webhooks_computer_checkin(): + generator = get_webhook_generator(ComputerCheckIn) + computer = generator.build() + + assert isinstance(computer, ComputerCheckIn) + assert computer.webhook.webhookEvent == "ComputerCheckIn" + # Computer data is nested in this event type + assert isinstance(computer.event.computer, ComputerEvent) + + +def test_webhooks_computer_inventory_completed(): + generator = get_webhook_generator(ComputerInventoryCompleted) + computer = generator.build() + + assert isinstance(computer, ComputerInventoryCompleted) + assert computer.webhook.webhookEvent == "ComputerInventoryCompleted" + + +def test_webhooks_computer_policy_finished(): + generator = get_webhook_generator(ComputerPolicyFinished) + computer = generator.build() + + assert isinstance(computer, ComputerPolicyFinished) + assert computer.webhook.webhookEvent == "ComputerPolicyFinished" + # Computer data is nested in this event type + assert isinstance(computer.event.computer, ComputerEvent) + + +def test_webhooks_computer_push_capability_changed(): + generator = get_webhook_generator(ComputerPushCapabilityChanged) + computer = generator.build() + + assert isinstance(computer, ComputerPushCapabilityChanged) + assert computer.webhook.webhookEvent == "ComputerPushCapabilityChanged" + + +def test_webhooks_device_added_to_dep(): + generator = get_webhook_generator(DeviceAddedToDep) + dep_event = generator.build() + + assert isinstance(dep_event, DeviceAddedToDep) + assert dep_event.webhook.webhookEvent == "DeviceAddedToDEP" + + +def test_webhooks_jss_startup(): + generator = get_webhook_generator(JssStartup) + startup_event = generator.build() + + assert isinstance(startup_event, JssStartup) + assert startup_event.webhook.webhookEvent == "JSSStartup" + + +def test_webhooks_jss_shutdown(): + generator = get_webhook_generator(JssShutdown) + shutdown_event = generator.build() + + assert isinstance(shutdown_event, JssShutdown) + assert shutdown_event.webhook.webhookEvent == "JSSShutdown" + + +def test_webhooks_mobile_device_checkin(): + generator = get_webhook_generator(MobileDeviceCheckIn) + mobile_device = generator.build() + + assert isinstance(mobile_device, MobileDeviceCheckIn) + assert mobile_device.webhook.webhookEvent == "MobileDeviceCheckIn" + + # These fields will not be re-tested for any other events that share the same type + assert isinstance(mobile_device.event.ipAddress, IPv4Address) + assert MAC_REGEX.match(mobile_device.event.bluetoothMacAddress) + assert SERIAL_REGEX.match(mobile_device.event.serialNumber) + assert UUID_REGEX.match(mobile_device.event.udid) + assert MAC_REGEX.match(mobile_device.event.wifiMacAddress) + + +def test_webhooks_mobile_device_enrolled(): + generator = get_webhook_generator(MobileDeviceEnrolled) + mobile_device = generator.build() + + assert isinstance(mobile_device, MobileDeviceEnrolled) + assert mobile_device.webhook.webhookEvent == "MobileDeviceEnrolled" + + +def test_webhooks_mobile_device_unenrolled(): + generator = get_webhook_generator(MobileDeviceUnEnrolled) + mobile_device = generator.build() + + assert isinstance(mobile_device, MobileDeviceUnEnrolled) + assert mobile_device.webhook.webhookEvent == "MobileDeviceUnEnrolled" + + +def test_webhooks_mobile_device_push_sent(): + generator = get_webhook_generator(MobileDevicePushSent) + mobile_device = generator.build() + + assert isinstance(mobile_device, MobileDevicePushSent) + assert mobile_device.webhook.webhookEvent == "MobileDevicePushSent" + + +def test_webhooks_push_sent(): + generator = get_webhook_generator(PushSent) + push_event = generator.build() + + assert isinstance(push_event, PushSent) + assert push_event.webhook.webhookEvent == "PushSent" + + +def test_webhooks_rest_api_op(): + generator = get_webhook_generator(RestApiOperation) + rest_api_op = generator.build() + + assert isinstance(rest_api_op, RestApiOperation) + assert rest_api_op.webhook.webhookEvent == "RestAPIOperation" + + +def test_webhooks_smart_group_computer_membership_change(): + generator = get_webhook_generator(SmartGroupComputerMembershipChange) + group_change = generator.build() + + assert isinstance(group_change, SmartGroupComputerMembershipChange) + assert group_change.webhook.webhookEvent == "SmartGroupComputerMembershipChange" + assert isinstance(group_change.event.computer, bool) and group_change.event.computer is True + assert isinstance(group_change.event.smartGroup, bool) and group_change.event.smartGroup is True + + +def test_webhooks_smart_group_mobile_device_membership_change(): + generator = get_webhook_generator(SmartGroupMobileDeviceMembershipChange) + group_change = generator.build() + + assert isinstance(group_change, SmartGroupMobileDeviceMembershipChange) + assert group_change.webhook.webhookEvent == "SmartGroupMobileDeviceMembershipChange" + assert isinstance(group_change.event.computer, bool) and group_change.event.computer is False + assert isinstance(group_change.event.smartGroup, bool) and group_change.event.smartGroup is True + + +def test_webhooks_smart_group_user_membership_change(): + generator = get_webhook_generator(SmartGroupUserMembershipChange) + group_change = generator.build() + + assert isinstance(group_change, SmartGroupUserMembershipChange) + assert group_change.webhook.webhookEvent == "SmartGroupUserMembershipChange" + assert not hasattr(group_change.event, "computer") + assert isinstance(group_change.event.smartGroup, bool) and group_change.event.smartGroup is True From 8d7ef50780efa4289547c1eaaa5e637b5201df58 Mon Sep 17 00:00:00 2001 From: Bryson Tyrrell Date: Fri, 29 Dec 2023 16:02:02 -0600 Subject: [PATCH 16/19] Starting integration test for classic API. --- .../integration/test_classic_client_computers.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 tests/integration/test_classic_client_computers.py diff --git a/tests/integration/test_classic_client_computers.py b/tests/integration/test_classic_client_computers.py new file mode 100644 index 0000000..d55a9d0 --- /dev/null +++ b/tests/integration/test_classic_client_computers.py @@ -0,0 +1,15 @@ +import random + + +def test_integration_classic_get_computers(jamf_client): + result_list = jamf_client.classic_api.list_all_computers(subsets=["basic"]) + + # Select five records at random, read, and verify their IDs + for _ in range(0, 5): + listed_computer = random.choice(result_list) + computer_read = jamf_client.classic_api.get_computer_by_id(listed_computer.id) + + assert computer_read.general.id == listed_computer.id + assert computer_read.general.udid == listed_computer.udid + assert computer_read.general.name == listed_computer.name + assert computer_read.general.mac_address == listed_computer.mac_address From 7a990d7dbe0103b6e41c85ba961b2e120829c038 Mon Sep 17 00:00:00 2001 From: Bryson Tyrrell Date: Fri, 29 Dec 2023 16:25:05 -0600 Subject: [PATCH 17/19] Enable output of test coverage --- .gitignore | 1 + pyproject.toml | 21 +++++++++++---------- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/.gitignore b/.gitignore index 4747dbe..6b363b3 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ .ruff_cache/ .venv/ build/ +coverage/ dist/ docs/contributors/_autosummary/ docs/reference/_autosummary/ diff --git a/pyproject.toml b/pyproject.toml index 645c815..f70fa88 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -108,26 +108,27 @@ minversion = "6.0" addopts = [ "--durations=5", "--color=yes", - "--cov-report=html:htmlcov", + "--cov=src", + "--cov-report=html:coverage/htmlcov", "--cov-report=term-missing", # "--cov-fail-under=90", ] testpaths = [ "./tests" ] -[tool.coverage.run] -source = ["jamf_pro_sdk"] -branch = true -parallel = true +#[tool.coverage.run] +#source = ["src", "jamf_pro_sdk"] +#branch = true +#parallel = true -[tool.coverage.report] -show_missing = true +#[tool.coverage.report] +#show_missing = true # Uncomment the following line to fail to build when the coverage is too low # fail_under = 99 #[tool.coverage.xml] -#output = "private/coverage/coverage.xml" -# +#output = "coverage/coverage.xml" + #[tool.coverage.html] -#directory = "private/coverage/" +#directory = "coverage/htmlcov" From ffd4ffb8001ef0fc39f45c254fccc5026bd78f43 Mon Sep 17 00:00:00 2001 From: Bryson Tyrrell Date: Fri, 29 Dec 2023 16:25:17 -0600 Subject: [PATCH 18/19] Fix assertion --- tests/unit/models/test_models_classic_computers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/models/test_models_classic_computers.py b/tests/unit/models/test_models_classic_computers.py index d50addb..3d215d3 100644 --- a/tests/unit/models/test_models_classic_computers.py +++ b/tests/unit/models/test_models_classic_computers.py @@ -296,7 +296,7 @@ def test_computer_model_parsing(): assert "Group 2" in computer.groups_accounts.computer_group_memberships assert computer.groups_accounts.local_accounts is not None # mypy - assert computer.groups_accounts.local_accounts[0].uid is "502" + assert computer.groups_accounts.local_accounts[0].uid == "502" assert computer.groups_accounts.local_accounts[0].administrator is True assert computer.groups_accounts.user_inventories is not None # mypy From 3b614204114ca0ba399c7c8af315c60219569b47 Mon Sep 17 00:00:00 2001 From: Bryson Tyrrell Date: Fri, 29 Dec 2023 16:34:35 -0600 Subject: [PATCH 19/19] Fix model name in doc --- docs/reference/models_classic.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference/models_classic.rst b/docs/reference/models_classic.rst index 166f93f..9cd408c 100644 --- a/docs/reference/models_classic.rst +++ b/docs/reference/models_classic.rst @@ -100,7 +100,7 @@ Network Segments :toctree: _autosummary ClassicNetworkSegment - ClassicNetworkSegmentsItem + ClassicNetworkSegmentItem Packages --------