solrctl 0.5.0__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.
solrctl/__init__.py ADDED
File without changes
solrctl/cli.py ADDED
@@ -0,0 +1,231 @@
1
+ """solrctl CLI – Verwaltung von Solr-Cores und Aliases."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import click
6
+ from loguru import logger
7
+
8
+ from solrctl.client import SolrClient, SolrClientError
9
+
10
+
11
+ class SolrContext:
12
+ """Hält den SolrClient für die Laufzeit des CLI-Aufrufs."""
13
+
14
+ def __init__(self, solr_url: str, auth: tuple[str, str] | None) -> None:
15
+ self.client = SolrClient(solr_url, auth=auth)
16
+
17
+ def close(self) -> None:
18
+ self.client.close()
19
+
20
+
21
+ pass_solr_ctx = click.make_pass_decorator(SolrContext)
22
+
23
+
24
+ def _handle_error(exc: SolrClientError) -> None:
25
+ click.secho(f"Fehler: {exc}", fg="red", err=True)
26
+ raise SystemExit(1)
27
+
28
+
29
+ @click.group()
30
+ @click.option(
31
+ "--solr-url",
32
+ envvar="SOLR_URL",
33
+ default="http://localhost:8983",
34
+ show_default=True,
35
+ help="Basis-URL der Solr-Instanz.",
36
+ )
37
+ @click.option(
38
+ "--user", envvar="SOLR_USER", default=None, help="Basic-Auth Benutzername."
39
+ )
40
+ @click.option(
41
+ "--password", envvar="SOLR_PASSWORD", default=None, help="Basic-Auth Passwort."
42
+ )
43
+ @click.pass_context
44
+ def cli(
45
+ ctx: click.Context, solr_url: str, user: str | None, password: str | None
46
+ ) -> None:
47
+ """solrctl – Verwaltung von Solr-Cores und Aliases."""
48
+ auth = (user, password) if user and password else None
49
+ ctx.obj = SolrContext(solr_url, auth)
50
+
51
+
52
+ @cli.command()
53
+ @pass_solr_ctx
54
+ def ping(ctx: SolrContext) -> None:
55
+ """Prüft, ob Solr erreichbar ist."""
56
+ if ctx.client.ping():
57
+ click.echo("Solr erreichbar.")
58
+ else:
59
+ click.secho("Solr NICHT erreichbar.", fg="red", err=True)
60
+ raise SystemExit(1)
61
+
62
+
63
+ # ── core ──────────────────────────────────────────────────────────
64
+
65
+
66
+ @cli.group()
67
+ def core() -> None:
68
+ """Standalone Core-Operationen."""
69
+
70
+
71
+ @core.command("list")
72
+ @pass_solr_ctx
73
+ def core_list(ctx: SolrContext) -> None:
74
+ """Listet alle vorhandenen Cores auf."""
75
+ try:
76
+ cores = ctx.client.list_cores()
77
+ except SolrClientError as exc:
78
+ _handle_error(exc)
79
+ return
80
+
81
+ if not cores:
82
+ click.echo("Keine Cores gefunden.")
83
+ return
84
+ for name in cores:
85
+ click.echo(name)
86
+
87
+
88
+ @core.command()
89
+ @click.option("--source", required=True, help="Name des neuen Cores (frische Daten).")
90
+ @click.option(
91
+ "--target", required=True, help="Name des aktiven Cores (wird getauscht)."
92
+ )
93
+ @pass_solr_ctx
94
+ def swap(ctx: SolrContext, source: str, target: str) -> None:
95
+ """Tauscht zwei Solr-Cores (CoreAdmin SWAP-Action)."""
96
+ logger.info("Swap: '{}' <-> '{}'", target, source)
97
+ try:
98
+ ctx.client.swap_cores(target, source)
99
+ click.secho(f"Swap erfolgreich: '{target}' <-> '{source}'.", fg="green")
100
+ except SolrClientError as exc:
101
+ _handle_error(exc)
102
+
103
+
104
+ @core.command()
105
+ @click.argument("name")
106
+ @click.option("--delete-index", is_flag=True, help="Index-Daten auf Disk löschen.")
107
+ @pass_solr_ctx
108
+ def delete(ctx: SolrContext, name: str, delete_index: bool) -> None:
109
+ """Löscht (unload) einen Solr-Core."""
110
+ try:
111
+ ctx.client.unload_core(name, delete_index=delete_index)
112
+ click.secho(f"Core '{name}' gelöscht.", fg="green")
113
+ except SolrClientError as exc:
114
+ _handle_error(exc)
115
+
116
+
117
+ @core.command("info")
118
+ @click.argument("name")
119
+ @pass_solr_ctx
120
+ def core_info(ctx: SolrContext, name: str) -> None:
121
+ """Zeigt detaillierte Informationen zu einem Core an."""
122
+ try:
123
+ info = ctx.client.get_core_info(name)
124
+ except SolrClientError as exc:
125
+ _handle_error(exc)
126
+ return
127
+
128
+ click.echo(f"Core: {info.get('name', name)}")
129
+ click.echo(f"InstanceDir: {info.get('instanceDir')}")
130
+ click.echo(f"DataDir: {info.get('dataDir')}")
131
+ click.echo(f"Config: {info.get('config')}")
132
+ click.echo(f"Schema: {info.get('schema')}")
133
+ click.echo(f"StartTime: {info.get('startTime')}")
134
+ uptime = info.get("uptime", 0)
135
+ click.echo(f"Uptime: {uptime // 1000}s")
136
+
137
+ index = info.get("index", {})
138
+ if index:
139
+ click.echo()
140
+ click.secho("Index:", bold=True)
141
+ click.echo(f" numDocs: {index.get('numDocs')}")
142
+ click.echo(f" maxDoc: {index.get('maxDoc')}")
143
+ click.echo(f" deletedDocs: {index.get('deletedDocs')}")
144
+ click.echo(f" segments: {index.get('segmentCount')}")
145
+ click.echo(f" size: {index.get('size')}")
146
+ click.echo(f" lastModified: {index.get('lastModified')}")
147
+
148
+
149
+ @core.command("config")
150
+ @click.argument("name")
151
+ @pass_solr_ctx
152
+ def core_config(ctx: SolrContext, name: str) -> None:
153
+ """Gibt die solrconfig.xml eines Cores als XML aus."""
154
+ try:
155
+ config_xml = ctx.client.get_core_config(name)
156
+ except SolrClientError as exc:
157
+ _handle_error(exc)
158
+ return
159
+ click.echo(config_xml)
160
+
161
+
162
+ @core.command("schema")
163
+ @click.argument("name")
164
+ @pass_solr_ctx
165
+ def core_schema(ctx: SolrContext, name: str) -> None:
166
+ """Gibt das Schema eines Cores als XML aus."""
167
+ try:
168
+ schema_xml = ctx.client.get_core_schema(name)
169
+ except SolrClientError as exc:
170
+ _handle_error(exc)
171
+ return
172
+ click.echo(schema_xml)
173
+
174
+
175
+ # ── alias ─────────────────────────────────────────────────────────
176
+
177
+
178
+ @cli.group()
179
+ def alias() -> None:
180
+ """SolrCloud Alias-Operationen."""
181
+
182
+
183
+ @alias.command("list")
184
+ @pass_solr_ctx
185
+ def alias_list(ctx: SolrContext) -> None:
186
+ """Listet alle Aliases auf."""
187
+ try:
188
+ aliases = ctx.client.list_aliases()
189
+ except SolrClientError as exc:
190
+ _handle_error(exc)
191
+ return
192
+
193
+ if not aliases:
194
+ click.echo("Keine Aliases gefunden.")
195
+ return
196
+ for alias_name, collection in aliases.items():
197
+ click.echo(f"{alias_name} -> {collection}")
198
+
199
+
200
+ @alias.command()
201
+ @click.option("--alias", required=True, help="Name des Alias.")
202
+ @click.option("--collection", required=True, help="Ziel-Collection.")
203
+ @pass_solr_ctx
204
+ def swap(ctx: SolrContext, alias: str, collection: str) -> None:
205
+ """Setzt einen Alias auf eine neue Collection um."""
206
+ try:
207
+ ctx.client.create_or_update_alias(alias, collection)
208
+ click.secho(f"Alias '{alias}' -> '{collection}' gesetzt.", fg="green")
209
+ except SolrClientError as exc:
210
+ _handle_error(exc)
211
+
212
+
213
+ @alias.command()
214
+ @click.argument("name")
215
+ @pass_solr_ctx
216
+ def delete(ctx: SolrContext, name: str) -> None:
217
+ """Löscht einen Alias."""
218
+ try:
219
+ ctx.client.delete_alias(name)
220
+ click.secho(f"Alias '{name}' gelöscht.", fg="green")
221
+ except SolrClientError as exc:
222
+ _handle_error(exc)
223
+
224
+
225
+ def main() -> None:
226
+ """Einstiegspunkt für die CLI."""
227
+ cli()
228
+
229
+
230
+ if __name__ == "__main__":
231
+ main()
solrctl/client.py ADDED
@@ -0,0 +1,473 @@
1
+ """Solr HTTP-Client auf Basis von httpx.
2
+
3
+ Kapselt alle API-Aufrufe gegen Solr 9.x:
4
+ - Collections API (SolrCloud): Alias-Verwaltung
5
+ - CoreAdmin API (Standalone): Core-Verwaltung
6
+ - Health-Check
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import logging
12
+ from typing import Any
13
+
14
+ import httpx
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ class SolrClientError(Exception):
20
+ """Wird bei Fehlern der Solr-API geworfen."""
21
+
22
+
23
+ class SolrClient:
24
+ """HTTP-Client für Solr 9.x.
25
+
26
+ Args:
27
+ base_url: Basis-URL der Solr-Instanz (z. B. ``http://localhost:8983``).
28
+ timeout: HTTP-Timeout in Sekunden.
29
+ auth: Optionales ``(user, password)``-Tupel für Basic Auth.
30
+ """
31
+
32
+ def __init__(
33
+ self,
34
+ base_url: str,
35
+ timeout: float = 30.0,
36
+ auth: tuple[str, str] | None = None,
37
+ ) -> None:
38
+ self._base_url = base_url.rstrip("/")
39
+ self._client = httpx.Client(
40
+ base_url=self._base_url,
41
+ timeout=timeout,
42
+ auth=auth,
43
+ )
44
+
45
+ # ------------------------------------------------------------------
46
+ # Health
47
+ # ------------------------------------------------------------------
48
+
49
+ def ping(self) -> bool:
50
+ """Prüft, ob die Solr-Instanz erreichbar ist.
51
+
52
+ Returns:
53
+ ``True`` wenn Solr antwortet, sonst ``False``.
54
+ """
55
+ try:
56
+ response = self._client.get(
57
+ "/solr/admin/info/system", params={"wt": "json"}
58
+ )
59
+ response.raise_for_status()
60
+ logger.debug("Solr erreichbar: %s", self._base_url)
61
+ return True
62
+ except Exception as exc:
63
+ logger.warning("Solr nicht erreichbar: %s", exc)
64
+ return False
65
+
66
+ # ------------------------------------------------------------------
67
+ # Collections API – Aliases (SolrCloud)
68
+ # ------------------------------------------------------------------
69
+
70
+ def list_aliases(self) -> dict[str, str]:
71
+ """Gibt alle bekannten Aliases zurück.
72
+
73
+ Returns:
74
+ Dict ``{alias_name: collection_name}``.
75
+
76
+ Raises:
77
+ SolrClientError: Bei API-Fehler.
78
+ """
79
+ response = self._request(
80
+ "GET",
81
+ "/solr/admin/collections",
82
+ params={"action": "LISTALIASES", "wt": "json"},
83
+ )
84
+ # Antwort: {"aliases": {"alias1": "col1", ...}, ...}
85
+ return response.get("aliases", {})
86
+
87
+ def create_or_update_alias(self, alias: str, collection: str) -> None:
88
+ """Erstellt einen neuen Alias oder setzt einen bestehenden um.
89
+
90
+ Args:
91
+ alias: Name des Alias.
92
+ collection: Ziel-Collection, auf die der Alias zeigen soll.
93
+
94
+ Raises:
95
+ SolrClientError: Bei API-Fehler.
96
+ """
97
+ logger.info("Setze Alias '%s' auf Collection '%s'", alias, collection)
98
+ self._request(
99
+ "GET",
100
+ "/solr/admin/collections",
101
+ params={
102
+ "action": "CREATEALIAS",
103
+ "name": alias,
104
+ "collections": collection,
105
+ "wt": "json",
106
+ },
107
+ )
108
+
109
+ def delete_alias(self, alias: str) -> None:
110
+ """Löscht einen Alias.
111
+
112
+ Args:
113
+ alias: Name des zu löschenden Alias.
114
+
115
+ Raises:
116
+ SolrClientError: Bei API-Fehler.
117
+ """
118
+ logger.info("Lösche Alias '%s'", alias)
119
+ self._request(
120
+ "GET",
121
+ "/solr/admin/collections",
122
+ params={"action": "DELETEALIAS", "name": alias, "wt": "json"},
123
+ )
124
+
125
+ def delete_collection(self, collection: str) -> None:
126
+ """Löscht eine SolrCloud Collection.
127
+
128
+ Args:
129
+ collection: Name der zu löschenden Collection.
130
+
131
+ Raises:
132
+ SolrClientError: Bei API-Fehler.
133
+ """
134
+ logger.info("Lösche Collection '%s'", collection)
135
+ self._request(
136
+ "GET",
137
+ "/solr/admin/collections",
138
+ params={"action": "DELETE", "name": collection, "wt": "json"},
139
+ )
140
+
141
+ # ------------------------------------------------------------------
142
+ # CoreAdmin API – Cores (Standalone)
143
+ # ------------------------------------------------------------------
144
+
145
+ def list_cores(self) -> list[str]:
146
+ """Gibt die Namen aller vorhandenen Cores zurück。
147
+
148
+ Returns:
149
+ Liste der Core-Namen。
150
+
151
+ Raises:
152
+ SolrClientError: Bei API-Fehler。
153
+ """
154
+ response = self._request(
155
+ "GET",
156
+ "/solr/admin/cores",
157
+ params={"action": "STATUS", "wt": "json"},
158
+ )
159
+ return list(response.get("status", {}).keys())
160
+
161
+ def get_core_info(self, core: str) -> dict[str, Any]:
162
+ """Liefert detaillierte Informationen zu einem Core。
163
+
164
+ Enthält Name, Pfade, Startzeit, Uptime und Index-Statistiken
165
+ (Dokumentanzahl, Segmentanzahl, Größe usw.)。
166
+
167
+ Args:
168
+ core: Name des Core。
169
+
170
+ Returns:
171
+ Dict mit den Core-Informationen aus der STATUS-Antwort。
172
+
173
+ Raises:
174
+ SolrClientError: Bei API-Fehler oder wenn der Core nicht existiert。
175
+ """
176
+ response = self._request(
177
+ "GET",
178
+ "/solr/admin/cores",
179
+ params={"action": "STATUS", "core": core, "wt": "json"},
180
+ )
181
+ status = response.get("status", {})
182
+ if core not in status:
183
+ raise SolrClientError(f"Core '{core}' nicht gefunden")
184
+ return status[core]
185
+
186
+ def swap_cores(self, core1: str, core2: str) -> None:
187
+ """Tauscht zwei Cores via CoreAdmin SWAP-Action.
188
+
189
+ Nach dem Swap übernimmt ``core1`` alle Daten von ``core2`` und umgekehrt.
190
+
191
+ Args:
192
+ core1: Name des ersten Core (wird zum neuen aktiven Core).
193
+ core2: Name des zweiten Core.
194
+
195
+ Raises:
196
+ SolrClientError: Bei API-Fehler.
197
+ """
198
+ logger.info("Swap Cores: '%s' <-> '%s'", core1, core2)
199
+ self._request(
200
+ "GET",
201
+ "/solr/admin/cores",
202
+ params={"action": "SWAP", "core": core1, "other": core2, "wt": "json"},
203
+ )
204
+
205
+ def unload_core(self, core: str, *, delete_index: bool = False) -> None:
206
+ """Entlädt (und löscht optional) einen Core.
207
+
208
+ Args:
209
+ core: Name des Core.
210
+ delete_index: Wenn ``True``, werden Index-Daten auf Disk gelöscht.
211
+
212
+ Raises:
213
+ SolrClientError: Bei API-Fehler.
214
+ """
215
+ logger.info("Unload Core '%s' (delete_index=%s)", core, delete_index)
216
+ self._request(
217
+ "GET",
218
+ "/solr/admin/cores",
219
+ params={
220
+ "action": "UNLOAD",
221
+ "core": core,
222
+ "deleteIndex": str(delete_index).lower(),
223
+ "deleteDataDir": str(delete_index).lower(),
224
+ "deleteInstanceDir": str(delete_index).lower(),
225
+ "wt": "json",
226
+ },
227
+ )
228
+
229
+ def create_collection(
230
+ self,
231
+ collection: str,
232
+ num_shards: int = 1,
233
+ replication_factor: int = 1,
234
+ config_set: str = "_default",
235
+ ) -> None:
236
+ """Erstellt eine neue SolrCloud Collection.
237
+
238
+ Args:
239
+ collection: Name der Collection.
240
+ num_shards: Anzahl Shards (default: 1).
241
+ replication_factor: Replikationsfaktor (default: 1).
242
+ config_set: Zu verwendendes ConfigSet (default: "_default").
243
+
244
+ Raises:
245
+ SolrClientError: Bei API-Fehler.
246
+ """
247
+ logger.info(
248
+ "Erstelle Collection '%s' (shards=%d, replicas=%d)",
249
+ collection,
250
+ num_shards,
251
+ replication_factor,
252
+ )
253
+ self._request(
254
+ "GET",
255
+ "/solr/admin/collections",
256
+ params={
257
+ "action": "CREATE",
258
+ "name": collection,
259
+ "numShards": num_shards,
260
+ "replicationFactor": replication_factor,
261
+ "collection.configName": config_set,
262
+ "wt": "json",
263
+ },
264
+ )
265
+
266
+ def create_core(self, core: str, config_set: str = "_default") -> None:
267
+ """Erstellt einen neuen Standalone Core.
268
+
269
+ Args:
270
+ core: Name des Core.
271
+ config_set: Zu verwendendes ConfigSet (default: "_default").
272
+
273
+ Raises:
274
+ SolrClientError: Bei API-Fehler.
275
+ """
276
+ logger.info("Erstelle Core '%s'", core)
277
+ self._request(
278
+ "GET",
279
+ "/solr/admin/cores",
280
+ params={
281
+ "action": "CREATE",
282
+ "name": core,
283
+ "configSet": config_set,
284
+ "wt": "json",
285
+ },
286
+ )
287
+
288
+ def index_documents(
289
+ self,
290
+ core_or_collection: str,
291
+ documents: list[dict],
292
+ commit: bool = True,
293
+ ) -> None:
294
+ """Indexiert Dokumente in einen Core oder Collection.
295
+
296
+ Args:
297
+ core_or_collection: Name des Ziel-Core/Collection.
298
+ documents: Liste von Dokumenten (Dicts).
299
+ commit: Wenn True, wird sofort committed.
300
+
301
+ Raises:
302
+ SolrClientError: Bei API-Fehler.
303
+ """
304
+ if not documents:
305
+ logger.debug("Keine Dokumente zu indexieren")
306
+ return
307
+
308
+ logger.info(
309
+ "Indexiere %d Dokumente in '%s'", len(documents), core_or_collection
310
+ )
311
+
312
+ # Solr Update API verwendet POST mit JSON-Body
313
+ url = f"/solr/{core_or_collection}/update"
314
+ params: dict[str, Any] = {"wt": "json"}
315
+ if commit:
316
+ params["commit"] = "true"
317
+
318
+ response = self._client.post(url, params=params, json=documents)
319
+ response.raise_for_status()
320
+
321
+ data: dict[str, Any] = response.json()
322
+ header = data.get("responseHeader", {})
323
+ if header.get("status", 0) != 0:
324
+ raise SolrClientError(f"Solr-Fehler beim Indexieren: {data}")
325
+
326
+ logger.debug("Indexierung erfolgreich: %s", data)
327
+
328
+ # ------------------------------------------------------------------
329
+ # Konfigurationsdateien
330
+ # ------------------------------------------------------------------
331
+
332
+ def get_core_config(self, core: str) -> str:
333
+ """Liefert die solrconfig.xml eines Cores als XML-String.
334
+
335
+ Args:
336
+ core: Name des Core.
337
+
338
+ Returns:
339
+ Inhalt der solrconfig.xml als String.
340
+
341
+ Raises:
342
+ SolrClientError: Bei API-Fehler.
343
+ """
344
+ logger.info("Lese solrconfig.xml für Core '%s'", core)
345
+ return self._get_file(core, "solrconfig.xml")
346
+
347
+ def get_core_schema(self, core: str) -> str:
348
+ """Liefert das Schema eines Cores als XML-String.
349
+
350
+ Bei managed-schema Cores wird das Schema über die Schema-API
351
+ im schema.xml-Format abgerufen, da die Datei auf Disk
352
+ ``managed-schema.xml`` heißt und automatisch verwaltet wird.
353
+
354
+ Args:
355
+ core: Name des Core.
356
+
357
+ Returns:
358
+ Schema-Inhalt als XML-String.
359
+
360
+ Raises:
361
+ SolrClientError: Bei API-Fehler.
362
+ """
363
+ logger.info("Lese Schema für Core '%s'", core)
364
+ info = self.get_core_info(core)
365
+ schema_name = info.get("schema", "managed-schema.xml")
366
+
367
+ if schema_name == "managed-schema.xml":
368
+ return self._get_schema_api(core)
369
+
370
+ return self._get_file(core, schema_name)
371
+
372
+ def _get_file(self, core: str, filename: str) -> str:
373
+ """Liest eine Konfigurationsdatei über die Admin-File-API.
374
+
375
+ Args:
376
+ core: Name des Core.
377
+ filename: Dateiname (z. B. ``solrconfig.xml``).
378
+
379
+ Returns:
380
+ Dateiinhalt als String.
381
+
382
+ Raises:
383
+ SolrClientError: Bei API-Fehler.
384
+ """
385
+ try:
386
+ response = self._client.get(
387
+ f"/solr/{core}/admin/file",
388
+ params={"file": filename},
389
+ )
390
+ response.raise_for_status()
391
+ except httpx.HTTPStatusError as exc:
392
+ raise SolrClientError(
393
+ f"HTTP {exc.response.status_code} von Solr: {exc.response.text}"
394
+ ) from exc
395
+ except httpx.HTTPError as exc:
396
+ raise SolrClientError(f"Verbindungsfehler zu Solr: {exc}") from exc
397
+
398
+ return response.text
399
+
400
+ def _get_schema_api(self, core: str) -> str:
401
+ """Liest das Schema über die Schema-API im schema.xml-Format.
402
+
403
+ Args:
404
+ core: Name des Core.
405
+
406
+ Returns:
407
+ Schema als XML-String.
408
+
409
+ Raises:
410
+ SolrClientError: Bei API-Fehler.
411
+ """
412
+ try:
413
+ response = self._client.get(
414
+ f"/solr/{core}/schema",
415
+ params={"wt": "schema.xml"},
416
+ )
417
+ response.raise_for_status()
418
+ except httpx.HTTPStatusError as exc:
419
+ raise SolrClientError(
420
+ f"HTTP {exc.response.status_code} von Solr: {exc.response.text}"
421
+ ) from exc
422
+ except httpx.HTTPError as exc:
423
+ raise SolrClientError(f"Verbindungsfehler zu Solr: {exc}") from exc
424
+
425
+ return response.text
426
+
427
+ # ------------------------------------------------------------------
428
+ # Interner Hilfsmethoden
429
+ # ------------------------------------------------------------------
430
+
431
+ def _request(self, method: str, path: str, **kwargs: Any) -> dict[str, Any]:
432
+ """Sendet eine HTTP-Anfrage und wertet die JSON-Antwort aus.
433
+
434
+ Args:
435
+ method: HTTP-Methode (z. B. ``"GET"``).
436
+ path: URL-Pfad relativ zur Basis-URL.
437
+ **kwargs: Werden direkt an ``httpx.Client.request`` weitergegeben.
438
+
439
+ Returns:
440
+ Geparste JSON-Antwort als Dict.
441
+
442
+ Raises:
443
+ SolrClientError: Bei HTTP-Fehler oder Solr-Fehler in der Antwort.
444
+ """
445
+ try:
446
+ response = self._client.request(method, path, **kwargs)
447
+ response.raise_for_status()
448
+ except httpx.HTTPStatusError as exc:
449
+ raise SolrClientError(
450
+ f"HTTP {exc.response.status_code} von Solr: {exc.response.text}"
451
+ ) from exc
452
+ except httpx.HTTPError as exc:
453
+ raise SolrClientError(f"Verbindungsfehler zu Solr: {exc}") from exc
454
+
455
+ data: dict[str, Any] = response.json()
456
+
457
+ # Solr signalisiert Fehler manchmal im Body mit status != 0
458
+ header = data.get("responseHeader", {})
459
+ if header.get("status", 0) != 0:
460
+ raise SolrClientError(f"Solr-Fehler in Antwort: {data}")
461
+
462
+ logger.debug("Solr-Antwort: %s", data)
463
+ return data
464
+
465
+ def close(self) -> None:
466
+ """Schließt den HTTP-Client."""
467
+ self._client.close()
468
+
469
+ def __enter__(self) -> "SolrClient":
470
+ return self
471
+
472
+ def __exit__(self, *_: object) -> None:
473
+ self.close()
File without changes