Skip to content

Commit f1c3c89

Browse files
committed
refactor
1 parent 613b85e commit f1c3c89

8 files changed

Lines changed: 449 additions & 3 deletions

File tree

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "alma-python-packages"
3-
version = "1.4.13"
3+
version = "1.4.14"
44
description = "Python module for making requests to the Alma API"
55
readme = "README.md"
66
requires-python = ">=3.14"
@@ -21,4 +21,4 @@ requires = ["hatchling"]
2121
build-backend = "hatchling.build"
2222

2323
[tool.hatch.build.targets.wheel]
24-
packages = "alma_python_packages"
24+
package = "src/alma_python_packages"

src/alma_python_packages/__init__.py

Whitespace-only changes.
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import xmljson
2+
from xml.etree.ElementTree import fromstring
3+
import re
4+
from collections import OrderedDict
5+
6+
7+
class AlmaAnalyticsException(Exception):
8+
"""Custom docstring"""
9+
10+
11+
class AlmaAnalyticsParser:
12+
13+
def __init__(self, i):
14+
def __parse_analytics__(xml_string):
15+
urn_schema = '{urn:schemas-microsoft-com:xml-analysis:rowset}'
16+
w3_schema = '{http://www.w3.org/2001/XMLSchema}'
17+
18+
raw_data = xmljson.badgerfish.data(fromstring(xml_string))
19+
20+
def __get_column_names__():
21+
snake_case_pattern = re.compile(r'(?<!^)(?=[A-Z])')
22+
23+
column_names = \
24+
raw_data['report']['QueryResult']['ResultXml'][urn_schema + 'rowset'][w3_schema + 'schema'][
25+
w3_schema + 'complexType'][w3_schema + 'sequence'][w3_schema + 'element']
26+
27+
temp_column_names = []
28+
for column in column_names[1:]: # Remove first column since its just the integer
29+
for attribute, attribute_value in column.items():
30+
if attribute == '@{urn:saw-sql}columnHeading':
31+
temp_column_names.append(
32+
snake_case_pattern.sub('_', attribute_value).lower().replace(' ', ''))
33+
34+
return temp_column_names
35+
36+
def __get_rows__():
37+
try:
38+
return raw_data['report']['QueryResult']['ResultXml'][urn_schema + 'rowset'][urn_schema + 'Row']
39+
except KeyError:
40+
return []
41+
42+
column_names = __get_column_names__()
43+
temp_table = []
44+
rows = __get_rows__()
45+
if type(rows) is list: # More than one item in the list
46+
for row in __get_rows__():
47+
row.popitem(last=False) # Remove the integer column
48+
temp_row = OrderedDict()
49+
iter = 0
50+
for column, column_value in row.items():
51+
try:
52+
temp_row[column_names[int(column[-1:])-1]] = column_value['$']
53+
except ValueError:
54+
raise (AlmaAnalyticsException('Failed to load column number'))
55+
56+
temp_table.append(temp_row)
57+
elif type(rows) is OrderedDict:
58+
rows.popitem(last=False) # Remove the integer column
59+
temp_row = OrderedDict()
60+
iter = 0
61+
for column, column_value in rows.items():
62+
try:
63+
temp_row[column_names[iter]] = column_value['$']
64+
iter = iter + 1
65+
except ValueError:
66+
raise (AlmaAnalyticsException('Failed to load column number'))
67+
68+
temp_table.append(temp_row)
69+
70+
return temp_table
71+
72+
self.list = __parse_analytics__(i)
73+
74+
def get_table(self):
75+
return self.list
76+
77+
def get_column(self, column_name):
78+
temp_list = []
79+
try:
80+
for row in self.list:
81+
temp_list.append(row[column_name])
82+
return temp_list
83+
except IndexError:
84+
return None

