Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- Problems: added `Problem` class with `ProblemPredefinedFilter`, `ProblemStatus`, and `ProblemImpact` enums; supports CRUD, archive/trash/restore, and sub-resources (requests, workflows, notes).
- ServiceInstances: added `ServiceInstance` class with `ServiceInstancePredefinedFilter` and `ServiceInstanceStatus` enums; supports CRUD and sub-resources (cis, slas, users).
- Releases: added `Release` class with `ReleasePredefinedFilter`, `ReleaseStatus`, and `ReleaseImpact` enums; supports CRUD, archive/trash/restore, and sub-resources (workflows, notes).
- Projects: added `Project` class with `ProjectPredefinedFilter`, `ProjectStatus`, and `ProjectCategory` enums; supports CRUD, archive/trash/restore, and sub-resources (tasks, phases, workflows, risks, notes).
- Contracts: added `Contract` class with `ContractPredefinedFilter` and `ContractStatus` enums; supports CRUD and CI listing.
- KnowledgeArticles: added `KnowledgeArticle` class with `KnowledgeArticlePredefinedFilter` and `KnowledgeArticleStatus` enums; supports CRUD, archive/trash/restore, and sub-resources (requests, service_instances, translations).
- Risks: added `Risk` class with `RiskPredefinedFilter`, `RiskStatus`, and `RiskSeverity` enums; supports CRUD, archive/trash/restore, and sub-resources (organizations, projects, services).
- ServiceOfferings: added `ServiceOffering` class with `ServiceOfferingPredefinedFilter` and `ServiceOfferingStatus` enums; supports CRUD.
- SkillPools: added `SkillPool` class with `SkillPoolPredefinedFilter` enum; supports CRUD, enable/disable, and sub-resources (members, effort_classes).
- Requests: added `get_attachments`, `get_knowledge_articles`, `get_automation_rules`, `get_satisfaction_feedback`, `get_tags`, and `get_watches` instance methods.
- Tasks: added `get_notes`, `add_note`, `get_approvals`, `get_cis`, `get_predecessors`, `get_successors`, `get_service_instances`, and `get_automation_rules` instance methods.
- Workflows: added `get_notes`, `add_note`, `get_automation_rules`, `get_phases`, `get_requests`, and `get_problems` instance methods.
- People: added `get_cis`, `get_addresses`, `get_contacts`, `get_permissions`, `get_ci_coverages`, `get_sla_coverages`, `get_service_coverages`, `get_out_of_office_periods`, and `get_skill_pools` instance methods.
- Organizations: added `get_addresses`, `get_contacts`, `get_contracts`, `get_risks`, `get_slas`, and `get_time_allocations` instance methods.
- Services: added `get_workflows`, `get_request_templates`, `get_risks`, `get_service_instances`, `get_slas`, and `get_service_offerings` instance methods.
- Calendars: added `get_duration`, `get_hours`, and `get_holidays` instance methods.
- Teams: added `get_service_instances` instance method.
- Holidays: added `get_calendars` instance method.
- ClosureCodes: added `ClosureCode` class; supports CRUD.
- Core: added `search(query, types)` for cross-resource full-text search via `GET /search`.
- Core: added `bulk_import(data, import_type, import_format)` for CSV/TSV bulk imports via `POST /import`.
- Core: added `list_archive(queryfilter)` to list all archived items via `GET /archive`.
- Core: added `list_trash(queryfilter)` to list all trashed items via `GET /trash`.
- Core: added `list_audit_lines(queryfilter)` to query the global audit log via `GET /audit_lines`.
- Docs: added `CLAUDE.md` with setup instructions, test commands, architecture overview, and changelog requirements for Claude Code.
- Products: added `Product` class with `ProductPredefinedFilter` and `ProductDepreciationMethod` enums; supports CRUD, enable/disable, and CI listing.
- ProductCategories: added `ProductCategory` class with `ProductCategoryRuleSet` enum; supports CRUD and enable/disable.
Expand Down
35 changes: 35 additions & 0 deletions src/xurrent/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
from .core import XurrentApiHelper, JsonSerializableDict
from .calendars import Calendar, CalendarPredefinedFilter
from .closure_codes import ClosureCode
from .configuration_items import ConfigurationItem, ConfigurationItemPredefinedFilter
from .contracts import Contract, ContractPredefinedFilter, ContractStatus
from .custom_collection_elements import CustomCollectionElement, CustomCollectionElementPredefinedFilter
from .custom_collections import CustomCollection, CustomCollectionPredefinedFilter
from .effort_classes import EffortClass, EffortClassPredefinedFilter
from .holidays import Holiday
from .knowledge_articles import KnowledgeArticle, KnowledgeArticlePredefinedFilter, KnowledgeArticleStatus
from .organizations import Organization, OrganizationPredefinedFilter
from .out_of_office_periods import OutOfOfficePeriod, OutOfOfficePeriodPredefinedFilter
from .people import Person, PeoplePredefinedFilter
from .problems import Problem, ProblemPredefinedFilter, ProblemStatus, ProblemImpact
from .product_categories import ProductCategory, ProductCategoryRuleSet
from .products import Product, ProductPredefinedFilter, ProductDepreciationMethod
from .projects import Project, ProjectPredefinedFilter, ProjectStatus, ProjectCategory
from .releases import Release, ReleasePredefinedFilter, ReleaseStatus, ReleaseImpact
from .request_templates import RequestTemplate, RequestTemplatePredefinedFilter, RequestTemplateCategory, RequestTemplateStatus, RequestTemplateImpact
from .requests import Request, RequestCategory, RequestStatus, CompletionReason, PredefinedFilter, PredefinedNotesFilter
from .risks import Risk, RiskPredefinedFilter, RiskStatus, RiskSeverity
from .service_instances import ServiceInstance, ServiceInstancePredefinedFilter, ServiceInstanceStatus
from .service_offerings import ServiceOffering, ServiceOfferingPredefinedFilter, ServiceOfferingStatus
from .services import Service, ServicePredefinedFilter
from .shop_article_categories import ShopArticleCategory, ShopArticleCategoryPredefinedFilter
from .shop_articles import ShopArticle, ShopArticlePredefinedFilter, ShopArticleRecurringPeriod
from .shop_order_lines import ShopOrderLine, ShopOrderLinePredefinedFilter, ShopOrderLineStatus, ShopOrderLineRecurringPeriod
from .sites import Site, SitePredefinedFilter
from .skill_pools import SkillPool, SkillPoolPredefinedFilter
from .tasks import Task, TaskPredefinedFilter, TaskStatus
from .teams import Team, TeamPredefinedFilter
from .time_allocations import TimeAllocation, TimeAllocationPredefinedFilter, TimeAllocationCustomerCategory, TimeAllocationServiceCategory, TimeAllocationDescriptionCategory
from .ui_extensions import UiExtension, UiExtensionCategory
from .workflow_templates import WorkflowTemplate, WorkflowTemplatePredefinedFilter, WorkflowTemplateCategory
from .workflows import Workflow, WorkflowCompletionReason, WorkflowStatus, WorkflowCategory, WorkflowPredefinedFilter
28 changes: 28 additions & 0 deletions src/xurrent/calendars.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,31 @@ def enable(self) -> T:

def disable(self) -> T:
return self.update({'disabled': True})

def get_duration(self, start: str, end: str, time_zone: str = None) -> dict:
"""
Calculate the duration between two timestamps according to the calendar.

:param start: Start datetime string (ISO 8601)
:param end: End datetime string (ISO 8601)
:param time_zone: Optional time zone name
:return: Duration data from the API
"""
uri = f'{self._connection_object.base_url}/{self.__resourceUrl__}/{self.id}/duration'
params = f'start={start}&end={end}'
if time_zone:
params += f'&time_zone={time_zone}'
uri += f'?{params}'
return self._connection_object.api_call(uri, 'GET')

def get_hours(self) -> List[dict]:
"""Retrieve the working hours of the calendar."""
uri = f'{self._connection_object.base_url}/{self.__resourceUrl__}/{self.id}/hours'
return self._connection_object.api_call(uri, 'GET')

def get_holidays(self) -> List:
"""Retrieve the holidays associated with this calendar."""
from .holidays import Holiday
uri = f'{self._connection_object.base_url}/{self.__resourceUrl__}/{self.id}/holidays'
response = self._connection_object.api_call(uri, 'GET')
return [Holiday.from_data(self._connection_object, h) for h in response]
61 changes: 61 additions & 0 deletions src/xurrent/closure_codes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
from __future__ import annotations
from typing import Optional, List, TypeVar
from .core import XurrentApiHelper, JsonSerializableDict

