Skip to content

Commit

Permalink
Merge pull request #54 from macserv/feature/add-pro-packages
Browse files Browse the repository at this point in the history
Add Packages to Pro API, and a few Typing Enhancements
  • Loading branch information
brysontyrrell authored Nov 11, 2024
2 parents 8705919 + 33e0945 commit aebf442
Show file tree
Hide file tree
Showing 16 changed files with 406 additions and 112 deletions.
2 changes: 2 additions & 0 deletions docs/contributors/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,8 @@ Any Jamf Pro API added to the clients must have the following elements code comp

* The API method has been added to the appropriate client, has a complete docstring, and has an interface in-line with other methods of that client.
* The API must have matching and complete Pydantic models.
* Provide ``@overload`` interfaces for API methods with dynamic return types (e.g. ``Union[list[Computer], Iterator[Page]]``) which be determined from the value of an argument.
* If the value of a variable, method argument, or return parameter can be ``None``, inform the type checker by wrapping its type with ``Optional[]``, e.g. ``description: Optional[str] = None``
* Unless your code is covered by another automated test you will need to add tests to ensure coverage.

The SDK references in the documentation automatically include all public method on the clients and no documentation changes may be required as a part of the contribution.
Expand Down
10 changes: 10 additions & 0 deletions docs/reference/models_pro.rst
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,16 @@ Computers
ComputerContentCaching
ComputerGroupMembership

Packages
--------

.. currentmodule:: jamf_pro_sdk.models.pro.packages

.. autosummary::
:toctree: _autosummary

Package

JCDS2
-----

Expand Down
4 changes: 4 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,10 @@ package-dir = {"" = "src"}
where = ["src"]


[tool.setuptools.package-data]
"jamf_pro_sdk" = ["py.typed"]


[tool.setuptools.dynamic]
version = {attr = "jamf_pro_sdk.__about__.__version__"}
readme = {file = ["README.md"], content-type = "text/markdown"}
Expand Down
12 changes: 12 additions & 0 deletions src/jamf_pro_sdk/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,15 @@
)
from .helpers import logger_quick_setup
from .models.client import SessionConfig

__all__ = [
"__title__",
"__version__",
"JamfProClient",
"BasicAuthProvider",
"LoadFromAwsSecretsManager",
"LoadFromKeychain",
"PromptForCredentials",
"logger_quick_setup",
"SessionConfig",
]
18 changes: 13 additions & 5 deletions src/jamf_pro_sdk/clients/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import logging
import tempfile
from pathlib import Path
from typing import Any, Callable, Dict, Iterable, Iterator, Optional, Type, Union
from typing import Any, BinaryIO, Callable, Dict, Iterable, Iterator, Optional, Type, Union
from urllib.parse import urlunparse

import certifi
Expand All @@ -27,7 +27,7 @@ def __init__(
server: str,
credentials: CredentialsProvider,
port: int = 443,
session_config: SessionConfig = None,
session_config: Optional[SessionConfig] = None,
):
"""The base client class for interacting with the Jamf Pro APIs.
Expand Down Expand Up @@ -138,7 +138,7 @@ def classic_api_request(
method: str,
resource_path: str,
data: Optional[Union[str, ClassicApiModel]] = None,
override_headers: dict = None,
override_headers: Optional[dict] = None,
) -> requests.Response:
"""Perform a request to the Classic API.
Expand Down Expand Up @@ -195,7 +195,8 @@ def pro_api_request(
resource_path: str,
query_params: Optional[Dict[str, str]] = None,
data: Optional[Union[dict, BaseModel]] = None,
override_headers: Dict[str, str] = None,
files: Optional[dict[str, tuple[str, BinaryIO, str]]] = None,
override_headers: Optional[Dict[str, str]] = None,
) -> requests.Response:
"""Perform a request to the Pro API.
Expand All @@ -214,6 +215,10 @@ def pro_api_request(
or ``BaseModel`` that is being sent.
:type data: dict | BaseModel
:param files: If the request is a ``POST``, a dictionary with a single ``files`` key,
and a tuple containing the filename, file-like object to upload, and mime type.
:type files: Optional[dict[str, tuple[str, BinaryIO, str]]]
:param override_headers: A dictionary of key-value pairs that will be set as
headers for the request. You cannot override the ``Authorization`` or
``Content-Type`` headers.
Expand Down Expand Up @@ -246,6 +251,9 @@ def pro_api_request(
else:
raise ValueError("'data' must be one of 'dict' or 'BaseModel'")

if files and (method.lower() == "post"):
pro_req["files"] = files

with self.session.request(**pro_req) as pro_resp:
logger.info("ProAPIRequest %s %s", method.upper(), resource_path)
try:
Expand All @@ -261,7 +269,7 @@ def concurrent_api_requests(
handler: Callable,
arguments: Iterable[Any],
return_model: Optional[Type[BaseModel]] = None,
max_concurrency: int = None,
max_concurrency: Optional[int] = None,
return_exceptions: Optional[bool] = None,
) -> Iterator[Union[Any, Exception]]:
"""An interface for performing concurrent API operations.
Expand Down
203 changes: 190 additions & 13 deletions src/jamf_pro_sdk/clients/pro_api/__init__.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,21 @@
from __future__ import annotations