src/alma_python_packages/api.py

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import httplib2 as http
2+
import xml.etree.ElementTree as ElementTree
3+
import json
4+
5+
6+
class AlmaAPIException(Exception):
7+
"""Custom docstring"""
8+
9+
10+
class AlmaAPI:
11+
12+
def __init__(self, api, api_key):
13+
self.api_url = 'https://api-eu.hosted.exlibrisgroup.com/almaws/v1/' + api + '/'
14+
self.api_key = '?apikey=' + api_key
15+
16+
def get(self, *, request=False, query_params=None):
17+
if request is False:
18+
return False
19+
20+
__query_params__ = ''
21+
if query_params is not None:
22+
for key, value in query_params.items():
23+
__query_params__ += '&' + key + '=' + value
24+
25+
url = self.api_url + request + self.api_key + __query_params__
26+
27+
try:
28+
(response, content) = http.Http().request(url)
29+
except TimeoutError as e:
30+
raise AlmaAPIException('GET - ' + str(e))
31+
32+
try:
33+
if response.status != 200:
34+
if query_params['format'] == 'json':
35+
error_data = json.loads(content)
36+
try:
37+
error_message = error_data['errorList']['error'][0]['errorMessage']
38+
except KeyError as e:
39+
error_message = 'Unknown Error'
40+
raise AlmaAPIException('GET - ' + str(response.status) + ' - ' + error_message)
41+
else:
42+
root = ElementTree.ElementTree(ElementTree.fromstring(content)).getroot()
43+
error_message = root[1][0][1].text
44+
raise AlmaAPIException('GET - ' + str(response.status) + ' - ' + error_message)
45+
except ElementTree.ParseError as e:
46+
raise AlmaAPIException('GET - ' + str(e))
47+
48+
return content.decode('utf8')
49+
50+
def put(self, *, request=False, body=False, query_params=None, content_type='application/xml'):
51+
if request is False or body is False:
52+
return False
53+
54+
__query_params__ = ''
55+
if query_params is not None:
56+
for key, value in query_params.items():
57+
__query_params__ += '&' + key + '=' + value
58+
59+
url = self.api_url + request + self.api_key + __query_params__
60+
headers = {'Content-type': content_type}
61+
62+
try:
63+
(response, content) = http.Http().request(url, 'PUT', headers=headers, body=body)
64+
except TimeoutError as e:
65+
raise AlmaAPIException('PUT - ' + str(e))
66+
67+
if response.status != 200:
68+
if query_params['format'] == 'json':
69+
error_data = json.loads(content)
70+
try:
71+
error_message = error_data['errorList']['error'][0]['errorMessage']
72+
except KeyError as e:
73+
error_message = 'Unknown Error'
74+
raise AlmaAPIException('PUT - ' + str(response.status) + ' - ' + error_message)
75+
else:
76+
root = ElementTree.ElementTree(ElementTree.fromstring(content)).getroot()
77+
error_message = root[1][0][1].text
78+
raise AlmaAPIException('PUT - ' + str(response.status) + ' - ' + error_message)
79+
80+
return content.decode('utf8')
81+
82+
def post(self, *, request=False, body=False, query_params=None, content_type='application/xml'):
83+
if request is False or body is False:
84+
return False
85+
86+
__query_params__ = ''
87+
if query_params is not None:
88+
for key, value in query_params.items():
89+
__query_params__ += '&' + key + '=' + value
90+
91+
url = self.api_url + request + self.api_key + __query_params__
92+
headers = {'Content-type': content_type}
93+
94+
try:
95+
(response, content) = http.Http().request(url, 'POST', headers=headers, body=body)
96+
except TimeoutError as e:
97+
raise AlmaAPIException('POST - ' + str(e))
98+
99+
if response.status != 200:
100+
if query_params['format'] == 'json':
101+
error_data = json.loads(content)
102+
try:
103+
error_message = error_data['errorList']['error'][0]['errorMessage']
104+
except KeyError as e:
105+
error_message = 'Unknown Error'
106+
raise AlmaAPIException('POST - ' + str(response.status) + ' - ' + error_message)
107+
else:
108+
root = ElementTree.ElementTree(ElementTree.fromstring(content)).getroot()
109+
error_message = root[1][0][1].text
110+
raise AlmaAPIException('POST - ' + str(response.status) + ' - ' + error_message)
111+
112+
return content.decode('utf8')

