diff --git a/ChangeLog.md b/ChangeLog.md index ea40d82..6f7b8c8 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -1,5 +1,24 @@ # Change Log +## v0.0.2.12 + +### New Features + +- configuration_items: add class ConfigurationItem +- configuration_items: add static methods: get_configuration_items, get_by_id +- Request: add class method get_cis_by_request_id to retrieve configuration items associated with a request by its ID. +- Request: add class method add_cis_to_request_by_id to link configuration items to a request by its ID. +- Request: add class method remove_cis_from_request_by_id to unlink configuration items from a request by its ID. +- Request: add instance method get_cis to retrieve configuration items associated with the current request instance. +- Request: add instance method add_cis to link configuration items to the current request instance. +- Request: add instance method remove_cis to unlink configuration items from the current request instance. + +### Bug Fixes + +- Core: fix issue where 204 status code was not handled correctly +- Core: paging, ensure that '<>' gets removed + + ## v0.0.2.11 ### New Features diff --git a/README.md b/README.md index 1ee24c2..b68b2d0 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ This module is used to interact with the Xurrent API. It provides a set of class apitoken = "********" - baseUrl = "https://api.4me.qa/v1" + baseUrl = "https://api.xurrent.qa/v1" account = "account-name" x_api_helper = XurrentApiHelper(baseUrl, apitoken, account) @@ -37,6 +37,54 @@ This module is used to interact with the Xurrent API. It provides a set of class ``` +#### Configuration Items + +```python + # Example usage of ConfigurationItem class + from xurrent.configuration_items import ConfigurationItem + + # Get a Configuration Item by ID + ci = ConfigurationItem.get_by_id(x_api_helper, ) + print(ci) + + # List all Configuration Items + all_cis = ConfigurationItem.get_configuration_items(x_api_helper) + print(all_cis) + + # List active Configuration Items + active_cis = ConfigurationItem.get_configuration_items(x_api_helper, predefinedFilter="active") + print(active_cis) + + # Update a Configuration Item + updated_ci = ci.update({"name": "Updated Name", "status": "being_repaired"}) + print(updated_ci) + + # Create a new Configuration Item + # creating without specifying the label, takes the last ci of the product and increments the label + # example: "wdc-02" -> "wdc-03" + data = {"name": "New CI", "type": "software", "status": "in_production", "product_id": ""} + new_ci = ConfigurationItem.create(api_helper, data) + print(new_ci) + + # Archive a Configuration Item (must be in an allowed state) + try: + archived_ci = ci.archive() + print(archived_ci) + except ValueError as e: + print(f"Error: {e}") + + # Trash a Configuration Item (must be in an allowed state) + try: + trashed_ci = ci.trash() + print(trashed_ci) + except ValueError as e: + print(f"Error: {e}") + + # Restore a Configuration Item + restored_ci = ci.restore() + print(restored_ci) +``` + #### People ```python @@ -93,6 +141,55 @@ This module is used to interact with the Xurrent API. It provides a set of class ``` +##### Request Configuration Items + +```python + from src.xurrent.requests import Request + + # Get Configuration Items for a Request + request_id = + + + # Add a Configuration Item to a Request + ci_id = + try: + response = Request.add_ci_to_request_by_id(x_api_helper, request_id, ci_id) + print("CI added:", response) + except ValueError as e: + print(f"Error: {e}") + + cis = Request.get_cis_by_request_id(x_api_helper, request_id) + print(cis) + + # Remove a Configuration Item from a Request + try: + response = Request.remove_ci_from_request_by_id(x_api_helper, request_id, ci_id) + print("CI removed:", response) + except ValueError as e: + print(f"Error: {e}") + + # Instance-based example + req = Request.get_by_id(x_api_helper, request_id) + + # Add a CI to this request + try: + response = req.add_ci(ci_id) + print("CI added:", response) + except ValueError as e: + print(f"Error: {e}") + + # Get CIs for this request + cis_instance = req.get_cis() + print(cis_instance) + + # Remove a CI from this request + try: + response = req.remove_ci(ci_id) + print("CI removed:", response) + except ValueError as e: + print(f"Error: {e}") +``` + ##### Request Notes ```python diff --git a/pyproject.toml b/pyproject.toml index 045c819..73061fd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "xurrent" -version = "0.0.2.11" +version = "0.0.2.12" authors = [ { name="Fabian Steiner", email="fabian@stei-ner.net" }, ] diff --git a/src/xurrent/configuration_items.py b/src/xurrent/configuration_items.py new file mode 100644 index 0000000..c714191 --- /dev/null +++ b/src/xurrent/configuration_items.py @@ -0,0 +1,134 @@ +from __future__ import annotations # Needed for forward references +from .core import XurrentApiHelper, JsonSerializableDict +from typing import Optional, List, Dict, Type, TypeVar +from enum import Enum + +T = TypeVar('T', bound='ConfigurationItem') + +class ConfigurationItemPredefinedFilter(str, Enum): + active = "active" # List all active configuration items + inactive = "inactive" # List all inactive configuration items + supported_by_my_teams = "supported_by_my_teams" # List all configuration items supported by the teams of the API user + +class ConfigurationItem(JsonSerializableDict): + # https://developer.xurrent.com/v1/configuration_items/ + __resourceUrl__ = 'cis' + + def __init__(self, + connection_object: XurrentApiHelper, + id: int, + label: Optional[str] = None, + name: Optional[str] = None, + type: Optional[str] = None, + status: Optional[str] = None, + attributes: Optional[Dict] = None, + **kwargs): + self.id = id + self._connection_object = connection_object + self.label = label + self.name = name + self.status = status + self.attributes = attributes or {} + + for key, value in kwargs.items(): + setattr(self, key, value) + + def __str__(self) -> str: + """Provide a human-readable string representation of the object.""" + return f"ConfigurationItem(id={self.id}, label={self.label},name={self.name}, status={self.status})" + + def ref_str(self) -> str: + """Provide a human-readable string representation of the object.""" + return f"ConfigurationItem(id={self.id}, label={self.label})" + + @classmethod + def from_data(cls, connection_object: XurrentApiHelper, data) -> T: + if not isinstance(data, dict): + raise TypeError(f"Expected 'data' to be a dictionary, got {type(data).__name__}") + if 'id' not in data: + raise ValueError("Data dictionary must contain an 'id' field.") + return cls(connection_object, **data) + + @classmethod + def get_by_id(cls, connection_object: XurrentApiHelper, id: int) -> T: + """ + Retrieve a configuration item by its ID. + """ + uri = f'{connection_object.base_url}/{cls.__resourceUrl__}/{id}' + return cls.from_data(connection_object, connection_object.api_call(uri, 'GET')) + + @classmethod + def get_configuration_items(cls, connection_object: XurrentApiHelper, predefinedFilter: ConfigurationItemPredefinedFilter = None, queryfilter: dict = None) -> List[T]: + """ + Retrieve all configuration items. + """ + uri = f'{connection_object.base_url}/{cls.__resourceUrl__}' + if predefinedFilter: + uri = f'{uri}/{predefinedFilter}' + if queryfilter: + uri += '?' + connection_object.create_filter_string(queryfilter) + response = connection_object.api_call(uri, 'GET') + return [cls.from_data(connection_object, ci) for ci in response] + + def update(self, data: dict) -> T: + """ + Update the current configuration item instance with new data. + """ + uri = f'{self._connection_object.base_url}/{self.__resourceUrl__}/{self.id}' + response = self._connection_object.api_call(uri, 'PATCH', data) + return ConfigurationItem.from_data(self._connection_object, response) + + @classmethod + def create(cls, connection_object: XurrentApiHelper, data: dict) -> T: + """ + Create a new configuration item. + """ + uri = f'{connection_object.base_url}/{cls.__resourceUrl__}' + response = connection_object.api_call(uri, 'POST', data) + return cls.from_data(connection_object, response) + + def archive(self) -> T: + """ + Archive the configuration item. + + Allowed statuses for archiving: + - undergoing_maintenance + - broken_down + - being_repaired + - archived + - to_be_removed + - lost_or_stolen + - removed + """ + if self.status not in {"undergoing_maintenance", "broken_down", "being_repaired", "archived", "to_be_removed", "lost_or_stolen", "removed"}: + raise ValueError("Configuration item must be in one of the following statuses to be archived: undergoing_maintenance, broken_down, being_repaired, archived, to_be_removed, lost_or_stolen, removed.") + uri = f'{self._connection_object.base_url}/{self.__resourceUrl__}/{self.id}/archive' + response = self._connection_object.api_call(uri, 'POST') + return ConfigurationItem.from_data(self._connection_object, response) + + def restore(self) -> T: + """ + Restore the configuration item. + """ + uri = f'{self._connection_object.base_url}/{self.__resourceUrl__}/{self.id}/restore' + response = self._connection_object.api_call(uri, 'POST') + return ConfigurationItem.from_data(self._connection_object, response) + + def trash(self) -> T: + """ + Trash the configuration item. + + Allowed statuses for trashing: + - undergoing_maintenance + - broken_down + - being_repaired + - archived + - to_be_removed + - lost_or_stolen + - removed + """ + if self.status not in {"undergoing_maintenance", "broken_down", "being_repaired", "archived", "to_be_removed", "lost_or_stolen", "removed"}: + raise ValueError("Configuration item must be in one of the following statuses to be trashed: undergoing_maintenance, broken_down, being_repaired, archived, to_be_removed, lost_or_stolen, removed.") + uri = f'{self._connection_object.base_url}/{self.__resourceUrl__}/{self.id}/trash' + response = self._connection_object.api_call(uri, 'POST') + return ConfigurationItem.from_data(self._connection_object, response) diff --git a/src/xurrent/core.py b/src/xurrent/core.py index 28449e1..9ad836f 100644 --- a/src/xurrent/core.py +++ b/src/xurrent/core.py @@ -164,6 +164,9 @@ def api_call(self, uri: str, method='GET', data=None, per_page=100): # Make the HTTP request response = requests.request(method, next_page_url, headers=headers, json=data) + if response.status_code == 204: + return None + # Handle rate limiting (429 status code) if response.status_code == 429: retry_after = int(response.headers.get('Retry-After', 1)) # Default to 1 second if not provided @@ -189,6 +192,8 @@ def api_call(self, uri: str, method='GET', data=None, per_page=100): links = {rel.strip(): url.strip('<>') for url, rel in (link.split(';') for link in link_header.split(','))} next_page_url = links.get('rel="next"') + if next_page_url: + next_page_url = next_page_url.replace('<', '').replace('>', '') else: next_page_url = None else: diff --git a/src/xurrent/people.py b/src/xurrent/people.py index 0942b7e..8bdff2d 100644 --- a/src/xurrent/people.py +++ b/src/xurrent/people.py @@ -16,7 +16,7 @@ class PeoplePredefinedFilter(str, Enum): T = TypeVar('T', bound='Person') class Person(JsonSerializableDict): - #https://developer.4me.com/v1/people/ + #https://developer.xurrent.com/v1/people/ __resourceUrl__ = 'people' def __init__(self, connection_object: XurrentApiHelper, id, name: str = None, primary_email: str = None,**kwargs): diff --git a/src/xurrent/requests.py b/src/xurrent/requests.py index f6a393a..7af2115 100644 --- a/src/xurrent/requests.py +++ b/src/xurrent/requests.py @@ -74,7 +74,7 @@ class PredefinedNotesFilter(str, Enum): class Request(JsonSerializableDict): - #https://developer.4me.com/v1/requests/ + #https://developer.xurrent.com/v1/requests/ __resourceUrl__ = 'requests' __references__ = ['workflow', 'requested_by', 'requested_for', 'created_by', 'member', 'team'] workflow: Optional[Workflow] @@ -331,3 +331,96 @@ def create(cls, connection_object: XurrentApiHelper, data: dict): uri = f'{connection_object.base_url}/{cls.__resourceUrl__}' response = connection_object.api_call(uri, 'POST', data) return cls.from_data(connection_object, response) + + # the following methods are for managing configuration items associated with requests + # Developer Documentation: https://developer.xurrent.com/v1/requests/cis + + @classmethod + def get_cis_by_request_id(cls, connection_object: XurrentApiHelper, request_id: int) -> List[ConfigurationItem]: + """ + Retrieve configuration items associated with a request. + + :param connection_object: Xurrent API connection object + :param request_id: ID of the request + :return: List of ConfigurationItem objects + """ + from .configuration_items import ConfigurationItem + uri = f'{connection_object.base_url}/requests/{request_id}/cis' + response = connection_object.api_call(uri, 'GET') + return [ConfigurationItem.from_data(connection_object, ci) for ci in response] + + @classmethod + def add_ci_to_request_by_id(cls, connection_object: XurrentApiHelper, request_id: int, ci_id: int) -> bool: + """ + Link configuration items to a request. + + :param connection_object: Xurrent API connection object + :param request_id: ID of the request + :param ci_id: item ID to link + :return: true if successful, false otherwise + """ + uri = f'{connection_object.base_url}/requests/{request_id}/cis/{ci_id}' + try: + connection_object.api_call(uri, 'POST') + return True + except Exception as e: + return False + + @classmethod + def remove_ci_from_request_by_id(cls, connection_object: XurrentApiHelper, request_id: int, ci_id: int) -> bool: + """ + Unlink configuration items from a request. + + :param connection_object: Xurrent API connection object + :param request_id: ID of the request + :param ci_id: item ID to unlink + :return: true if successful, false otherwise + """ + uri = f'{connection_object.base_url}/requests/{request_id}/cis/{ci_id}' + try: + connection_object.api_call(uri, 'DELETE') + return True + except Exception as e: + return False + + def get_cis(self) -> List[ConfigurationItem]: + """ + Retrieve configuration items associated with this request instance. + + :return: List of ConfigurationItem objects + """ + from .configuration_items import ConfigurationItem + uri = f'{self._connection_object.base_url}/requests/{self.id}/cis' + response = self._connection_object.api_call(uri, 'GET') + return [ConfigurationItem.from_data(self._connection_object, ci) for ci in response] + + def add_ci(self, ci_id: int) -> bool: + """ + Link configuration items to this request instance. + + :param ci_ids: List of configuration item IDs to link + :return: true if successful, false otherwise + """ + + uri = f'{self._connection_object.base_url}/requests/{self.id}/cis/{ci_id}' + try: + self._connection_object.api_call(uri, 'POST') + return True + except Exception as e: + return False + + + def remove_ci(self, ci_id: int) -> bool: + """ + Unlink configuration items from this request instance. + + :param ci_ids: List of configuration item IDs to unlink + :return: true if successful, false otherwise + """ + uri = f'{self._connection_object.base_url}/requests/{self.id}/cis/{ci_id}' + try: + self._connection_object.api_call(uri, 'DELETE') + return True + except Exception as e: + return False + diff --git a/src/xurrent/tasks.py b/src/xurrent/tasks.py index 1da8fb1..7b388dc 100644 --- a/src/xurrent/tasks.py +++ b/src/xurrent/tasks.py @@ -31,7 +31,7 @@ class TaskStatus(str, Enum): class Task(JsonSerializableDict): - #https://developer.4me.com/v1/tasks/ + #https://developer.xurrent.com/v1/tasks/ __resourceUrl__ = 'tasks' def __init__(self, connection_object: XurrentApiHelper, id, subject: str = None, workflow: dict = None,description: str = None, **kwargs): diff --git a/src/xurrent/teams.py b/src/xurrent/teams.py index 1e3ce10..e33575f 100644 --- a/src/xurrent/teams.py +++ b/src/xurrent/teams.py @@ -11,6 +11,7 @@ class TeamPredefinedFilter(str, Enum): T = TypeVar('T', bound='Team') class Team(JsonSerializableDict): + #https://developer.xurrent.com/v1/teams/ __resourceUrl__ = 'teams' def __init__(self, connection_object: XurrentApiHelper, id, name: str = None, description: str = None, **kwargs): diff --git a/src/xurrent/workflows.py b/src/xurrent/workflows.py index ca396a0..d34a13d 100644 --- a/src/xurrent/workflows.py +++ b/src/xurrent/workflows.py @@ -49,7 +49,7 @@ class WorkflowPredefinedFilter(str, Enum): class Workflow(JsonSerializableDict): - # Endpoint for workflows + # https://developer.xurrent.com/v1/workflows/ __resourceUrl__ = 'workflows' def __init__(self,