from typing import TYPE_CHECKING, Callable, Iterator, List, Union
from typing import TYPE_CHECKING, Callable, Iterator, List, Literal, Optional, Union, overload
from uuid import UUID

from ...models.pro.api_options import * # noqa: F403
from ...models.pro.api_options import (
get_computer_inventory_v1_allowed_filter_fields,
get_computer_inventory_v1_allowed_sections,
get_computer_inventory_v1_allowed_sort_fields,
get_mdm_commands_v2_allowed_command_types,
get_mdm_commands_v2_allowed_filter_fields,
get_mdm_commands_v2_allowed_sort_fields,
get_mobile_device_inventory_v2_allowed_filter_fields,
get_mobile_device_inventory_v2_allowed_sections,
get_mobile_device_inventory_v2_allowed_sort_fields,
get_packages_v1_allowed_filter_fields,
get_packages_v1_allowed_sort_fields,
)
from ...models.pro.computers import Computer
from ...models.pro.jcds2 import DownloadUrl, File, NewFile
from ...models.pro.mdm import (
Expand All @@ -21,6 +33,7 @@
ShutDownDeviceCommand,
)
from ...models.pro.mobile_devices import MobileDevice
from ...models.pro.packages import Package
from .pagination import Paginator

if TYPE_CHECKING:
Expand All @@ -42,14 +55,38 @@ def __init__(

# Computer Inventory APIs

@overload
def get_computer_inventory_v1(
self,
sections: Optional[List[str]] = ...,
start_page: int = ...,
end_page: Optional[int] = ...,
page_size: int = ...,
sort_expression: Optional[SortExpression] = ...,
filter_expression: Optional[FilterExpression] = ...,
return_generator: Literal[False] = False,
) -> List[Computer]: ...

@overload
def get_computer_inventory_v1(
self,
sections: Optional[List[str]] = ...,
start_page: int = ...,
end_page: Optional[int] = ...,
page_size: int = ...,
sort_expression: Optional[SortExpression] = ...,
filter_expression: Optional[FilterExpression] = ...,
return_generator: Literal[True] = True,
) -> Iterator[Page]: ...

def get_computer_inventory_v1(
self,
sections: List[str] = None,
sections: Optional[List[str]] = None,
start_page: int = 0,
end_page: int = None,
end_page: Optional[int] = None,
page_size: int = 100,
sort_expression: SortExpression = None,
filter_expression: FilterExpression = None,
sort_expression: Optional[SortExpression] = None,
filter_expression: Optional[FilterExpression] = None,
return_generator: bool = False,
) -> Union[List[Computer], Iterator[Page]]:
"""Returns a list of computer inventory records.
Expand Down Expand Up @@ -132,6 +169,98 @@ def get_computer_inventory_v1(

return paginator(return_generator=return_generator)

# Package APIs

@overload
def get_packages_v1(
self,
start_page: int = ...,
end_page: Optional[int] = ...,
page_size: int = ...,
sort_expression: Optional[SortExpression] = ...,
filter_expression: Optional[FilterExpression] = ...,
return_generator: Literal[False] = False,
) -> List[Package]: ...

@overload
def get_packages_v1(
self,
start_page: int = ...,
end_page: Optional[int] = ...,
page_size: int = ...,
sort_expression: Optional[SortExpression] = ...,
filter_expression: Optional[FilterExpression] = ...,
return_generator: Literal[True] = True,
) -> Iterator[Page]: ...

def get_packages_v1(
self,
start_page: int = 0,
end_page: Optional[int] = None,
page_size: int = 100,
sort_expression: Optional[SortExpression] = None,
filter_expression: Optional[FilterExpression] = None,
return_generator: bool = False,
) -> Union[List[Package], Iterator[Page]]:
"""Returns a list of package records.
:param start_page: (optional) The page to begin returning results from. See
:class:`Paginator` for more information.
:type start_page: int
:param end_page: (optional) The page to end returning results at. See :class:`Paginator` for
more information.
:type start_page: int
:param page_size: (optional) The number of results to include in each requested page. See
:class:`Paginator` for more information.
:type page_size: int
:param sort_expression: (optional) The sort fields to apply to the request. See the
documentation for :ref:`Pro API Sorting` for more information.
Allowed sort fields:
.. autoapioptions:: jamf_pro_sdk.models.pro.api_options.get_packages_v1_allowed_sort_fields
:type sort_expression: SortExpression
:param filter_expression: (optional) The filter expression to apply to the request. See the
documentation for :ref:`Pro API Filtering` for more information.
Allowed filter fields:
.. autoapioptions:: jamf_pro_sdk.models.pro.api_options.get_packages_v1_allowed_filter_fields
:type filter_expression: FilterExpression
:param return_generator: If ``True`` a generator is returned to iterate over pages. By
default, the results for all pages will be returned in a single response.
:type return_generator: bool
:return: List of packages OR a paginator generator.
:rtype: List[~jamf_pro_sdk.models.pro.packages.package] | Iterator[Page]
"""
if sort_expression:
sort_expression.validate(get_packages_v1_allowed_sort_fields)

if filter_expression:
filter_expression.validate(get_packages_v1_allowed_filter_fields)

paginator = Paginator(
api_client=self,
resource_path="v1/packages",
return_model=Package,
start_page=start_page,
end_page=end_page,
page_size=page_size,
sort_expression=sort_expression,
filter_expression=filter_expression,
)

return paginator(return_generator=return_generator)

# JCDS APIs

def get_jcds_files_v1(self) -> List[File]:
Expand Down Expand Up @@ -256,13 +385,35 @@ def send_mdm_command_preview(
resp = self.api_request(method="post", resource_path="preview/mdm/commands", data=data)
return [SendMdmCommandResponse(**i) for i in resp.json()]

@overload
def get_mdm_commands_v2(
self,
filter_expression: FilterExpression,
start_page: int = ...,
end_page: Optional[int] = ...,
page_size: int = ...,
sort_expression: Optional[SortExpression] = ...,
return_generator: Literal[False] = False,
) -> List[MdmCommandStatus]: ...

@overload
def get_mdm_commands_v2(
self,
filter_expression: FilterExpression,
start_page: int = ...,
end_page: Optional[int] = ...,
page_size: int = ...,
sort_expression: Optional[SortExpression] = ...,
return_generator: Literal[True] = True,
) -> Iterator[Page]: ...

def get_mdm_commands_v2(
self,
filter_expression: FilterExpression,
start_page: int = 0,
end_page: int = None,
end_page: Optional[int] = None,
page_size: int = 100,
sort_expression: SortExpression = None,
sort_expression: Optional[SortExpression] = None,
return_generator: bool = False,
) -> Union[List[MdmCommandStatus], Iterator[Page]]:
"""Returns a list of MDM commands.
Expand Down Expand Up @@ -302,7 +453,7 @@ def get_mdm_commands_v2(
the results for all pages will be returned in a single response.
:type return_generator: bool
:return: List of computers OR a paginator generator.
:return: List of MDM commands OR a paginator generator.
:rtype: List[~jamf_pro_sdk.models.pro.mdm.MdmCommand] | Iterator[Page]
"""

Expand Down Expand Up @@ -333,14 +484,40 @@ def get_mdm_commands_v2(

return paginator(return_generator=return_generator)

# Mobile Device Inventory APIs

@overload
def get_mobile_device_inventory_v2(
self,
sections: Optional[List[str]] = ...,
start_page: int = ...,
end_page: Optional[int] = ...,
page_size: int = ...,
sort_expression: Optional[SortExpression] = ...,
filter_expression: Optional[FilterExpression] = ...,
return_generator: Literal[False] = False,
) -> List[MobileDevice]: ...

@overload
def get_mobile_device_inventory_v2(
self,
sections: Optional[List[str]] = ...,
start_page: int = ...,
end_page: Optional[int] = ...,
page_size: int = ...,
sort_expression: Optional[SortExpression] = ...,
filter_expression: Optional[FilterExpression] = ...,
return_generator: Literal[True] = True,
) -> Iterator[Page]: ...

def get_mobile_device_inventory_v2(
self,
sections: List[str] = None,
sections: Optional[List[str]] = None,
start_page: int = 0,
end_page: int = None,
end_page: Optional[int] = None,
page_size: int = 100,
sort_expression: SortExpression = None,
filter_expression: FilterExpression = None,
sort_expression: Optional[SortExpression] = None,
filter_expression: Optional[FilterExpression] = None,
return_generator: bool = False,
) -> Union[List[MobileDevice], Iterator[Page]]:
"""Returns a list of mobile device (iOS and tvOS) inventory records.
Expand Down
Loading

0 comments on commit aebf442

Please sign in to comment.