almapy 9.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- almapy/__init__.py +6 -0
- almapy/_acq.py +134 -0
- almapy/_analytics.py +85 -0
- almapy/_base.py +223 -0
- almapy/_bibs.py +1156 -0
- almapy/_client.py +295 -0
- almapy/_config.py +661 -0
- almapy/_endpoints.py +80 -0
- almapy/_logging.py +74 -0
- almapy/_primo.py +126 -0
- almapy/_throttle.py +152 -0
- almapy/_users.py +733 -0
- almapy/_utils.py +324 -0
- almapy/exceptions.py +282 -0
- almapy/py.typed +0 -0
- almapy-9.1.1.dist-info/METADATA +285 -0
- almapy-9.1.1.dist-info/RECORD +19 -0
- almapy-9.1.1.dist-info/WHEEL +4 -0
- almapy-9.1.1.dist-info/licenses/LICENSE +21 -0
almapy/__init__.py
ADDED
almapy/_acq.py
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
from datetime import date
|
|
2
|
+
from typing import Any, Literal, overload
|
|
3
|
+
|
|
4
|
+
from almapy._base import BaseNamespace
|
|
5
|
+
from almapy._endpoints import AlmaEndpoint
|
|
6
|
+
from almapy._utils import RESP_TYPE, Body, _ModelT
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class AlmaClientAcqNS(BaseNamespace):
|
|
10
|
+
"""Namespace for acquisitions functionality."""
|
|
11
|
+
|
|
12
|
+
@overload
|
|
13
|
+
async def get_po_line(self, po_line_id: str, *, model: type[_ModelT]) -> _ModelT: ...
|
|
14
|
+
|
|
15
|
+
@overload
|
|
16
|
+
async def get_po_line(self, po_line_id: str, *, model: None = ...) -> RESP_TYPE: ...
|
|
17
|
+
|
|
18
|
+
async def get_po_line(self, po_line_id: str, *, model: Any = None) -> Any:
|
|
19
|
+
return await self._get(AlmaEndpoint.PO_LINE, {"PO_LINE_ID": po_line_id}, model=model)
|
|
20
|
+
|
|
21
|
+
@overload
|
|
22
|
+
async def update_po_line(
|
|
23
|
+
self,
|
|
24
|
+
po_line_id: str,
|
|
25
|
+
updated_po_line: Body,
|
|
26
|
+
*,
|
|
27
|
+
update_inventory: bool = ...,
|
|
28
|
+
redistribute_funds: bool = ...,
|
|
29
|
+
model: type[_ModelT],
|
|
30
|
+
) -> _ModelT: ...
|
|
31
|
+
|
|
32
|
+
@overload
|
|
33
|
+
async def update_po_line(
|
|
34
|
+
self,
|
|
35
|
+
po_line_id: str,
|
|
36
|
+
updated_po_line: Body,
|
|
37
|
+
*,
|
|
38
|
+
update_inventory: bool = ...,
|
|
39
|
+
redistribute_funds: bool = ...,
|
|
40
|
+
model: None = ...,
|
|
41
|
+
) -> RESP_TYPE: ...
|
|
42
|
+
|
|
43
|
+
async def update_po_line(
|
|
44
|
+
self,
|
|
45
|
+
po_line_id: str,
|
|
46
|
+
updated_po_line: Body,
|
|
47
|
+
*,
|
|
48
|
+
update_inventory: bool = False,
|
|
49
|
+
redistribute_funds: bool = False,
|
|
50
|
+
model: Any = None,
|
|
51
|
+
) -> Any:
|
|
52
|
+
return await self._put(
|
|
53
|
+
AlmaEndpoint.PO_LINE,
|
|
54
|
+
{"PO_LINE_ID": po_line_id},
|
|
55
|
+
model=model,
|
|
56
|
+
params={"update_inventory": update_inventory, "redistribute_funds": redistribute_funds},
|
|
57
|
+
json=updated_po_line,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
@overload
|
|
61
|
+
async def receive_existing_item(
|
|
62
|
+
self,
|
|
63
|
+
po_line_id: str,
|
|
64
|
+
item_pid: str,
|
|
65
|
+
*,
|
|
66
|
+
receive_date: date | None = ...,
|
|
67
|
+
department: str | None = ...,
|
|
68
|
+
department_library: str | None = ...,
|
|
69
|
+
updated_item: Body | None = ...,
|
|
70
|
+
model: type[_ModelT],
|
|
71
|
+
) -> _ModelT: ...
|
|
72
|
+
|
|
73
|
+
@overload
|
|
74
|
+
async def receive_existing_item(
|
|
75
|
+
self,
|
|
76
|
+
po_line_id: str,
|
|
77
|
+
item_pid: str,
|
|
78
|
+
*,
|
|
79
|
+
receive_date: date | None = ...,
|
|
80
|
+
department: str | None = ...,
|
|
81
|
+
department_library: str | None = ...,
|
|
82
|
+
updated_item: Body | None = ...,
|
|
83
|
+
model: None = ...,
|
|
84
|
+
) -> RESP_TYPE: ...
|
|
85
|
+
|
|
86
|
+
async def receive_existing_item(
|
|
87
|
+
self,
|
|
88
|
+
po_line_id: str,
|
|
89
|
+
item_pid: str,
|
|
90
|
+
*,
|
|
91
|
+
receive_date: date | None = None,
|
|
92
|
+
department: str | None = None,
|
|
93
|
+
department_library: str | None = None,
|
|
94
|
+
updated_item: Body | None = None,
|
|
95
|
+
model: Any = None,
|
|
96
|
+
) -> Any:
|
|
97
|
+
if updated_item is None:
|
|
98
|
+
updated_item = {}
|
|
99
|
+
params: dict[str, Any] = {"op": "receive"}
|
|
100
|
+
if receive_date:
|
|
101
|
+
params["receive_date"] = receive_date.strftime("%Y-%m-%dZ")
|
|
102
|
+
if department:
|
|
103
|
+
params["department"] = department
|
|
104
|
+
if department_library:
|
|
105
|
+
params["department_library"] = department_library
|
|
106
|
+
return await self._post(
|
|
107
|
+
AlmaEndpoint.PO_LINE_ITEM,
|
|
108
|
+
{"PO_LINE_ID": po_line_id, "ITEM_PID": item_pid},
|
|
109
|
+
model=model,
|
|
110
|
+
params=params,
|
|
111
|
+
json=updated_item,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
async def cancel_po_line(
|
|
115
|
+
self,
|
|
116
|
+
po_line_id: str,
|
|
117
|
+
reason_code: str,
|
|
118
|
+
*,
|
|
119
|
+
comment: str | None = None,
|
|
120
|
+
inform_vendor: bool = False,
|
|
121
|
+
override: bool = False,
|
|
122
|
+
bib_handling: Literal["retain", "delete", "suppress"] = "retain",
|
|
123
|
+
) -> None:
|
|
124
|
+
params: dict[str, Any] = {
|
|
125
|
+
"reason": reason_code,
|
|
126
|
+
"inform_vendor": inform_vendor,
|
|
127
|
+
"override": override,
|
|
128
|
+
"bib": bib_handling,
|
|
129
|
+
}
|
|
130
|
+
if comment:
|
|
131
|
+
params["comment"] = comment
|
|
132
|
+
await self._delete(
|
|
133
|
+
AlmaEndpoint.PO_LINE, {"PO_LINE_ID": po_line_id}, parser="none", params=params
|
|
134
|
+
)
|
almapy/_analytics.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
import xmltodict
|
|
4
|
+
|
|
5
|
+
from almapy._base import BaseNamespace
|
|
6
|
+
from almapy._endpoints import AlmaEndpoint
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _as_list(value: Any) -> list[Any]:
|
|
10
|
+
"""Normalise xmltodict's repeated-element handling.
|
|
11
|
+
|
|
12
|
+
xmltodict returns a list for repeated elements, a bare dict when exactly one
|
|
13
|
+
is present, and nothing at all when there are none. Every site that iterates
|
|
14
|
+
rows or columns has to go through here: iterating the dict form yields its
|
|
15
|
+
keys as strings, which fails later with a confusing AttributeError rather
|
|
16
|
+
than at the point of the mistake.
|
|
17
|
+
"""
|
|
18
|
+
if value is None:
|
|
19
|
+
return []
|
|
20
|
+
return value if isinstance(value, list) else [value]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def headers_to_dict(headers: list[dict[str, Any]] | dict[str, Any]) -> dict[str, Any]:
|
|
24
|
+
"""Map Alma's internal column names to their report headings.
|
|
25
|
+
|
|
26
|
+
Accepts the single-column dict form as well as a list — a report with one
|
|
27
|
+
non-Column0 column previously raised TypeError here.
|
|
28
|
+
"""
|
|
29
|
+
return {header["@name"]: header["@saw-sql:columnHeading"] for header in _as_list(headers)}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class AlmaClientAnalyticsNS(BaseNamespace):
|
|
33
|
+
"""Namespace for analytics functionality."""
|
|
34
|
+
|
|
35
|
+
async def get_raw_report(
|
|
36
|
+
self,
|
|
37
|
+
path: str,
|
|
38
|
+
limit: int = 100,
|
|
39
|
+
*,
|
|
40
|
+
token: str | None = None,
|
|
41
|
+
report_filter: str | None = None,
|
|
42
|
+
) -> str:
|
|
43
|
+
params: dict[str, Any] = {"path": path, "limit": limit}
|
|
44
|
+
if report_filter:
|
|
45
|
+
params["filter"] = report_filter
|
|
46
|
+
if token:
|
|
47
|
+
params["token"] = token
|
|
48
|
+
return await self._get_text(
|
|
49
|
+
AlmaEndpoint.REPORTS,
|
|
50
|
+
params=params,
|
|
51
|
+
headers={"Accept": "application/xml"},
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
async def get_full_report(
|
|
55
|
+
self,
|
|
56
|
+
path: str,
|
|
57
|
+
limit: int = 100,
|
|
58
|
+
header_override: dict[str, str] | None = None,
|
|
59
|
+
report_filter: str | None = None,
|
|
60
|
+
) -> list[dict[str, str]]:
|
|
61
|
+
initial_response = await self.get_raw_report(path, limit, report_filter=report_filter)
|
|
62
|
+
parsed_resp = xmltodict.parse(initial_response)
|
|
63
|
+
finished = parsed_resp["report"]["QueryResult"]["IsFinished"]
|
|
64
|
+
rowset = parsed_resp["report"]["QueryResult"]["ResultXml"]["rowset"]
|
|
65
|
+
result = _as_list(rowset.get("Row"))
|
|
66
|
+
headers = headers_to_dict(
|
|
67
|
+
rowset["xsd:schema"]["xsd:complexType"]["xsd:sequence"]["xsd:element"]
|
|
68
|
+
)
|
|
69
|
+
if header_override:
|
|
70
|
+
headers.update(header_override)
|
|
71
|
+
token = parsed_resp["report"]["QueryResult"]["ResumptionToken"]
|
|
72
|
+
|
|
73
|
+
while finished == "false":
|
|
74
|
+
resp = await self.get_raw_report(path, limit, token=token, report_filter=report_filter)
|
|
75
|
+
parsed_resp = xmltodict.parse(resp)
|
|
76
|
+
finished = parsed_resp["report"]["QueryResult"]["IsFinished"]
|
|
77
|
+
# Same coercion as the first page. Indexing ["Row"] directly meant a
|
|
78
|
+
# final page with one row raised AttributeError and one with no rows
|
|
79
|
+
# raised KeyError — reachable by any report whose total is just over
|
|
80
|
+
# a multiple of `limit`.
|
|
81
|
+
page = parsed_resp["report"]["QueryResult"]["ResultXml"]["rowset"]
|
|
82
|
+
result.extend(_as_list(page.get("Row")))
|
|
83
|
+
|
|
84
|
+
data = [{headers[k]: v for k, v in row.items() if k != "Column0"} for row in result]
|
|
85
|
+
return data
|
almapy/_base.py
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
"""Base namespace for Alma API client namespaces."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable, Mapping
|
|
4
|
+
from typing import TYPE_CHECKING, Any, Literal, Protocol, cast, overload
|
|
5
|
+
|
|
6
|
+
from almapy._endpoints import AlmaEndpoint
|
|
7
|
+
from almapy._utils import RESP_TYPE, _ModelT
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
import niquests
|
|
11
|
+
|
|
12
|
+
Parser = Literal["json", "xml", "none", "text"]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class _AlmaExecutable(Protocol):
|
|
16
|
+
"""Protocol for the execute method AlmaClient provides."""
|
|
17
|
+
|
|
18
|
+
_gateway: str
|
|
19
|
+
"""Regional gateway root (e.g. https://api-eu.hosted.exlibrisgroup.com)."""
|
|
20
|
+
|
|
21
|
+
@overload
|
|
22
|
+
async def execute(
|
|
23
|
+
self, method: str, url: str, *, parser: Parser, model: type[_ModelT], **kwargs: Any
|
|
24
|
+
) -> _ModelT: ...
|
|
25
|
+
|
|
26
|
+
@overload
|
|
27
|
+
async def execute(
|
|
28
|
+
self, method: str, url: str, *, parser: Parser, model: None = ..., **kwargs: Any
|
|
29
|
+
) -> RESP_TYPE: ...
|
|
30
|
+
|
|
31
|
+
async def execute(
|
|
32
|
+
self,
|
|
33
|
+
method: str,
|
|
34
|
+
url: str,
|
|
35
|
+
*,
|
|
36
|
+
parser: Parser,
|
|
37
|
+
model: Any = None,
|
|
38
|
+
validate: "Callable[[niquests.Response], None]" = ...,
|
|
39
|
+
retry: bool | None = None,
|
|
40
|
+
**kwargs: Any,
|
|
41
|
+
) -> Any: ...
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class BaseNamespace: # ruff: ignore[class-as-data-structure]
|
|
45
|
+
"""Base class for AlmaClient namespace objects.
|
|
46
|
+
|
|
47
|
+
Owns URL building via AlmaEndpoint.build() and delegates HTTP execution to
|
|
48
|
+
AlmaClient.execute(). Namespace methods pass ``retry=`` straight through to
|
|
49
|
+
control whether a write is replayed on an ambiguous transport failure.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
def __init__(self, client: _AlmaExecutable) -> None:
|
|
53
|
+
self._client = client
|
|
54
|
+
|
|
55
|
+
@overload
|
|
56
|
+
async def _get(
|
|
57
|
+
self,
|
|
58
|
+
endpoint: AlmaEndpoint,
|
|
59
|
+
path: Mapping[str, str] | None = None,
|
|
60
|
+
*,
|
|
61
|
+
model: type[_ModelT],
|
|
62
|
+
parser: Parser = ...,
|
|
63
|
+
**kwargs: Any,
|
|
64
|
+
) -> _ModelT: ...
|
|
65
|
+
|
|
66
|
+
@overload
|
|
67
|
+
async def _get(
|
|
68
|
+
self,
|
|
69
|
+
endpoint: AlmaEndpoint,
|
|
70
|
+
path: Mapping[str, str] | None = None,
|
|
71
|
+
*,
|
|
72
|
+
model: None = ...,
|
|
73
|
+
parser: Parser = ...,
|
|
74
|
+
**kwargs: Any,
|
|
75
|
+
) -> RESP_TYPE: ...
|
|
76
|
+
|
|
77
|
+
async def _get(
|
|
78
|
+
self,
|
|
79
|
+
endpoint: AlmaEndpoint,
|
|
80
|
+
path: Mapping[str, str] | None = None,
|
|
81
|
+
*,
|
|
82
|
+
model: Any = None,
|
|
83
|
+
parser: Parser = "json",
|
|
84
|
+
**kwargs: Any,
|
|
85
|
+
) -> Any:
|
|
86
|
+
return await self._client.execute(
|
|
87
|
+
"GET", endpoint.build(path), parser=parser, model=model, **kwargs
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
@overload
|
|
91
|
+
async def _post(
|
|
92
|
+
self,
|
|
93
|
+
endpoint: AlmaEndpoint,
|
|
94
|
+
path: Mapping[str, str] | None = None,
|
|
95
|
+
*,
|
|
96
|
+
model: type[_ModelT],
|
|
97
|
+
parser: Parser = ...,
|
|
98
|
+
**kwargs: Any,
|
|
99
|
+
) -> _ModelT: ...
|
|
100
|
+
|
|
101
|
+
@overload
|
|
102
|
+
async def _post(
|
|
103
|
+
self,
|
|
104
|
+
endpoint: AlmaEndpoint,
|
|
105
|
+
path: Mapping[str, str] | None = None,
|
|
106
|
+
*,
|
|
107
|
+
model: None = ...,
|
|
108
|
+
parser: Parser = ...,
|
|
109
|
+
**kwargs: Any,
|
|
110
|
+
) -> RESP_TYPE: ...
|
|
111
|
+
|
|
112
|
+
async def _post(
|
|
113
|
+
self,
|
|
114
|
+
endpoint: AlmaEndpoint,
|
|
115
|
+
path: Mapping[str, str] | None = None,
|
|
116
|
+
*,
|
|
117
|
+
model: Any = None,
|
|
118
|
+
parser: Parser = "json",
|
|
119
|
+
**kwargs: Any,
|
|
120
|
+
) -> Any:
|
|
121
|
+
return await self._client.execute(
|
|
122
|
+
"POST", endpoint.build(path), parser=parser, model=model, **kwargs
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
@overload
|
|
126
|
+
async def _put(
|
|
127
|
+
self,
|
|
128
|
+
endpoint: AlmaEndpoint,
|
|
129
|
+
path: Mapping[str, str] | None = None,
|
|
130
|
+
*,
|
|
131
|
+
model: type[_ModelT],
|
|
132
|
+
parser: Parser = ...,
|
|
133
|
+
**kwargs: Any,
|
|
134
|
+
) -> _ModelT: ...
|
|
135
|
+
|
|
136
|
+
@overload
|
|
137
|
+
async def _put(
|
|
138
|
+
self,
|
|
139
|
+
endpoint: AlmaEndpoint,
|
|
140
|
+
path: Mapping[str, str] | None = None,
|
|
141
|
+
*,
|
|
142
|
+
model: None = ...,
|
|
143
|
+
parser: Parser = ...,
|
|
144
|
+
**kwargs: Any,
|
|
145
|
+
) -> RESP_TYPE: ...
|
|
146
|
+
|
|
147
|
+
async def _put(
|
|
148
|
+
self,
|
|
149
|
+
endpoint: AlmaEndpoint,
|
|
150
|
+
path: Mapping[str, str] | None = None,
|
|
151
|
+
*,
|
|
152
|
+
model: Any = None,
|
|
153
|
+
parser: Parser = "json",
|
|
154
|
+
**kwargs: Any,
|
|
155
|
+
) -> Any:
|
|
156
|
+
return await self._client.execute(
|
|
157
|
+
"PUT", endpoint.build(path), parser=parser, model=model, **kwargs
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
@overload
|
|
161
|
+
async def _delete(
|
|
162
|
+
self,
|
|
163
|
+
endpoint: AlmaEndpoint,
|
|
164
|
+
path: Mapping[str, str] | None = None,
|
|
165
|
+
*,
|
|
166
|
+
model: type[_ModelT],
|
|
167
|
+
parser: Parser = ...,
|
|
168
|
+
**kwargs: Any,
|
|
169
|
+
) -> _ModelT: ...
|
|
170
|
+
|
|
171
|
+
@overload
|
|
172
|
+
async def _delete(
|
|
173
|
+
self,
|
|
174
|
+
endpoint: AlmaEndpoint,
|
|
175
|
+
path: Mapping[str, str] | None = None,
|
|
176
|
+
*,
|
|
177
|
+
model: None = ...,
|
|
178
|
+
parser: Parser = ...,
|
|
179
|
+
**kwargs: Any,
|
|
180
|
+
) -> RESP_TYPE: ...
|
|
181
|
+
|
|
182
|
+
async def _delete(
|
|
183
|
+
self,
|
|
184
|
+
endpoint: AlmaEndpoint,
|
|
185
|
+
path: Mapping[str, str] | None = None,
|
|
186
|
+
*,
|
|
187
|
+
model: Any = None,
|
|
188
|
+
parser: Parser = "json",
|
|
189
|
+
**kwargs: Any,
|
|
190
|
+
) -> Any:
|
|
191
|
+
return await self._client.execute(
|
|
192
|
+
"DELETE", endpoint.build(path), parser=parser, model=model, **kwargs
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
async def _get_text(
|
|
196
|
+
self,
|
|
197
|
+
endpoint: AlmaEndpoint,
|
|
198
|
+
path: Mapping[str, str] | None = None,
|
|
199
|
+
**kwargs: Any,
|
|
200
|
+
) -> str:
|
|
201
|
+
"""GET returning raw response text (e.g., MARC XML records)."""
|
|
202
|
+
result: Any = await self._get(endpoint, path, parser="text", **kwargs)
|
|
203
|
+
return cast(str, result)
|
|
204
|
+
|
|
205
|
+
async def _post_text(
|
|
206
|
+
self,
|
|
207
|
+
endpoint: AlmaEndpoint,
|
|
208
|
+
path: Mapping[str, str] | None = None,
|
|
209
|
+
**kwargs: Any,
|
|
210
|
+
) -> str:
|
|
211
|
+
"""POST returning raw response text."""
|
|
212
|
+
result: Any = await self._post(endpoint, path, parser="text", **kwargs)
|
|
213
|
+
return cast(str, result)
|
|
214
|
+
|
|
215
|
+
async def _put_text(
|
|
216
|
+
self,
|
|
217
|
+
endpoint: AlmaEndpoint,
|
|
218
|
+
path: Mapping[str, str] | None = None,
|
|
219
|
+
**kwargs: Any,
|
|
220
|
+
) -> str:
|
|
221
|
+
"""PUT returning raw response text."""
|
|
222
|
+
result: Any = await self._put(endpoint, path, parser="text", **kwargs)
|
|
223
|
+
return cast(str, result)
|