T = TypeVar('T', bound='ClosureCode')


class ClosureCode(JsonSerializableDict):
# https://developer.xurrent.com/v1/closure_codes/
__resourceUrl__ = 'closure_codes'

def __init__(self,
connection_object: XurrentApiHelper,
id: int,
name: Optional[str] = None,
**kwargs):
self.id = id
self._connection_object = connection_object
self.name = name

for key, value in kwargs.items():
setattr(self, key, value)

def __str__(self) -> str:
return f"ClosureCode(id={self.id}, name={self.name})"

def ref_str(self) -> str:
return f"ClosureCode(id={self.id}, name={self.name})"

@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:
uri = f'{connection_object.base_url}/{cls.__resourceUrl__}/{id}'
return cls.from_data(connection_object, connection_object.api_call(uri, 'GET'))

@classmethod
def get_closure_codes(cls, connection_object: XurrentApiHelper,
queryfilter: dict = None) -> List[T]:
uri = f'{connection_object.base_url}/{cls.__resourceUrl__}'
if queryfilter:
uri += '?' + connection_object.create_filter_string(queryfilter)
response = connection_object.api_call(uri, 'GET')
return [cls.from_data(connection_object, item) for item in response]

def update(self, data: dict) -> T:
uri = f'{self._connection_object.base_url}/{self.__resourceUrl__}/{self.id}'
response = self._connection_object.api_call(uri, 'PATCH', data)
return ClosureCode.from_data(self._connection_object, response)

@classmethod
def create(cls, connection_object: XurrentApiHelper, data: dict) -> T:
uri = f'{connection_object.base_url}/{cls.__resourceUrl__}'
response = connection_object.api_call(uri, 'POST', data)
return cls.from_data(connection_object, response)
97 changes: 97 additions & 0 deletions src/xurrent/contracts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
from __future__ import annotations
from typing import Optional, List, TypeVar
from .core import XurrentApiHelper, JsonSerializableDict
from enum import Enum

T = TypeVar('T', bound='Contract')


class ContractPredefinedFilter(str, Enum):
active = "active"
inactive = "inactive"

def __str__(self):
return self.value


class ContractStatus(str, Enum):
being_created = "being_created"
active = "active"
expired = "expired"

def __str__(self):
return self.value


class Contract(JsonSerializableDict):
# https://developer.xurrent.com/v1/contracts/
__resourceUrl__ = 'contracts'

def __init__(self,
connection_object: XurrentApiHelper,
id: int,
name: Optional[str] = None,
status: Optional[str] = None,
customer=None,
**kwargs):
self.id = id
self._connection_object = connection_object
self.name = name
self.status = ContractStatus(status) if isinstance(status, str) else status

from .organizations import Organization
self.customer = (customer if isinstance(customer, Organization)
else Organization.from_data(connection_object, customer) if customer else None)

for key, value in kwargs.items():
setattr(self, key, value)

def __str__(self) -> str:
return f"Contract(id={self.id}, name={self.name}, status={self.status})"

def ref_str(self) -> str:
return f"Contract(id={self.id}, name={self.name})"

@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:
uri = f'{connection_object.base_url}/{cls.__resourceUrl__}/{id}'
return cls.from_data(connection_object, connection_object.api_call(uri, 'GET'))

