diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..085d722 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,8 @@ +# Changelog + + + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). diff --git a/docs/contributors/index.rst b/docs/contributors/index.rst index 6c81052..8663c66 100644 --- a/docs/contributors/index.rst +++ b/docs/contributors/index.rst @@ -131,10 +131,8 @@ Here is an example from the Classic API Models page. Follow this pattern for all :nosignatures: ClassicComputerGroup - ClassicComputerGroupMembershipUpdate ClassicComputerGroupMember - ClassicComputerGroupCriterion - ClassicComputerGroupCriterionSearchType + ClassicComputerGroupMembershipUpdate Other ----- diff --git a/docs/reference/models_classic.rst b/docs/reference/models_classic.rst index 4c66526..166f93f 100644 --- a/docs/reference/models_classic.rst +++ b/docs/reference/models_classic.rst @@ -35,6 +35,19 @@ Categories ClassicCategory ClassicCategoriesItem +Advanced Computer Searches +-------------------------- + +.. currentmodule:: jamf_pro_sdk.models.classic.advanced_computer_searches + +.. autosummary:: + :toctree: _autosummary + + ClassicAdvancedComputerSearch + ClassicAdvancedComputerSearchesItem + ClassicAdvancedComputerSearchResult + ClassicAdvancedComputerSearchDisplayField + Computers --------- @@ -75,10 +88,8 @@ Computer Groups :nosignatures: ClassicComputerGroup - ClassicComputerGroupMembershipUpdate ClassicComputerGroupMember - ClassicComputerGroupCriterion - ClassicComputerGroupCriterionSearchType + ClassicComputerGroupMembershipUpdate Network Segments ---------------- @@ -87,7 +98,28 @@ Network Segments .. autosummary:: :toctree: _autosummary - :nosignatures: ClassicNetworkSegment ClassicNetworkSegmentsItem + +Packages +-------- + +.. currentmodule:: jamf_pro_sdk.models.classic.packages + +.. autosummary:: + :toctree: _autosummary + + ClassicPackage + ClassicPackageItem + +Group and Search Criteria +------------------------- + +.. currentmodule:: jamf_pro_sdk.models.classic.criteria + +.. autosummary:: + :toctree: _autosummary + + ClassicCriterion + ClassicCriterionSearchType diff --git a/pyproject.toml b/pyproject.toml index 3079dbe..84c7323 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,6 +2,7 @@ name = "jamf-pro-sdk" dynamic = ["readme", "version"] description = "Jamf Pro SDK for Python" +keywords = ["jamf", "jamf pro", "jss", "jps"] license = {text = "MIT"} requires-python = ">=3.9, <4" dependencies = [ @@ -22,6 +23,12 @@ classifiers = [ ] +[project.urls] +Documentation = "https://macadmins.github.io/jamf-pro-sdk-python" +Source = "https://github.com/macadmins/jamf-pro-sdk-python" +Changelog = "https://github.com/macadmins/jamf-pro-sdk-python/blob/main/CHANGELOG.md" + + [project.optional-dependencies] aws = [ "boto3>=1.26.45,<2" diff --git a/src/jamf_pro_sdk/clients/classic_api.py b/src/jamf_pro_sdk/clients/classic_api.py index ddf1cb7..02af4c8 100644 --- a/src/jamf_pro_sdk/clients/classic_api.py +++ b/src/jamf_pro_sdk/clients/classic_api.py @@ -6,6 +6,11 @@ from defusedxml.ElementTree import fromstring +from ..models.classic.advanced_computer_searches import ( + ClassicAdvancedComputerSearch, + ClassicAdvancedComputerSearchesItem, +) +from ..models.classic.categories import ClassicCategoriesItem, ClassicCategory from ..models.classic.computer_groups import ( ClassicComputerGroup, ClassicComputerGroupMember, @@ -34,7 +39,11 @@ "configurationprofiles", ) #: Valid subsets for the :meth:`~ClassicApi.list_computers` operation. +CategoryId = Union[int, ClassicCategory, ClassicCategoriesItem] ComputerId = Union[int, ClassicComputer, ClassicComputersItem] +AdvancedComputerSearchId = Union[ + int, ClassicAdvancedComputerSearch, ClassicAdvancedComputerSearchesItem +] PackageId = Union[int, ClassicPackage, ClassicPackageItem] @@ -55,6 +64,44 @@ def __init__( self.api_request = request_method self.concurrent_api_requests = concurrent_requests_method + @staticmethod + def _parse_id(model: Union[int, object]) -> int: + """If the model has an ``id`` attribute return that value (most Classic API models have this + as top-level field).If the model is a ``ClassicComputer`` return the nested value. + """ + if hasattr(model, "id"): + return model.id + elif isinstance(model, ClassicComputer): + return model.general.id + else: + return model + + # /categories APIs + + def list_all_categories(self) -> List[ClassicCategoriesItem]: + """Returns a list of all categories. + + :return: List of categories. + :rtype: List[ClassicCategoriesItem] + + """ + resp = self.api_request(method="get", resource_path="categories") + return [ClassicCategoriesItem(**i) for i in resp.json()["categories"]] + + def get_category_by_id(self, category: CategoryId) -> ClassicCategory: + """Returns a single category record using the ID. + + :param category: A category ID or supported Classic API model. + :type category: Union[int, ClassicCategory, ClassicCategoriesItem] + + :return: Category. + :rtype: ClassicCategory + + """ + category_id = ClassicApi._parse_id(category) + resp = self.api_request(method="get", resource_path=f"categories/id/{category_id}") + return ClassicCategory(**resp.json()["category"]) + # /computers APIs def list_all_computers(self, subsets: Iterable[str] = None) -> List[ClassicComputersItem]: @@ -66,7 +113,7 @@ def list_all_computers(self, subsets: Iterable[str] = None) -> List[ClassicCompu :type subsets: Iterable :return: List of computers. - :rtype: List[ClassicComputersItem] + :rtype: List[~jamf_pro_sdk.models.classic.computers.ClassicComputersItem] """ if subsets: @@ -79,25 +126,13 @@ def list_all_computers(self, subsets: Iterable[str] = None) -> List[ClassicCompu resp = self.api_request(method="get", resource_path=path) return [ClassicComputersItem(**i) for i in resp.json()["computers"]] - @staticmethod - def _parse_id(model: Union[int, object]) -> int: - """If the model has an ``id`` attribute return that value (most Classic API models have this - as top-level field).If the model is a ``ClassicComputer`` return the nested value. - """ - if hasattr(model, "id"): - return model.id - elif isinstance(model, ClassicComputer): - return model.general.id - else: - return model - def get_computer_by_id( self, computer: ComputerId, subsets: Iterable[str] = None ) -> ClassicComputer: """Returns a single computer record using the ID. :param computer: A computer ID or supported Classic API model. - :type computer: Union[int, ~jamf_pro_sdk.models.classic.computers.Computer, ComputersItem] + :type computer: Union[int, ClassicComputer, ClassicComputersItem] :param subsets: (optional) This operation accepts subset values to limit the details returned with the computer record. The following subset values are @@ -106,7 +141,7 @@ def get_computer_by_id( ``groupsaccounts``, and ``configurationprofiles``. :return: Computer. - :rtype: ~jamf_pro_sdk.models.classic.computers.Computer + :rtype: ~jamf_pro_sdk.models.classic.computers.ClassicComputer """ computer_id = ClassicApi._parse_id(computer) @@ -129,7 +164,7 @@ def get_computers( the full list of computer IDs. :param computers: (optional) A list of computer IDs or supported Classic API models. - :type computers: List[Union[int, ~jamf_pro_sdk.models.classic.computers.Computer, ComputersItem]] + :type computers: List[Union[int, ClassicComputer, ClassicComputersItem]] :param subsets: (optional) This operation accepts subset values to limit the details returned with the computer record. The following subset values are @@ -138,7 +173,7 @@ def get_computers( ``groupsaccounts``, and ``configurationprofiles``. :return: List of computers. - :rtype: List[~jamf_pro_sdk.models.classic.computers.Computer] + :rtype: List[~jamf_pro_sdk.models.classic.computers.ClassicComputer] """ if not computers: @@ -158,7 +193,7 @@ def update_computer_by_id( Not all fields in a computer record can be updated. :param computer: A computer ID or supported Classic API model. - :type computer: Union[int, ~jamf_pro_sdk.models.classic.computers.Computer, ComputersItem] + :type computer: Union[int, ClassicComputer, ClassicComputersItem] :param data: Can be an XML string or a :class:`~jamf_pro_sdk.models.classic.computers.ClassicComputer` object. @@ -172,7 +207,7 @@ def delete_computer_by_id(self, computer: ComputerId) -> None: """Delete a single computer record using the ID. :param computer: A computer ID or supported Classic API model. - :type computer: Union[int, ~jamf_pro_sdk.models.classic.computers.Computer, ComputersItem] + :type computer: Union[int, ClassicComputer, ClassicComputersItem] """ computer_id = ClassicApi._parse_id(computer) @@ -364,25 +399,103 @@ def update_static_computer_group_membership_by_id( # /advancedcomputersearches APIs - def list_all_advanced_computer_searches(self): - """Not implemented...""" - pass + def create_advanced_computer_search( + self, data: Union[str, ClassicAdvancedComputerSearch] + ) -> int: + """Create a new advanced computer search. - def create_advanced_computer_search(self): - """Not implemented...""" - pass + :param data: Can be an XML string or a + :class:`~jamf_pro_sdk.models.classic.advanced_computer_searches.ClassicAdvancedComputerSearch` + object. + :type data: Union[str, ClassicAdvancedComputerSearch] + + :return: ID of the new computer group. + :rtype: int + + """ + resp = self.api_request(method="post", resource_path="advancedcomputersearches", data=data) + return parse_response_id(resp.text) + + def list_all_advanced_computer_searches(self) -> List[ClassicAdvancedComputerSearchesItem]: + """Returns a list of all advanced computer searches. - def update_advanced_computer_search(self): - """Not implemented...""" - pass + :return: List of advanced computer searches. + :rtype: List[ClassicAdvancedComputerSearchesItem] + + """ + resp = self.api_request(method="get", resource_path="advancedcomputersearches") + return [ + ClassicAdvancedComputerSearchesItem(**i) + for i in resp.json()["advanced_computer_searches"] + ] - def get_advanced_computer_search_by_id(self): - """Not implemented...""" - pass + def get_advanced_computer_search_by_id(self, advanced_search: AdvancedComputerSearchId): + """Returns a single advanced computer search using the ID. - def delete_advanced_computer_search_by_id(self): - """Not implemented...""" - pass + This operation returns the results of the advanced search when called. The ``computers`` + field will contain an array of the results with data fields matching those in the + ``display_fields`` field. + + Results are calculated on each call to this operation. + + :param advanced_search: An advanced computer search ID or supported Classic API model. + :type advanced_search: Union[int, ClassicAdvancedComputerSearch, ClassicAdvancedComputerSearchesItem] + + :return: Advanced computer search. + :rtype: ~jamf_pro_sdk.models.classic.advanced_computer_searches.ClassicAdvancedComputerSearch + + """ + advanced_search_id = ClassicApi._parse_id(advanced_search) + resp = self.api_request( + method="get", resource_path=f"advancedcomputersearches/id/{advanced_search_id}" + ) + return ClassicAdvancedComputerSearch(**resp.json()["advanced_computer_search"]) + + def update_advanced_computer_search_by_id( + self, + advanced_search: AdvancedComputerSearchId, + data: Union[str, ClassicAdvancedComputerSearch], + return_updated: bool = False, + ): + """Update an advanced computer search using the ID. + + :param advanced_search: An advanced computer search ID or supported Classic API model. + :type advanced_search: Union[int, ClassicAdvancedComputerSearch, ClassicAdvancedComputerSearchesItem] + + :param data: Can be an XML string or a + :class:`~jamf_pro_sdk.models.classic.advanced_computer_searches.ClassicAdvancedComputerSearch` + object. + :type data: Union[str, ClassicAdvancedComputerSearch] + + :param return_updated: If ``True`` the :meth:`~jamf_pro_sdk.clients.classic_api.ClassicApi.get_advanced_computer_search_by_id` + operation will be called after the update operation to return the updated results. + :type return_updated: bool + + :return: Advanced computer search if ``return_updated`` is ``True``. + :rtype: Union[None, ~jamf_pro_sdk.models.classic.advanced_computer_searches.ClassicAdvancedComputerSearch] + + """ + advanced_search_id = ClassicApi._parse_id(advanced_search) + self.api_request( + method="put", + resource_path=f"advancedcomputersearches/id/{advanced_search_id}", + data=data, + ) + if return_updated: + return self.get_advanced_computer_search_by_id(advanced_search_id) + + def delete_advanced_computer_search_by_id(self, advanced_search: AdvancedComputerSearchId): + """Delete an advanced computer search using the ID. + + :param advanced_search: An advanced computer search ID or supported Classic API model. + :type advanced_search: Union[int, ClassicAdvancedComputerSearch, ClassicAdvancedComputerSearchesItem] + + """ + advanced_search_id = ClassicApi._parse_id(advanced_search) + self.api_request( + method="delete", + resource_path=f"advancedcomputersearches/id/{advanced_search_id}", + ) # /packages APIs 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 e69de29..214a5ca 100644 --- a/src/jamf_pro_sdk/models/classic/advanced_computer_searches.py +++ b/src/jamf_pro_sdk/models/classic/advanced_computer_searches.py @@ -0,0 +1,70 @@ +from typing import List, Optional + +from pydantic import Extra + +from .. import BaseModel +from . import ClassicApiModel, ClassicSite +from .criteria import ClassicCriterion + +_XML_ARRAY_ITEM_NAMES = { + "advanced_computer_searches": "advanced_computer_search", + "computers": "computer", + "display_fields": "display_field", +} + + +class ClassicAdvancedComputerSearchDisplayField(BaseModel, extra=Extra.allow): + """ClassicAdvancedComputerSearch nested model: advanced_computer_search.display_fields. + + Display fields are additional data that are returned with the results of an advanced search. + Note that the API field names are **not** the same as the display field names. Refer to the + Jamf Pro UI for the supported names. + """ + + name: Optional[str] + + +class ClassicAdvancedComputerSearchResult(BaseModel, extra=Extra.allow): + """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] + + +class ClassicAdvancedComputerSearchesItem(ClassicApiModel): + """Represents an advanced computer search record returned by the + :meth:`~jamf_pro_sdk.clients.classic_api.ClassicApi.list_all_advanced_computer_searches` + operation. + """ + + id: Optional[int] + name: Optional[str] + + +class ClassicAdvancedComputerSearch(ClassicApiModel): + """Represents an advanced computer search record returned by the + :meth:`~jamf_pro_sdk.clients.classic_api.ClassicApi.list_all_advanced_computer_searches` + operation. + + When exporting to XML for a ``POST``/``PUT`` operation, the SDK by default will only + include ``name``, ``site``, ``criteria``, and ``display_fields``. To bypass this behavior + export the model using + :meth:`~jamf_pro_sdk.models.classic.ClassicApiModel.xml` before pasting to the API + operation. + """ + + _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]] diff --git a/src/jamf_pro_sdk/models/classic/categories.py b/src/jamf_pro_sdk/models/classic/categories.py index a9966ed..6e31084 100644 --- a/src/jamf_pro_sdk/models/classic/categories.py +++ b/src/jamf_pro_sdk/models/classic/categories.py @@ -8,14 +8,13 @@ _XML_ARRAY_ITEM_NAMES = {} -class ClassicCategoryItem(BaseModel, extra=Extra.allow): +class ClassicCategoriesItem(BaseModel, extra=Extra.allow): """Represents a category record returned by the - :meth:`~jamf_pro_sdk.clients.classic_api.ClassicApi.list_categories` operation. + :meth:`~jamf_pro_sdk.clients.classic_api.ClassicApi.list_all_categories` operation. """ id: Optional[int] name: Optional[str] - priority: Optional[int] class ClassicCategory(ClassicApiModel): @@ -23,9 +22,9 @@ class ClassicCategory(ClassicApiModel): :meth:`~jamf_pro_sdk.clients.classic_api.ClassicApi.get_category_by_id` operation. When exporting to XML for a ``POST``/``PUT`` operation, the SDK by default will only - include ``name``, and ``priority. To bypass this + include ``name``, and ``priority``. To bypass this behavior export the model using :meth:`~jamf_pro_sdk.models.classic.ClassicApiModel.xml` before - pasting to the API operation. + passing to the API operation. """ _xml_root_name = "category" diff --git a/src/jamf_pro_sdk/models/classic/computer_groups.py b/src/jamf_pro_sdk/models/classic/computer_groups.py index bcf9152..bd9fb71 100644 --- a/src/jamf_pro_sdk/models/classic/computer_groups.py +++ b/src/jamf_pro_sdk/models/classic/computer_groups.py @@ -1,10 +1,10 @@ -from enum import Enum from typing import List, Optional from pydantic import Extra from .. import BaseModel from . import ClassicApiModel, ClassicSite +from .criteria import ClassicCriterion _XML_ARRAY_ITEM_NAMES = { "criteria": "criterion", @@ -14,40 +14,6 @@ } -class ClassicComputerGroupCriterionAndOr(str, Enum): - and_: str = "and" - or_: str = "or" - - -class ClassicComputerGroupCriterionSearchType(str, Enum): - """ComputerGroup nested model: computer_group.criteria.search_type - - .. attention:: - - The supported search types are dependent on the field that is entered in ``name``. - - """ - - is_: str = "is" - is_not: str = "is not" - like: str = "like" - not_like: str = "not like" - has: str = "has" - does_not_have: str = "does not have" - matches_regex: str = "matches regex" - does_not_match_regex: str = "does not match regex" - before_yyyy_mm_dd: str = "before (yyyy-mm-dd)" - after_yyyy_mm_dd: str = "after (yyyy-mm-dd)" - more_than_x_days_ago: str = "more than x days ago" - less_than_x_days_ago: str = "less than x days ago" - current: str = "current" - not_current: str = "not current" - greater_than: str = "greater than" - less_than: str = "less than" - greater_than_or_equal: str = "greater than or equal" - less_than_or_equal: str = "less than or equal" - - # class ClassicComputerGroupsItem(BaseModel, extra=Extra.allow): # """Represents a computer group record returned by the # :meth:`~jamf_pro_sdk.clients.classic_api.ClassicApi.list_computer_groups` operation. @@ -58,21 +24,6 @@ class ClassicComputerGroupCriterionSearchType(str, Enum): # is_smart: bool -class ClassicComputerGroupCriterion(BaseModel, extra=Extra.allow): - """ComputerGroup nested model: computer_group.criteria - - Only valid for smart groups (``is_smart == True``). - """ - - name: str - priority: int - and_or: ClassicComputerGroupCriterionAndOr - search_type: ClassicComputerGroupCriterionSearchType - value: str - opening_paren: bool - closing_paren: bool - - class ClassicComputerGroupMember(BaseModel, extra=Extra.allow): """ComputerGroup nested model: computer_group.computers, computer_group.computer_additions, computer_group.computer_deletions @@ -109,7 +60,7 @@ class ClassicComputerGroup(ClassicApiModel): name: Optional[str] is_smart: Optional[bool] site: Optional[ClassicSite] - criteria: Optional[List[ClassicComputerGroupCriterion]] + criteria: Optional[List[ClassicCriterion]] computers: Optional[List[ClassicComputerGroupMember]] diff --git a/src/jamf_pro_sdk/models/classic/criteria.py b/src/jamf_pro_sdk/models/classic/criteria.py new file mode 100644 index 0000000..e33a9e8 --- /dev/null +++ b/src/jamf_pro_sdk/models/classic/criteria.py @@ -0,0 +1,50 @@ +from enum import Enum + +from pydantic import BaseModel, Extra + + +class ClassicCriterionAndOr(str, Enum): + and_: str = "and" + or_: str = "or" + + +class ClassicCriterionSearchType(str, Enum): + """Supported search types for Classic API criteria. + + .. attention:: + + The supported search types are dependent on the field that is entered in the criterion's + ``name`` field. + + """ + + is_: str = "is" + is_not: str = "is not" + like: str = "like" + not_like: str = "not like" + has: str = "has" + does_not_have: str = "does not have" + matches_regex: str = "matches regex" + does_not_match_regex: str = "does not match regex" + before_yyyy_mm_dd: str = "before (yyyy-mm-dd)" + after_yyyy_mm_dd: str = "after (yyyy-mm-dd)" + more_than_x_days_ago: str = "more than x days ago" + less_than_x_days_ago: str = "less than x days ago" + current: str = "current" + not_current: str = "not current" + greater_than: str = "greater than" + less_than: str = "less than" + greater_than_or_equal: str = "greater than or equal" + less_than_or_equal: str = "less than or equal" + + +class ClassicCriterion(BaseModel, extra=Extra.allow): + """Classic API criterion. Used by Smart Groups and Advanced Searches.""" + + name: str + priority: int + and_or: ClassicCriterionAndOr + search_type: ClassicCriterionSearchType + value: str + opening_paren: bool + closing_paren: bool diff --git a/src/jamf_pro_sdk/models/classic/packages.py b/src/jamf_pro_sdk/models/classic/packages.py index 8559e99..9e9d0d4 100644 --- a/src/jamf_pro_sdk/models/classic/packages.py +++ b/src/jamf_pro_sdk/models/classic/packages.py @@ -9,7 +9,7 @@ class ClassicPackageItem(BaseModel, extra=Extra.allow): - """Represents a computer record returned by the + """Represents a package record returned by the :meth:`~jamf_pro_sdk.clients.classic_api.ClassicApi.list_packages` operation. """