Skip to content

Commit

Permalink
Merge pull request #46 from cedarville-university/develop
Browse files Browse the repository at this point in the history
Changes for 0.3.0 release
  • Loading branch information
nattyboyme3 authored Nov 18, 2021
2 parents 95dc026 + 68a8ef0 commit d3a563c
Show file tree
Hide file tree
Showing 8 changed files with 395 additions and 34 deletions.
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
setup(
name='TDXLib',
description='a python library for interacting with the TeamDynamix Web API',
version='0.2.14',
version='0.3.0',
author='Nat Biggs, Stephen Gaines, Josiah Lansford',
author_email='tdxlib@cedarville.edu',
packages=['tdxlib'],
Expand Down
2 changes: 1 addition & 1 deletion tdxlib/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@
import tdxlib.tdx_ticket
import tdxlib.tdx_utils

__version__ = "0.2.14"
__version__ = "0.3.0"
197 changes: 177 additions & 20 deletions tdxlib/tdx_asset_integration.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import copy
import datetime
import tdx_utils
import tdxlib.tdx_integration
from tdxlib.tdx_api_exceptions import *

Expand Down Expand Up @@ -383,6 +385,48 @@ def get_asset_by_id(self, asset_id: str) -> dict:
"""
return self.make_call(str(asset_id), 'get')

def add_asset_user(self, asset_id: str, user_uid: str):
"""
Adds a users to an asset
:param asset_id: the ID of the asset to get users
:param user_uid: the UID of the person to add
:return: the API's response (success/failure only)
"""
return self.make_call(f'{asset_id}/users/{user_uid}', 'post', {'data': None})

def get_asset_users(self, asset_id: str) -> list:
"""
Gets users of an asset
:param asset_id: the ID of the asset to get users
:return: list of this asset's users, each represented by a dict
"""
return self.make_call(f'{asset_id}/users', 'get')

def delete_asset_users(self, asset_id: str, users: list):
"""
Deletes specified users of an asset
:param asset_id: the ID of the asset to delete users from
:param users: a list of the users (maybe from get_asset_users()) or user UIDs to delete
:return: list of this asset's users, each represented by a dict
"""
results = list()
if not isinstance(users, list):
users = [users]
for user in users:
if isinstance(user, str):
id_to_delete = user
else:
id_to_delete = user['Value']
results.append(self.make_call(f'{asset_id}/users/{id_to_delete}', 'delete'))

def search_assets(self, criteria, max_results=25, retired=False, disposed=False, full_record=False) -> list:
"""
Searches for assets, based on criteria
Expand Down Expand Up @@ -620,41 +664,59 @@ def update_assets(self, assets, changed_attributes: dict, clear_custom_attribute
:param assets: a list of assets (maybe from search_assets()) or a single asset (only ID required)
:param changed_attributes: a dict of attributes in the ticket to be changed
:param clear_custom_attributes: (default: False) Indicates whether or not custom attributes should be cleared
:param clear_custom_attributes: (default: False) Indicates whether or not custom attributes not specified
in the changed_attributes argument should be cleared
:return: list of updated assets
"""
changed_custom_attributes = False
# Get everything into a list
if not isinstance(assets, list):
asset_list = list()
asset_list.append(assets)
else:
asset_list = assets
updated_assets = list()
# Separate CA changes into their own object: 'changed_custom_attributes'.'
# need to make a full copy of this dict so we can reuse it
changed_attributes_copy = copy.deepcopy(changed_attributes)
if 'Attributes' in changed_attributes:
if isinstance(changed_attributes['Attributes'], list):
changed_custom_attributes = changed_attributes['Attributes']
else:
changed_custom_attributes = [changed_attributes['Attributes']]
del changed_attributes['Attributes']
# Remove attributes field of "changed_attributes" so it doesn't mess up the existing asset(s) CA's
# need to set it to an empty list first so it doesn't delete the object that was passed in
del changed_attributes_copy['Attributes']
for this_asset in asset_list:
this_asset = self.get_asset_by_id(this_asset['ID'])
if 'Attributes' not in this_asset.keys():
this_asset['Attributes'] = []
if changed_custom_attributes and not clear_custom_attributes:
# Need to get the full record so that we can see existing CA's
if isinstance(this_asset, str) or isinstance(this_asset, int):
full_asset = self.get_asset_by_id(this_asset)
else:
full_asset = self.get_asset_by_id(this_asset['ID'])
# not totally sure the first part is necessary. I think it always comes through as an empty list if no CA's
if 'Attributes' not in full_asset.keys() or clear_custom_attributes:
full_asset['Attributes'] = []
# we take this branch if we have attributes to update, and we're not clobbering existing CA's
if changed_custom_attributes:
# Loop through each of the CAs to be changed
for new_attrib in changed_custom_attributes:
# Drop a marker so we know if
new_attrib_marker = True
for attrib in this_asset['Attributes']:
# Loop through the existing CA's, to look for stuff to update
for attrib in full_asset['Attributes']:
# if we find a match, we update it in the existing asset record
if str(new_attrib['ID']) == str(attrib['ID']):
attrib['Value'] = new_attrib['Value']
new_attrib_marker = False
# if we go through all the asset's CA's, an haven't updated something, we just put it in.
if new_attrib_marker:
this_asset['Attributes'].append(new_attrib)
if clear_custom_attributes:
this_asset['Attributes'] = []
this_asset.update(changed_attributes)
updated_assets.append(self.make_call(str(this_asset['ID']), 'post', this_asset))
full_asset['Attributes'].append(new_attrib)
# incorporate the non-custom changed attributes to the existing asset record
full_asset.update(changed_attributes_copy)
# Call a post with the existing asset record to update the values
updated_assets.append(self.make_call(str(full_asset['ID']), 'post', full_asset))
return updated_assets

def change_asset_owner(self, asset, new_owner, new_dept=None) -> list:
Expand Down Expand Up @@ -688,6 +750,39 @@ def change_asset_owner(self, asset, new_owner, new_dept=None) -> list:
)
return self.update_assets(asset, changed_attributes)

def change_asset_location(self, asset, new_location, new_room=None):
"""
Updates Location data in a list of assets
:param asset: asset to update (doesn't have to be full record), or list of same
:param new_location: name of new location, or dict of location data
:param new_room: name of new room, or dict of room data
:return: list of the updated assets
"""
changed_attributes = dict()
if isinstance(new_location, str):
location = self.get_location_by_name(new_location)
changed_attributes['LocationID'] = location['ID']
elif isinstance(new_location, dict):
location = new_location
changed_attributes['LocationID'] = new_location['ID']
else:
raise TdxApiObjectTypeError(
f"Department of type {str(type(new_location))} not searchable."
)
if new_room:
if isinstance(new_room, str):
changed_attributes['LocationRoomID'] = self.get_room_by_name(location, new_room)
elif isinstance(new_location, dict):
changed_attributes['LocationRoomID'] = new_room['ID']
else:
raise TdxApiObjectTypeError(
f"Department of type {str(type(new_location))} not searchable."
)
return self.update_assets(asset, changed_attributes)

def change_asset_requesting_dept(self, asset, new_dept)-> list:
"""
Updates Requesting Department data in a list of assets
Expand Down Expand Up @@ -730,6 +825,8 @@ def build_asset_custom_attribute_value(self, custom_attribute, value) -> dict:
if len(ca['Choices']) > 0:
ca_choice = self.get_custom_attribute_choice_by_name_id(ca, value)
value = ca_choice['ID']
if isinstance(value, datetime.datetime):
value = tdx_utils.export_tdx_date(value)
return {'ID': ca['ID'], 'Value': value}

def change_asset_custom_attribute_value(self, asset, custom_attributes: list) -> list:
Expand All @@ -744,6 +841,49 @@ def change_asset_custom_attribute_value(self, asset, custom_attributes: list) ->
to_change = {'Attributes': custom_attributes}
return self.update_assets(asset, to_change)

def clear_asset_custom_attributes(self, asset: dict, attributes_to_clear: list):
"""
Takes a list of CA names and removes those custom attributes from the provided asset
:param asset: asset to update (doesn't have to be full record)
:param attributes_to_clear: List of names of custom attributes to remove
:return: the updated asset in dict format
"""
if not isinstance(attributes_to_clear, list):
attributes_to_clear = [attributes_to_clear]
full_asset = self.get_asset_by_id(asset['ID'])
to_change = {'Attributes': []}
for ca in full_asset['Attributes']:
if ca['Name'] not in attributes_to_clear:
to_change['Attributes'].append(ca)
return self.update_assets(full_asset, to_change, clear_custom_attributes=True)[0]

def get_asset_custom_attribute_value_by_name(self, asset, key: str, id: bool=False) -> str:
"""
Returns the current value of a specific CA in the specified asset
:param asset: asset to get CA value for, in dict or ID form
:param key: Name or ID of CA to find in the asset
:param id: (Default: False) Return the ID of the value, instead of ValueText (only for choice-based CA's)
:return: a string representation of the value or ID
"""
if isinstance(asset, str):
this_asset = self.get_asset_by_id(asset)
elif isinstance(asset, dict):
this_asset = asset
else:
raise TdxApiObjectTypeError(
f"Asset of type {str(type(asset))} not searchable."
)
ca_id = self.get_asset_custom_attribute_by_name_id(key)['ID']
for ca in this_asset['Attributes']:
if str(ca['ID']) == str(ca_id):
if ca['Choices'] and id:
return ca['Value']
else:
return ca['ValueText']

def move_child_assets(self, source_asset: dict, target_asset: dict) -> list:
"""
Moves child assets from one parent asset to another
Expand All @@ -759,7 +899,8 @@ def move_child_assets(self, source_asset: dict, target_asset: dict) -> list:
children = self.search_assets(search_params)
return self.update_assets(children, update_params)

def copy_asset_attributes(self, source_asset, target_asset, copy_name=False, exclude=None, new_status: str = None):
def copy_asset_attributes(self, source_asset, target_asset, copy_name=False, exclude=None, new_status: str = None,
new_name: str = None, is_full_source=False):
"""
Copies asset attributes from one asset to another. Does not include attributes like Serial Number, Asset Tag,
and other hardware-specific fields.
Expand All @@ -769,24 +910,36 @@ def copy_asset_attributes(self, source_asset, target_asset, copy_name=False, exc
:param copy_name: Set to true to copy the name of the source asset to the target asset
:param exclude: List of attributes to be excluded, in addition to defaults
:param new_status: Name or ID of new status for source asset
:param new_name: New name for source asset (usually used with copy_name=True). Default: False
:param is_full_source: Boolean indicating whether source_asset is a full asset record or not. Default: False
:return: list of the target and source asset data
"""
excluded_attributes = ['SerialNumber', 'Tag', 'ExternalID', 'ModelID', 'SupplierID', 'ManufacturerID',
'PurchaseCost', 'ExpectedReplacementDate', 'AcquisitionDate']
excluded_attributes = ['ID', 'SerialNumber', 'Tag', 'ExternalID', 'ModelID', 'SupplierID', 'ManufacturerID',
'PurchaseCost', 'ExpectedReplacementDate', 'AcquisitionDate', 'MAC Address',
'WiFi MAC Address', 'Year Purchased', 'Warranty Expiration Date', 'Order Number',
'cu.Responsible Group', 'OwningCustomerID', 'OwningDepartmentID']
if exclude:
excluded_attributes.append(exclude)
if not copy_name:
excluded_attributes.append('Name')
full_source = dict(self.get_asset_by_id(source_asset['ID']))
if is_full_source:
full_source = source_asset
else:
full_source = self.get_asset_by_id(source_asset['ID'])
source_id = full_source['ID']
for protected_attribute in excluded_attributes:
full_source.pop(protected_attribute, None)
updated_target = self.update_assets(target_asset, full_source)
updated_source = None
if new_status:
update_params = {'StatusID': self.get_asset_status_by_name_id(new_status)}
updated_source = self.update_assets(full_source, update_params)
if new_status or new_name or isinstance(new_name, str):
update_params = dict()
if new_status:
update_params['StatusID'] = self.get_asset_status_by_name_id(new_status)['ID']
if new_name or isinstance(new_name, str):
update_params['Name'] = new_name
updated_source = self.update_assets(source_id, update_params)
return [updated_target, updated_source]

def build_asset(self, asset_name, serial_number, status_name, location_name=None, room_name=None,
Expand All @@ -812,7 +965,7 @@ def build_asset(self, asset_name, serial_number, status_name, location_name=None
:param external_id: String with external id for new asset (Default: serial Number)
:param product_model: String with name of product model
:param form: Name of the Asset form to use
:param asset_custom_attributes: a dictionary of asset custom attribute values
:param asset_custom_attributes: a dictionary of asset custom attribute values (or list from asset['Attributes'])
:return: dict usable in create_asset()
Expand All @@ -838,6 +991,10 @@ def build_asset(self, asset_name, serial_number, status_name, location_name=None
# set up attribute values
if asset_custom_attributes:
data['Attributes'] = []
if isinstance(asset_custom_attributes, dict) and 'Attributes' in asset_custom_attributes.keys():
asset_custom_attributes = asset_custom_attributes['Attributes']
if isinstance(asset_custom_attributes, list):
asset_custom_attributes = {i['Name']: i['Value'] for i in asset_custom_attributes}
for attrib_name, value in asset_custom_attributes.items():
new_attrib = self.build_asset_custom_attribute_value(attrib_name, value)
data['Attributes'].append(new_attrib)
Expand Down
4 changes: 3 additions & 1 deletion tdxlib/tdx_ticket_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,9 @@ def get_ticket_by_id(self, ticket_id: int) -> tdxlib.tdx_ticket.TDXTicket:
:rtype: dict
"""
return tdxlib.tdx_ticket.TDXTicket(self, self.make_call(str(ticket_id), 'get'))
ticket_data = self.make_call(str(ticket_id), 'get')
if ticket_data:
return tdxlib.tdx_ticket.TDXTicket(self, ticket_data)

def search_tickets(self, criteria: dict, max_results: int = 25, closed: bool = False, cancelled: bool = False,
other_status: bool = False) -> list:
Expand Down
3 changes: 3 additions & 0 deletions tdxlib/tdx_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import datetime
import json


# Prints out dict as JSON with indents
def print_nice(myjson):
print(json.dumps(myjson, indent=4))
Expand All @@ -22,6 +23,7 @@ def print_simple(my_json, attributes=None):
if i in j:
print(i,':\t', j[i])


# Print only ['Name'] attribute of list of objects
def print_names(myjson):
if isinstance(myjson,list):
Expand All @@ -32,6 +34,7 @@ def print_names(myjson):
if 'Name' in i:
print(i['Name'])


# Imports a string from a TDX Datetime attribute, returns a python datetime object
def import_tdx_date(date_string: str) -> datetime:
return dateutil.parser.parse(date_string)
Expand Down
Loading

0 comments on commit d3a563c

Please sign in to comment.