@classmethod
def get_contracts(cls, connection_object: XurrentApiHelper,
predefinedFilter: ContractPredefinedFilter = None,
queryfilter: dict = None) -> List[T]:
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, item) for item in response]

def update(self, data: dict) -> T:
uri = f'{self._connection_object.base_url}/{self.__resourceUrl__}/{self.id}'
response = self._connection_object.api_call(uri, 'PATCH', data)
return Contract.from_data(self._connection_object, response)

@classmethod
def create(cls, connection_object: XurrentApiHelper, data: dict) -> T:
uri = f'{connection_object.base_url}/{cls.__resourceUrl__}'
response = connection_object.api_call(uri, 'POST', data)
return cls.from_data(connection_object, response)

def get_cis(self, queryfilter: dict = None) -> List:
from .configuration_items import ConfigurationItem
uri = f'{self._connection_object.base_url}/{self.__resourceUrl__}/{self.id}/cis'
if queryfilter:
uri += '?' + self._connection_object.create_filter_string(queryfilter)
response = self._connection_object.api_call(uri, 'GET')
return [ConfigurationItem.from_data(self._connection_object, item) for item in response]
59 changes: 59 additions & 0 deletions src/xurrent/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,65 @@ def bulk_export(self, type: str, export_format='csv', save_as=None, poll_timeout
return True
return result

def search(self, query: str, types: list = None) -> list:
"""
Perform a cross-resource full-text search.
:param query: Search query string
:param types: Optional list of resource types to search (e.g. ['request', 'person'])
:return: List of search results
"""
uri = f'/search?q={query}'
if types:
uri += '&types=' + ','.join(types)
return self.api_call(uri, 'GET')

def bulk_import(self, data: str, import_type: str, import_format: str = 'csv') -> dict:
"""
Perform a bulk import of records.
:param data: CSV/TSV data as a string
:param import_type: Resource type to import (e.g. 'people', 'configuration_items')
:param import_format: Format of the import data ('csv' or 'tsv', default: 'csv')
:return: Import result from the API
"""
return self.api_call('/import', method='POST', data={
'type': import_type,
'import_format': import_format,
'data': data
})

def list_archive(self, queryfilter: dict = None) -> list:
"""
List all archived items.
:param queryfilter: Optional query filter parameters
:return: List of archived items
"""
uri = '/archive'
if queryfilter:
uri += '?' + self.create_filter_string(queryfilter)
return self.api_call(uri, 'GET')

def list_trash(self, queryfilter: dict = None) -> list:
"""
List all trashed items.
:param queryfilter: Optional query filter parameters
:return: List of trashed items
"""
uri = '/trash'
if queryfilter:
uri += '?' + self.create_filter_string(queryfilter)
return self.api_call(uri, 'GET')

def list_audit_lines(self, queryfilter: dict = None) -> list:
"""
List audit log entries.
:param queryfilter: Optional query filter parameters
:return: List of audit log entries
"""
uri = '/audit_lines'
if queryfilter:
uri += '?' + self.create_filter_string(queryfilter)
return self.api_call(uri, 'GET')

def custom_fields_to_object(self, custom_fields):
"""
Convert a list of custom fields to a dictionary.
Expand Down
7 changes: 7 additions & 0 deletions src/xurrent/holidays.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,10 @@ def create(cls, connection_object: XurrentApiHelper, data: dict) -> T:
uri = f'{connection_object.base_url}/{cls.__resourceUrl__}'
response = connection_object.api_call(uri, 'POST', data)
return cls.from_data(connection_object, response)

def get_calendars(self) -> List:
"""Retrieve calendars that contain this holiday."""
from .calendars import Calendar
uri = f'{self._connection_object.base_url}/{self.__resourceUrl__}/{self.id}/calendars'
response = self._connection_object.api_call(uri, 'GET')
return [Calendar.from_data(self._connection_object, c) for c in response]
Loading