src/alma_python_packages/marc.py

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
import xml.etree.ElementTree as ElementTree
2+
from io import BytesIO as bytes
3+
4+
5+
class AlmaMARCException(Exception):
6+
pass
7+
8+
9+
class AlmaMARCRecord:
10+
11+
def __init__(self, input, *, file=False):
12+
13+
# Parse string or file into XML Object
14+
def __parse__(i, f):
15+
if f:
16+
return ElementTree.parse(i)
17+
else:
18+
return ElementTree.ElementTree(ElementTree.fromstring(i))
19+
self.xml = __parse__(input, file)
20+
21+
# Set root and type
22+
root = self.xml.getroot()
23+
self.type = root.tag
24+
25+
# Except if type is not Bib MARC or Holding Marc
26+
if self.type != 'bib' and self.type != 'holding':
27+
raise AlmaMARCException('Not a MARC XML Object')
28+
29+
# Map record fields to a list
30+
self.fields = list(self.xml.findall('.//record/*'))
31+
32+
# Get the attributes of the XMl Object
33+
def __get_attributes__(xml):
34+
record = xml.find('.//record')
35+
attributes = list(xml.findall('./'))
36+
attributes.remove(record)
37+
38+
output = {}
39+
for attribute in attributes:
40+
output[attribute.tag] = attribute.text
41+
42+
return output
43+
44+
self.attributes = __get_attributes__(self.xml)
45+
46+
def get_fields(self, *, tag=None, field_type='datafield'):
47+
48+
array = []
49+
50+
for field in self.fields:
51+
52+
# If no tag is specified, grab all fields with the same type
53+
if not tag:
54+
if field.tag == field_type:
55+
marc_field = Field(field)
56+
array.append(marc_field)
57+
else:
58+
if field.attrib.get('tag') == tag and field.tag == field_type:
59+
marc_field = Field(field)
60+
array.append(marc_field)
61+
62+
return array
63+
64+
def add_field(self, *, tag, field_type='datafield', ind1='', ind2='', subfields=None):
65+
record_root = self.xml.find('.//record')
66+
new_field = ElementTree.Element(field_type, tag=tag, ind1=ind1, ind2=ind2)
67+
68+
if subfields is None:
69+
subfields = {}
70+
for code, value in subfields.items():
71+
new_subfield = ElementTree.Element('subfield', code=code)
72+
new_subfield.text = value
73+
new_field.append(new_subfield)
74+
75+
self.fields.append(new_field)
76+
record_root.append(new_field)
77+
return self.get_fields(field_type=field_type, tag=tag)
78+
79+
def remove_fields(self, *, tag=False, field_type='datafield'):
80+
record_root = self.xml.find('.//record')
81+
removal_list = []
82+
for field in self.fields:
83+
if not tag:
84+
if field.tag == field_type:
85+
removal_list.append(field)
86+
record_root.remove(field)
87+
else:
88+
if field.attrib.get('tag') == tag and field.tag == field_type:
89+
removal_list.append(field)
90+
record_root.remove(field)
91+
92+
self.fields = [e for e in self.fields if e not in removal_list]
93+
return self.fields
94+
95+
def to_string(self):
96+
# Write to fake file so that we can write our own XML declaration string.
97+
fake_file = bytes()
98+
self.xml.write(fake_file, encoding='utf-8', xml_declaration=False)
99+
return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' + fake_file.getvalue().decode('utf-8')
100+
101+
102+
class Field:
103+
104+
def __init__(self, reference):
105+
self.__reference__ = reference
106+
self.field_type = reference.tag
107+
self.attributes = reference.attrib
108+
self.value = reference.text if not list(reference) else None
109+
self.subfields = []
110+
for subfield in list(reference):
111+
self.subfields.append(Subfield(subfield))
112+
113+
def get_subfield(self, code):
114+
for subfield in self.subfields:
115+
if subfield.code == code:
116+
return subfield
117+
return None
118+
119+
def add_subfield(self, *, code, value):
120+
new_subfield = ElementTree.Element('subfield', code=code)
121+
new_subfield.text = value
122+
self.subfields.append(Subfield(new_subfield))
123+
self.__reference__.append(new_subfield)
124+
return self.get_subfield(code=code)
125+
126+
def get_subfields(self):
127+
return self.subfields
128+
129+
def set_subfield_value(self, *, code, value):
130+
for subfield in self.subfields:
131+
if subfield.code == code:
132+
subfield.set_value(value)
133+
return subfield
134+
return None
135+
136+
137+
class Subfield:
138+
def __init__(self, reference):
139+
self.__reference__ = reference
140+
self.code = reference.attrib['code']
141+
self.value = reference.text
142+
143+
def set_code(self, code):
144+
self.code = code
145+
self.__reference__.attrib['code'] = code
146+
147+
def set_value(self, value):
148+
self.value = value
149+
self.__reference__.text = value

0 commit comments

Comments
 (0)