|
1 | 1 | import logging |
| 2 | +import re |
2 | 3 | import time |
3 | 4 |
|
4 | 5 | from attrdict import AttrMap |
@@ -66,14 +67,13 @@ def search(self, query): |
66 | 67 | search_url = "%s/search" % self.api_base |
67 | 68 | params = { |
68 | 69 | "query": query, |
69 | | - "httpAccept": "application/json", |
70 | | - "maximumRecords": 50, # TODO: configurable ? |
| 70 | + "maximumRecords": 10, # TODO: configurable ? |
71 | 71 | # sort by number of holdings (default sort on web search) |
72 | 72 | # - so better known names show up first |
73 | | - "sortKeys": "holdingscount", |
| 73 | + "sortKey": "holdingscount", |
74 | 74 | } |
75 | 75 |
|
76 | | - response = requests.get(search_url, params=params) |
| 76 | + response = requests.get(search_url, params=params, headers={"Accept": "application/json"}) |
77 | 77 | logger.debug( |
78 | 78 | "search '%s': %s %s, %0.2f", |
79 | 79 | params["query"], |
@@ -130,7 +130,10 @@ def rdf(self): |
130 | 130 | """VIAF data for this entity as :class:`rdflib.Graph`""" |
131 | 131 | start = time.time() |
132 | 132 | graph = rdflib.Graph() |
133 | | - graph.parse(self.uri) |
| 133 | + # 2025 update: Accept header now required, so use requests.get to retrieve RDF |
| 134 | + response = requests.get(self.uri, headers={"Accept": "application/rdf+xml"}) |
| 135 | + response.raise_for_status() # raise HTTPError on non-success status |
| 136 | + graph.parse(data=response.text, format="xml") |
134 | 137 | logger.debug("Loaded VIAF RDF %s: %0.2f sec", self.uri, time.time() - start) |
135 | 138 | return graph |
136 | 139 |
|
@@ -183,37 +186,59 @@ def __init__(self, data): |
183 | 186 | @cached_property |
184 | 187 | def total_results(self): |
185 | 188 | """number of records matching the query""" |
186 | | - return int(self._data.get("numberOfRecords", 0)) |
| 189 | + return int(self._data.get("numberOfRecords", {}).get("content", 0)) |
187 | 190 |
|
188 | 191 | @cached_property |
189 | 192 | def records(self): |
190 | | - """list of results as :class:`SRUItem`.""" |
191 | | - return [SRUItem(d["record"]) for d in self._data.get("records", [])] |
| 193 | + """List of results as :class:`SRUItem`.""" |
| 194 | + record_or_records = self._data.get("records", {}).get("record") |
| 195 | + if isinstance(record_or_records, dict): |
| 196 | + return [SRUItem(self.normalize_record(record_or_records))] |
| 197 | + elif isinstance(record_or_records, list): |
| 198 | + return [SRUItem(self.normalize_record(d)) for d in record_or_records] |
| 199 | + return [] |
192 | 200 |
|
| 201 | + def normalize_record(self, data): |
| 202 | + """Added in May 2025 to match updates to the /search API records, where |
| 203 | + the JSON response now uses namespaced keys that increase per result: |
| 204 | + ns2, ns3, ns4, and so on, applying to most subkeys (ns2:VIAFCluster, |
| 205 | + ns2:Document, etc). This method strips all nsX: prefixes recursively""" |
| 206 | + if isinstance(data, dict): |
| 207 | + return { |
| 208 | + re.sub(r"^ns\d+:", "", key): self.normalize_record(value) |
| 209 | + for key, value in data.items() |
| 210 | + } |
| 211 | + elif isinstance(data, list): |
| 212 | + return [self.normalize_record(item) for item in data] |
| 213 | + else: |
| 214 | + return data |
193 | 215 |
|
194 | 216 | class SRUItem(AttrMap): |
195 | 217 | """Single item returned by a SRU search, for use with |
196 | | - :meth:`ViafAPI.search` and :class:`SRUResult`.""" |
| 218 | + :meth:`ViafAPI.search` and :class:`SRUResult`. |
| 219 | + |
| 220 | + The `VIAFCluster` attribute was added to each property lookup in 2025 to |
| 221 | + match updates to the /search API's JSON response.""" |
197 | 222 |
|
198 | 223 | @property |
199 | 224 | def uri(self): |
200 | 225 | """VIAF URI for this result""" |
201 | | - return self.recordData.Document["@about"] |
| 226 | + return self.recordData.VIAFCluster.Document["about"] |
202 | 227 |
|
203 | 228 | @property |
204 | 229 | def viaf_id(self): |
205 | 230 | """VIAF numeric identifier""" |
206 | | - return self.recordData.viafID |
| 231 | + return self.recordData.VIAFCluster.viafID |
207 | 232 |
|
208 | 233 | @property |
209 | 234 | def nametype(self): |
210 | 235 | """type of name (personal, corporate, title, etc)""" |
211 | | - return self.recordData.nameType |
| 236 | + return self.recordData.VIAFCluster.nameType |
212 | 237 |
|
213 | 238 | @property |
214 | 239 | def label(self): |
215 | 240 | """first main heading for this item""" |
216 | 241 | try: |
217 | | - return self.recordData.mainHeadings.data[0].text |
218 | | - except KeyError: |
219 | | - return self.recordData.mainHeadings.data.text |
| 242 | + return self.recordData.VIAFCluster.mainHeadings.data[0].text |
| 243 | + except (KeyError, IndexError): |
| 244 | + return self.recordData.VIAFCluster.mainHeadings.data.text |
0 commit comments