Skip to content

Commit

Permalink
Merge pull request #36 from macadmins/pydantic-2-update
Browse files Browse the repository at this point in the history
Pydantic 2 Update
  • Loading branch information
brysontyrrell authored Jan 1, 2024
2 parents d8614d2 + 3b61420 commit 6d61b17
Show file tree
Hide file tree
Showing 37 changed files with 1,195 additions and 753 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
.ruff_cache/
.venv/
build/
coverage/
dist/
docs/contributors/_autosummary/
docs/reference/_autosummary/
Expand Down
7 changes: 5 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion docs/reference/models_classic.rst
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ Network Segments
:toctree: _autosummary

ClassicNetworkSegment
ClassicNetworkSegmentsItem
ClassicNetworkSegmentItem

Packages
--------
Expand Down
27 changes: 14 additions & 13 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
]
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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"
9 changes: 5 additions & 4 deletions src/jamf_pro_sdk/clients/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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:
Expand Down
1 change: 0 additions & 1 deletion src/jamf_pro_sdk/clients/classic_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@
if TYPE_CHECKING:
import requests


VALID_COMPUTER_SUBSETS = (
"general",
"location",
Expand Down
4 changes: 2 additions & 2 deletions src/jamf_pro_sdk/clients/pro_api/pagination.py
Original file line number Diff line number Diff line change
Expand Up @@ -228,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"],
)
Expand All @@ -249,7 +249,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,
)
],
Expand Down
26 changes: 13 additions & 13 deletions src/jamf_pro_sdk/clients/webhooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand Down
83 changes: 41 additions & 42 deletions src/jamf_pro_sdk/models/classic/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -52,6 +52,8 @@ 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
Expand All @@ -69,7 +71,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,
)
Expand All @@ -83,57 +85,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
Loading

0 comments on commit 6d61b17

Please sign in to comment.