charmarr-lib-core 0.12.2__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.
Files changed (32) hide show
  1. charmarr_lib/core/__init__.py +126 -0
  2. charmarr_lib/core/_arr/__init__.py +72 -0
  3. charmarr_lib/core/_arr/_arr_client.py +154 -0
  4. charmarr_lib/core/_arr/_base_client.py +314 -0
  5. charmarr_lib/core/_arr/_config_builders.py +214 -0
  6. charmarr_lib/core/_arr/_config_xml.py +121 -0
  7. charmarr_lib/core/_arr/_protocols.py +54 -0
  8. charmarr_lib/core/_arr/_reconcilers.py +269 -0
  9. charmarr_lib/core/_arr/_recyclarr.py +150 -0
  10. charmarr_lib/core/_juju/__init__.py +27 -0
  11. charmarr_lib/core/_juju/_pebble.py +102 -0
  12. charmarr_lib/core/_juju/_reconciler.py +137 -0
  13. charmarr_lib/core/_juju/_secrets.py +44 -0
  14. charmarr_lib/core/_k8s/__init__.py +43 -0
  15. charmarr_lib/core/_k8s/_hardware.py +191 -0
  16. charmarr_lib/core/_k8s/_permission_check.py +310 -0
  17. charmarr_lib/core/_k8s/_storage.py +253 -0
  18. charmarr_lib/core/_variant.py +37 -0
  19. charmarr_lib/core/_version.py +3 -0
  20. charmarr_lib/core/constants.py +29 -0
  21. charmarr_lib/core/enums.py +55 -0
  22. charmarr_lib/core/interfaces/__init__.py +78 -0
  23. charmarr_lib/core/interfaces/_base.py +103 -0
  24. charmarr_lib/core/interfaces/_download_client.py +125 -0
  25. charmarr_lib/core/interfaces/_flaresolverr.py +69 -0
  26. charmarr_lib/core/interfaces/_media_indexer.py +131 -0
  27. charmarr_lib/core/interfaces/_media_manager.py +111 -0
  28. charmarr_lib/core/interfaces/_media_server.py +74 -0
  29. charmarr_lib/core/interfaces/_media_storage.py +99 -0
  30. charmarr_lib_core-0.12.2.dist-info/METADATA +136 -0
  31. charmarr_lib_core-0.12.2.dist-info/RECORD +32 -0
  32. charmarr_lib_core-0.12.2.dist-info/WHEEL +4 -0
@@ -0,0 +1,126 @@
1
+ # Copyright 2025 The Charmarr Project
2
+ # See LICENSE file for licensing details.
3
+
4
+ """Core libraries for Charmarr charms."""
5
+
6
+ from charmarr_lib.core._arr import (
7
+ ApplicationConfigBuilder,
8
+ ArrApiClient,
9
+ ArrApiConnectionError,
10
+ ArrApiError,
11
+ ArrApiResponseError,
12
+ BaseArrApiClient,
13
+ DownloadClientConfigBuilder,
14
+ DownloadClientResponse,
15
+ HostConfigResponse,
16
+ MediaIndexerClient,
17
+ MediaManagerConnection,
18
+ QualityProfileResponse,
19
+ RecyclarrError,
20
+ RootFolderResponse,
21
+ SecretGetter,
22
+ config_has_api_key,
23
+ generate_api_key,
24
+ read_api_key,
25
+ reconcile_config_xml,
26
+ reconcile_download_clients,
27
+ reconcile_external_url,
28
+ reconcile_media_manager_connections,
29
+ reconcile_root_folder,
30
+ sync_trash_profiles,
31
+ update_api_key,
32
+ )
33
+ from charmarr_lib.core._juju import (
34
+ all_events,
35
+ ensure_pebble_user,
36
+ get_config_hash,
37
+ get_secret_rotation_policy,
38
+ observe_events,
39
+ reconcilable_events_k8s,
40
+ reconcilable_events_k8s_workloadless,
41
+ sync_secret_rotation_policy,
42
+ )
43
+ from charmarr_lib.core._k8s import (
44
+ K8sResourceManager,
45
+ PermissionCheckResult,
46
+ PermissionCheckStatus,
47
+ ReconcileResult,
48
+ check_storage_permissions,
49
+ delete_permission_check_job,
50
+ is_hardware_device_mounted,
51
+ is_storage_mounted,
52
+ reconcile_hardware_transcoding,
53
+ reconcile_storage_volume,
54
+ )
55
+ from charmarr_lib.core._variant import (
56
+ get_default_trash_profiles,
57
+ get_root_folder,
58
+ )
59
+ from charmarr_lib.core.constants import (
60
+ MEDIA_MANAGER_IMPLEMENTATIONS,
61
+ MEDIA_TYPE_DOWNLOAD_PATHS,
62
+ )
63
+ from charmarr_lib.core.enums import (
64
+ ContentVariant,
65
+ DownloadClient,
66
+ DownloadClientType,
67
+ MediaIndexer,
68
+ MediaManager,
69
+ RequestManager,
70
+ )
71
+
72
+ __all__ = [
73
+ "MEDIA_MANAGER_IMPLEMENTATIONS",
74
+ "MEDIA_TYPE_DOWNLOAD_PATHS",
75
+ "ApplicationConfigBuilder",
76
+ "ArrApiClient",
77
+ "ArrApiConnectionError",
78
+ "ArrApiError",
79
+ "ArrApiResponseError",
80
+ "BaseArrApiClient",
81
+ "ContentVariant",
82
+ "DownloadClient",
83
+ "DownloadClientConfigBuilder",
84
+ "DownloadClientResponse",
85
+ "DownloadClientType",
86
+ "HostConfigResponse",
87
+ "K8sResourceManager",
88
+ "MediaIndexer",
89
+ "MediaIndexerClient",
90
+ "MediaManager",
91
+ "MediaManagerConnection",
92
+ "PermissionCheckResult",
93
+ "PermissionCheckStatus",
94
+ "QualityProfileResponse",
95
+ "ReconcileResult",
96
+ "RecyclarrError",
97
+ "RequestManager",
98
+ "RootFolderResponse",
99
+ "SecretGetter",
100
+ "all_events",
101
+ "check_storage_permissions",
102
+ "config_has_api_key",
103
+ "delete_permission_check_job",
104
+ "ensure_pebble_user",
105
+ "generate_api_key",
106
+ "get_config_hash",
107
+ "get_default_trash_profiles",
108
+ "get_root_folder",
109
+ "get_secret_rotation_policy",
110
+ "is_hardware_device_mounted",
111
+ "is_storage_mounted",
112
+ "observe_events",
113
+ "read_api_key",
114
+ "reconcilable_events_k8s",
115
+ "reconcilable_events_k8s_workloadless",
116
+ "reconcile_config_xml",
117
+ "reconcile_download_clients",
118
+ "reconcile_external_url",
119
+ "reconcile_hardware_transcoding",
120
+ "reconcile_media_manager_connections",
121
+ "reconcile_root_folder",
122
+ "reconcile_storage_volume",
123
+ "sync_secret_rotation_policy",
124
+ "sync_trash_profiles",
125
+ "update_api_key",
126
+ ]
@@ -0,0 +1,72 @@
1
+ # Copyright 2025 The Charmarr Project
2
+ # See LICENSE file for licensing details.
3
+
4
+ """API clients, config builders, and reconcilers for *ARR applications."""
5
+
6
+ from charmarr_lib.core._arr._arr_client import (
7
+ ArrApiClient,
8
+ DownloadClientResponse,
9
+ HostConfigResponse,
10
+ QualityProfileResponse,
11
+ RootFolderResponse,
12
+ )
13
+ from charmarr_lib.core._arr._base_client import (
14
+ ArrApiConnectionError,
15
+ ArrApiError,
16
+ ArrApiResponseError,
17
+ BaseArrApiClient,
18
+ )
19
+ from charmarr_lib.core._arr._config_builders import (
20
+ ApplicationConfigBuilder,
21
+ DownloadClientConfigBuilder,
22
+ SecretGetter,
23
+ )
24
+ from charmarr_lib.core._arr._config_xml import (
25
+ config_has_api_key,
26
+ generate_api_key,
27
+ read_api_key,
28
+ reconcile_config_xml,
29
+ update_api_key,
30
+ )
31
+ from charmarr_lib.core._arr._protocols import (
32
+ MediaIndexerClient,
33
+ MediaManagerConnection,
34
+ )
35
+ from charmarr_lib.core._arr._reconcilers import (
36
+ reconcile_download_clients,
37
+ reconcile_external_url,
38
+ reconcile_media_manager_connections,
39
+ reconcile_root_folder,
40
+ )
41
+ from charmarr_lib.core._arr._recyclarr import (
42
+ RecyclarrError,
43
+ sync_trash_profiles,
44
+ )
45
+
46
+ __all__ = [
47
+ "ApplicationConfigBuilder",
48
+ "ArrApiClient",
49
+ "ArrApiConnectionError",
50
+ "ArrApiError",
51
+ "ArrApiResponseError",
52
+ "BaseArrApiClient",
53
+ "DownloadClientConfigBuilder",
54
+ "DownloadClientResponse",
55
+ "HostConfigResponse",
56
+ "MediaIndexerClient",
57
+ "MediaManagerConnection",
58
+ "QualityProfileResponse",
59
+ "RecyclarrError",
60
+ "RootFolderResponse",
61
+ "SecretGetter",
62
+ "config_has_api_key",
63
+ "generate_api_key",
64
+ "read_api_key",
65
+ "reconcile_config_xml",
66
+ "reconcile_download_clients",
67
+ "reconcile_external_url",
68
+ "reconcile_media_manager_connections",
69
+ "reconcile_root_folder",
70
+ "sync_trash_profiles",
71
+ "update_api_key",
72
+ ]
@@ -0,0 +1,154 @@
1
+ # Copyright 2025 The Charmarr Project
2
+ # See LICENSE file for licensing details.
3
+
4
+ """API client for Radarr, Sonarr, and Lidarr (/api/v3)."""
5
+
6
+ from typing import Any
7
+
8
+ from pydantic import BaseModel, Field
9
+
10
+ from charmarr_lib.core._arr._base_client import RESPONSE_MODEL_CONFIG, BaseArrApiClient
11
+
12
+
13
+ class DownloadClientResponse(BaseModel):
14
+ """Download client response from *arr API."""
15
+
16
+ model_config = RESPONSE_MODEL_CONFIG
17
+
18
+ id: int
19
+ name: str
20
+ enable: bool
21
+ protocol: str
22
+ implementation: str
23
+
24
+
25
+ class RootFolderResponse(BaseModel):
26
+ """Root folder response from *arr API."""
27
+
28
+ model_config = RESPONSE_MODEL_CONFIG
29
+
30
+ id: int
31
+ path: str
32
+ accessible: bool
33
+
34
+
35
+ class QualityProfileResponse(BaseModel):
36
+ """Quality profile response from *arr API."""
37
+
38
+ model_config = RESPONSE_MODEL_CONFIG
39
+
40
+ id: int
41
+ name: str
42
+
43
+
44
+ class HostConfigResponse(BaseModel):
45
+ """Host configuration response from *arr API."""
46
+
47
+ model_config = RESPONSE_MODEL_CONFIG
48
+
49
+ id: int
50
+ bind_address: str = Field(alias="bindAddress")
51
+ port: int
52
+ url_base: str | None = Field(default=None, alias="urlBase")
53
+
54
+
55
+ class ArrApiClient(BaseArrApiClient):
56
+ """API client for Radarr, Sonarr, and Lidarr (/api/v3).
57
+
58
+ Provides methods for managing download clients, root folders,
59
+ quality profiles, and host configuration.
60
+ """
61
+
62
+ def __init__(
63
+ self,
64
+ base_url: str,
65
+ api_key: str,
66
+ *,
67
+ timeout: float = 30.0,
68
+ max_retries: int = 3,
69
+ ) -> None:
70
+ """Initialize the v3 API client.
71
+
72
+ Args:
73
+ base_url: Base URL of the arr application (e.g., "http://localhost:7878")
74
+ api_key: API key for authentication
75
+ timeout: Request timeout in seconds
76
+ max_retries: Maximum number of retries for transient failures
77
+ """
78
+ super().__init__(
79
+ base_url=base_url,
80
+ api_key=api_key,
81
+ api_version="v3",
82
+ timeout=timeout,
83
+ max_retries=max_retries,
84
+ )
85
+
86
+ # Download Clients
87
+
88
+ def get_download_clients(self) -> list[DownloadClientResponse]:
89
+ """Get all configured download clients."""
90
+ return self._get_validated_list("/downloadclient", DownloadClientResponse)
91
+
92
+ def get_download_client(self, client_id: int) -> dict[str, Any]:
93
+ """Get a download client by ID as raw dict.
94
+
95
+ Args:
96
+ client_id: ID of the download client
97
+ """
98
+ return self._get(f"/downloadclient/{client_id}")
99
+
100
+ def add_download_client(self, config: dict[str, Any]) -> DownloadClientResponse:
101
+ """Add a new download client.
102
+
103
+ Args:
104
+ config: Download client configuration payload
105
+ """
106
+ return self._post_validated("/downloadclient", config, DownloadClientResponse)
107
+
108
+ def update_download_client(
109
+ self, client_id: int, config: dict[str, Any]
110
+ ) -> DownloadClientResponse:
111
+ """Update an existing download client.
112
+
113
+ Args:
114
+ client_id: ID of the download client to update
115
+ config: Updated download client configuration
116
+ """
117
+ config_with_id = {**config, "id": client_id}
118
+ return self._put_validated(
119
+ f"/downloadclient/{client_id}", config_with_id, DownloadClientResponse
120
+ )
121
+
122
+ def delete_download_client(self, client_id: int) -> None:
123
+ """Delete a download client.
124
+
125
+ Args:
126
+ client_id: ID of the download client to delete
127
+ """
128
+ self._delete(f"/downloadclient/{client_id}")
129
+
130
+ # Root Folders
131
+
132
+ def get_root_folders(self) -> list[RootFolderResponse]:
133
+ """Get all configured root folders."""
134
+ return self._get_validated_list("/rootfolder", RootFolderResponse)
135
+
136
+ def add_root_folder(self, path: str) -> RootFolderResponse:
137
+ """Add a new root folder.
138
+
139
+ Args:
140
+ path: Filesystem path for the root folder
141
+ """
142
+ return self._post_validated("/rootfolder", {"path": path}, RootFolderResponse)
143
+
144
+ # Quality Profiles (read-only for media-manager relation)
145
+
146
+ def get_quality_profiles(self) -> list[QualityProfileResponse]:
147
+ """Get all configured quality profiles."""
148
+ return self._get_validated_list("/qualityprofile", QualityProfileResponse)
149
+
150
+ # Host Config (get_host_config_raw and update_host_config are in BaseArrApiClient)
151
+
152
+ def get_host_config(self) -> HostConfigResponse:
153
+ """Get host configuration with typed response."""
154
+ return self._get_validated("/config/host", HostConfigResponse)
@@ -0,0 +1,314 @@
1
+ # Copyright 2025 The Charmarr Project
2
+ # See LICENSE file for licensing details.
3
+
4
+ """Base API client for *arr applications."""
5
+
6
+ import logging
7
+ from typing import Any, Self
8
+
9
+ import httpx
10
+ from pydantic import BaseModel, ConfigDict
11
+ from tenacity import (
12
+ retry,
13
+ retry_if_exception_type,
14
+ stop_after_attempt,
15
+ wait_exponential,
16
+ )
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+ # Response models use extra="allow" to accept unknown fields from the API.
21
+ # This ensures forward compatibility when *arr APIs add new fields, while
22
+ # still providing type safety for the fields we actually use.
23
+ RESPONSE_MODEL_CONFIG = ConfigDict(extra="allow", populate_by_name=True)
24
+
25
+
26
+ class ArrApiError(Exception):
27
+ """Base exception for arr API errors."""
28
+
29
+ def __init__(self, message: str, status_code: int | None = None) -> None:
30
+ super().__init__(message)
31
+ self.status_code = status_code
32
+
33
+
34
+ class ArrApiConnectionError(ArrApiError):
35
+ """Raised when connection to the API fails."""
36
+
37
+
38
+ class ArrApiResponseError(ArrApiError):
39
+ """Raised when the API returns an error response."""
40
+
41
+
42
+ class BaseArrApiClient:
43
+ """Shared HTTP mechanics for all arr applications.
44
+
45
+ Provides common HTTP patterns for interacting with *arr application APIs.
46
+ Handles session management, API key authentication, and error handling
47
+ with exponential backoff retries for transient failures.
48
+ """
49
+
50
+ def __init__(
51
+ self,
52
+ base_url: str,
53
+ api_key: str,
54
+ api_version: str,
55
+ *,
56
+ timeout: float = 30.0,
57
+ max_retries: int = 3,
58
+ ) -> None:
59
+ """Initialize the API client.
60
+
61
+ Args:
62
+ base_url: Base URL of the arr application (e.g., "http://localhost:7878")
63
+ api_key: API key for authentication
64
+ api_version: API version string (e.g., "v3" for Radarr, "v1" for Prowlarr)
65
+ timeout: Request timeout in seconds
66
+ max_retries: Maximum number of retries for transient failures
67
+ """
68
+ self._base_url = base_url.rstrip("/")
69
+ self._api_key = api_key
70
+ self._api_version = api_version
71
+ self._timeout = timeout
72
+ self._max_retries = max_retries
73
+ self._client: httpx.Client | None = None
74
+
75
+ @property
76
+ def client(self) -> httpx.Client:
77
+ """Get or create the HTTP client with configured headers."""
78
+ if self._client is None:
79
+ self._client = httpx.Client(
80
+ headers={"X-Api-Key": self._api_key},
81
+ timeout=self._timeout,
82
+ )
83
+ return self._client
84
+
85
+ def close(self) -> None:
86
+ """Close the HTTP client and release resources."""
87
+ if self._client is not None:
88
+ self._client.close()
89
+ self._client = None
90
+
91
+ def __enter__(self) -> Self:
92
+ """Context manager entry."""
93
+ return self
94
+
95
+ def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
96
+ """Context manager exit - close client."""
97
+ self.close()
98
+
99
+ def _url(self, endpoint: str) -> str:
100
+ """Build full URL for an API endpoint.
101
+
102
+ Args:
103
+ endpoint: API endpoint path (e.g., "/downloadclient")
104
+
105
+ Returns:
106
+ Full URL including base URL and API version prefix
107
+ """
108
+ endpoint = endpoint.lstrip("/")
109
+ return f"{self._base_url}/api/{self._api_version}/{endpoint}"
110
+
111
+ def _request(
112
+ self,
113
+ method: str,
114
+ endpoint: str,
115
+ *,
116
+ json: dict[str, Any] | None = None,
117
+ params: dict[str, Any] | None = None,
118
+ ) -> httpx.Response:
119
+ """Make an HTTP request with exponential backoff retry.
120
+
121
+ Args:
122
+ method: HTTP method (GET, POST, PUT, DELETE)
123
+ endpoint: API endpoint path
124
+ json: JSON body for POST/PUT requests
125
+ params: Query parameters
126
+
127
+ Returns:
128
+ HTTP response object
129
+
130
+ Raises:
131
+ ArrApiConnectionError: If connection fails after all retries
132
+ ArrApiResponseError: If the API returns an error response
133
+ """
134
+ url = self._url(endpoint)
135
+
136
+ @retry(
137
+ retry=retry_if_exception_type((httpx.ConnectError, httpx.TimeoutException)),
138
+ stop=stop_after_attempt(self._max_retries),
139
+ wait=wait_exponential(multiplier=1, min=1, max=10),
140
+ reraise=True,
141
+ )
142
+ def _do_request() -> httpx.Response:
143
+ response = self.client.request(
144
+ method=method,
145
+ url=url,
146
+ json=json,
147
+ params=params,
148
+ )
149
+ response.raise_for_status()
150
+ return response
151
+
152
+ try:
153
+ return _do_request()
154
+ except httpx.ConnectError as e:
155
+ raise ArrApiConnectionError(
156
+ f"Failed to connect to {url} after {self._max_retries} attempts"
157
+ ) from e
158
+ except httpx.TimeoutException as e:
159
+ raise ArrApiConnectionError(
160
+ f"Request to {url} timed out after {self._max_retries} attempts"
161
+ ) from e
162
+ except httpx.HTTPStatusError as e:
163
+ raise ArrApiResponseError(
164
+ f"API request failed: {e.response.status_code} {e.response.reason_phrase}",
165
+ status_code=e.response.status_code,
166
+ ) from e
167
+
168
+ def _get(self, endpoint: str, *, params: dict[str, Any] | None = None) -> Any:
169
+ """Make a GET request and return JSON response.
170
+
171
+ Args:
172
+ endpoint: API endpoint path
173
+ params: Optional query parameters
174
+
175
+ Returns:
176
+ Parsed JSON response (dict or list)
177
+ """
178
+ response = self._request("GET", endpoint, params=params)
179
+ return response.json()
180
+
181
+ def _post(self, endpoint: str, json: dict[str, Any]) -> dict[str, Any]:
182
+ """Make a POST request and return JSON response.
183
+
184
+ Args:
185
+ endpoint: API endpoint path
186
+ json: JSON body to send
187
+
188
+ Returns:
189
+ Parsed JSON response
190
+ """
191
+ response = self._request("POST", endpoint, json=json)
192
+ return response.json()
193
+
194
+ def _put(self, endpoint: str, json: dict[str, Any]) -> dict[str, Any]:
195
+ """Make a PUT request and return JSON response.
196
+
197
+ Args:
198
+ endpoint: API endpoint path
199
+ json: JSON body to send
200
+
201
+ Returns:
202
+ Parsed JSON response
203
+ """
204
+ response = self._request("PUT", endpoint, json=json)
205
+ return response.json()
206
+
207
+ def _delete(self, endpoint: str) -> None:
208
+ """Make a DELETE request.
209
+
210
+ Args:
211
+ endpoint: API endpoint path
212
+ """
213
+ self._request("DELETE", endpoint)
214
+
215
+ def _get_validated[ModelT: BaseModel](
216
+ self,
217
+ endpoint: str,
218
+ response_model: type[ModelT],
219
+ *,
220
+ params: dict[str, Any] | None = None,
221
+ ) -> ModelT:
222
+ """Make a GET request and validate response against a Pydantic model.
223
+
224
+ Args:
225
+ endpoint: API endpoint path
226
+ response_model: Pydantic model class to validate against
227
+ params: Optional query parameters
228
+
229
+ Returns:
230
+ Validated Pydantic model instance
231
+ """
232
+ data = self._get(endpoint, params=params)
233
+ return response_model.model_validate(data)
234
+
235
+ def _get_validated_list[ModelT: BaseModel](
236
+ self,
237
+ endpoint: str,
238
+ item_model: type[ModelT],
239
+ *,
240
+ params: dict[str, Any] | None = None,
241
+ ) -> list[ModelT]:
242
+ """Make a GET request and validate response as a list of Pydantic models.
243
+
244
+ Args:
245
+ endpoint: API endpoint path
246
+ item_model: Pydantic model class for list items
247
+ params: Optional query parameters
248
+
249
+ Returns:
250
+ List of validated Pydantic model instances
251
+ """
252
+ data = self._get(endpoint, params=params)
253
+ return [item_model.model_validate(item) for item in data]
254
+
255
+ def _post_validated[ModelT: BaseModel](
256
+ self,
257
+ endpoint: str,
258
+ json: dict[str, Any],
259
+ response_model: type[ModelT],
260
+ ) -> ModelT:
261
+ """Make a POST request and validate response against a Pydantic model.
262
+
263
+ Args:
264
+ endpoint: API endpoint path
265
+ json: JSON body to send
266
+ response_model: Pydantic model class to validate against
267
+
268
+ Returns:
269
+ Validated Pydantic model instance
270
+ """
271
+ data = self._post(endpoint, json)
272
+ return response_model.model_validate(data)
273
+
274
+ def _put_validated[ModelT: BaseModel](
275
+ self,
276
+ endpoint: str,
277
+ json: dict[str, Any],
278
+ response_model: type[ModelT],
279
+ ) -> ModelT:
280
+ """Make a PUT request and validate response against a Pydantic model.
281
+
282
+ Args:
283
+ endpoint: API endpoint path
284
+ json: JSON body to send
285
+ response_model: Pydantic model class to validate against
286
+
287
+ Returns:
288
+ Validated Pydantic model instance
289
+ """
290
+ data = self._put(endpoint, json)
291
+ return response_model.model_validate(data)
292
+
293
+ def get_host_config_raw(self) -> dict[str, Any]:
294
+ """Get host configuration as raw dict.
295
+
296
+ All *arr applications have a /config/host endpoint that returns
297
+ settings like bindAddress, port, urlBase, and applicationUrl.
298
+ """
299
+ return self._get("/config/host")
300
+
301
+ def update_host_config(self, config: dict[str, Any]) -> dict[str, Any]:
302
+ """Update host configuration.
303
+
304
+ Merges provided config with current settings and PUTs the result.
305
+
306
+ Args:
307
+ config: Host configuration settings to update
308
+
309
+ Returns:
310
+ Updated host configuration
311
+ """
312
+ current = self._get("/config/host")
313
+ updated = {**current, **config}
314
+ return self._put("/config/host", updated)