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.
- charmarr_lib/core/__init__.py +126 -0
- charmarr_lib/core/_arr/__init__.py +72 -0
- charmarr_lib/core/_arr/_arr_client.py +154 -0
- charmarr_lib/core/_arr/_base_client.py +314 -0
- charmarr_lib/core/_arr/_config_builders.py +214 -0
- charmarr_lib/core/_arr/_config_xml.py +121 -0
- charmarr_lib/core/_arr/_protocols.py +54 -0
- charmarr_lib/core/_arr/_reconcilers.py +269 -0
- charmarr_lib/core/_arr/_recyclarr.py +150 -0
- charmarr_lib/core/_juju/__init__.py +27 -0
- charmarr_lib/core/_juju/_pebble.py +102 -0
- charmarr_lib/core/_juju/_reconciler.py +137 -0
- charmarr_lib/core/_juju/_secrets.py +44 -0
- charmarr_lib/core/_k8s/__init__.py +43 -0
- charmarr_lib/core/_k8s/_hardware.py +191 -0
- charmarr_lib/core/_k8s/_permission_check.py +310 -0
- charmarr_lib/core/_k8s/_storage.py +253 -0
- charmarr_lib/core/_variant.py +37 -0
- charmarr_lib/core/_version.py +3 -0
- charmarr_lib/core/constants.py +29 -0
- charmarr_lib/core/enums.py +55 -0
- charmarr_lib/core/interfaces/__init__.py +78 -0
- charmarr_lib/core/interfaces/_base.py +103 -0
- charmarr_lib/core/interfaces/_download_client.py +125 -0
- charmarr_lib/core/interfaces/_flaresolverr.py +69 -0
- charmarr_lib/core/interfaces/_media_indexer.py +131 -0
- charmarr_lib/core/interfaces/_media_manager.py +111 -0
- charmarr_lib/core/interfaces/_media_server.py +74 -0
- charmarr_lib/core/interfaces/_media_storage.py +99 -0
- charmarr_lib_core-0.12.2.dist-info/METADATA +136 -0
- charmarr_lib_core-0.12.2.dist-info/RECORD +32 -0
- charmarr_lib_core-0.12.2.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# Copyright 2025 The Charmarr Project
|
|
2
|
+
# See LICENSE file for licensing details.
|
|
3
|
+
|
|
4
|
+
"""Media storage interface for shared PVC and PUID/PGID coordination."""
|
|
5
|
+
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from ops import EventBase, EventSource, ObjectEvents
|
|
9
|
+
from pydantic import BaseModel, Field
|
|
10
|
+
|
|
11
|
+
from charmarr_lib.core.interfaces._base import (
|
|
12
|
+
EventObservingMixin,
|
|
13
|
+
RelationInterfaceBase,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class MediaStorageProviderData(BaseModel):
|
|
18
|
+
"""Data published by charmarr-storage charm."""
|
|
19
|
+
|
|
20
|
+
pvc_name: str = Field(description="Name of the shared PVC to mount")
|
|
21
|
+
mount_path: str = Field(
|
|
22
|
+
default="/data", description="Mount path for the shared storage inside containers"
|
|
23
|
+
)
|
|
24
|
+
puid: int = Field(default=1000, description="User ID for file ownership (LinuxServer PUID)")
|
|
25
|
+
pgid: int = Field(default=1000, description="Group ID for file ownership (LinuxServer PGID)")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class MediaStorageRequirerData(BaseModel):
|
|
29
|
+
"""Data published by apps mounting storage."""
|
|
30
|
+
|
|
31
|
+
instance_name: str = Field(description="Juju application name")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class MediaStorageChangedEvent(EventBase):
|
|
35
|
+
"""Event emitted when media-storage relation state changes."""
|
|
36
|
+
|
|
37
|
+
pass
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class MediaStorageProvider(
|
|
41
|
+
RelationInterfaceBase[MediaStorageProviderData, MediaStorageRequirerData]
|
|
42
|
+
):
|
|
43
|
+
"""Provider side of media-storage interface (passive - no events)."""
|
|
44
|
+
|
|
45
|
+
def __init__(self, charm: Any, relation_name: str = "media-storage") -> None:
|
|
46
|
+
super().__init__(charm, relation_name)
|
|
47
|
+
|
|
48
|
+
def _get_remote_data_model(self) -> type[MediaStorageRequirerData]:
|
|
49
|
+
return MediaStorageRequirerData
|
|
50
|
+
|
|
51
|
+
def publish_data(self, data: MediaStorageProviderData) -> None:
|
|
52
|
+
"""Publish provider data to all relations."""
|
|
53
|
+
self._publish_to_all_relations(data)
|
|
54
|
+
|
|
55
|
+
def clear_data(self) -> None:
|
|
56
|
+
"""Clear provider data from all relations."""
|
|
57
|
+
if not self._charm.unit.is_leader():
|
|
58
|
+
return
|
|
59
|
+
|
|
60
|
+
for relation in self._charm.model.relations.get(self._relation_name, []):
|
|
61
|
+
if "config" in relation.data[self._charm.app]:
|
|
62
|
+
del relation.data[self._charm.app]["config"]
|
|
63
|
+
|
|
64
|
+
def get_connected_apps(self) -> list[str]:
|
|
65
|
+
"""Get list of connected application names (for logging/metrics)."""
|
|
66
|
+
return [r.instance_name for r in self._get_all_remote_app_data()]
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class MediaStorageRequirerEvents(ObjectEvents):
|
|
70
|
+
"""Events emitted by MediaStorageRequirer."""
|
|
71
|
+
|
|
72
|
+
changed = EventSource(MediaStorageChangedEvent)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class MediaStorageRequirer(
|
|
76
|
+
EventObservingMixin, RelationInterfaceBase[MediaStorageRequirerData, MediaStorageProviderData]
|
|
77
|
+
):
|
|
78
|
+
"""Requirer side of media-storage interface."""
|
|
79
|
+
|
|
80
|
+
on = MediaStorageRequirerEvents() # type: ignore[assignment]
|
|
81
|
+
|
|
82
|
+
def __init__(self, charm: Any, relation_name: str = "media-storage") -> None:
|
|
83
|
+
super().__init__(charm, relation_name)
|
|
84
|
+
self._setup_event_observation()
|
|
85
|
+
|
|
86
|
+
def _get_remote_data_model(self) -> type[MediaStorageProviderData]:
|
|
87
|
+
return MediaStorageProviderData
|
|
88
|
+
|
|
89
|
+
def publish_data(self, data: MediaStorageRequirerData) -> None:
|
|
90
|
+
"""Publish requirer data to the relation."""
|
|
91
|
+
self._publish_to_single_relation(data)
|
|
92
|
+
|
|
93
|
+
def get_provider(self) -> MediaStorageProviderData | None:
|
|
94
|
+
"""Get storage provider data if available."""
|
|
95
|
+
return self._get_single_provider_data()
|
|
96
|
+
|
|
97
|
+
def is_ready(self) -> bool:
|
|
98
|
+
"""Check if storage is available."""
|
|
99
|
+
return self.get_provider() is not None
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: charmarr-lib-core
|
|
3
|
+
Version: 0.12.2
|
|
4
|
+
Summary: Core charm libraries for Charmarr media automation
|
|
5
|
+
Author-email: Adhitya Ravi <adhityashan95@gmail.com>
|
|
6
|
+
License: LGPL-3.0-or-later
|
|
7
|
+
Classifier: Development Status :: 3 - Alpha
|
|
8
|
+
Classifier: Intended Audience :: Developers
|
|
9
|
+
Classifier: License :: OSI Approved :: GNU Lesser General Public License v3 or later (LGPLv3+)
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
+
Requires-Python: >=3.12
|
|
13
|
+
Requires-Dist: charmarr-lib-krm>=0.1
|
|
14
|
+
Requires-Dist: httpx>=0.27
|
|
15
|
+
Requires-Dist: lightkube>=0.15
|
|
16
|
+
Requires-Dist: ops>=2.0
|
|
17
|
+
Requires-Dist: pydantic>=2.0
|
|
18
|
+
Provides-Extra: dev
|
|
19
|
+
Requires-Dist: mypy>=1.8; extra == 'dev'
|
|
20
|
+
Requires-Dist: pytest-cov>=4.0; extra == 'dev'
|
|
21
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
22
|
+
Requires-Dist: ruff>=0.1; extra == 'dev'
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
<p align="center">
|
|
26
|
+
<img src="../assets/charmarr-charmarr-lib.png" width="350" alt="Charmarr Lib">
|
|
27
|
+
</p>
|
|
28
|
+
|
|
29
|
+
<h1 align="center">charmarr-lib-core</h1>
|
|
30
|
+
|
|
31
|
+
Core charm libraries for Charmarr media automation.
|
|
32
|
+
|
|
33
|
+
## Features
|
|
34
|
+
|
|
35
|
+
- Juju relation interfaces for media automation
|
|
36
|
+
- API clients for *arr applications (Radarr, Sonarr, Prowlarr, etc.)
|
|
37
|
+
- Reconcilers for managing application configuration
|
|
38
|
+
- Pydantic models for type-safe data validation
|
|
39
|
+
- K8s storage utilities for StatefulSet patching
|
|
40
|
+
|
|
41
|
+
## Installation
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
pip install charmarr-lib-core
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Usage
|
|
48
|
+
|
|
49
|
+
### Interfaces
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
from charmarr_lib.core.interfaces import (
|
|
53
|
+
# Media Indexer (Prowlarr <-> Radarr/Sonarr/Lidarr)
|
|
54
|
+
MediaIndexerProvider,
|
|
55
|
+
MediaIndexerRequirer,
|
|
56
|
+
MediaIndexerProviderData,
|
|
57
|
+
MediaIndexerRequirerData,
|
|
58
|
+
# Download Client (qBittorrent/SABnzbd <-> Radarr/Sonarr/Lidarr)
|
|
59
|
+
DownloadClientProvider,
|
|
60
|
+
DownloadClientRequirer,
|
|
61
|
+
DownloadClientProviderData,
|
|
62
|
+
DownloadClientRequirerData,
|
|
63
|
+
# Media Storage (shared PVC <-> all apps)
|
|
64
|
+
MediaStorageProvider,
|
|
65
|
+
MediaStorageRequirer,
|
|
66
|
+
MediaStorageProviderData,
|
|
67
|
+
MediaStorageRequirerData,
|
|
68
|
+
# Media Manager (Radarr/Sonarr <-> Overseerr/Jellyseerr)
|
|
69
|
+
MediaManagerProvider,
|
|
70
|
+
MediaManagerRequirer,
|
|
71
|
+
MediaManagerProviderData,
|
|
72
|
+
MediaManagerRequirerData,
|
|
73
|
+
QualityProfile,
|
|
74
|
+
)
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### API Clients
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
from charmarr_lib.core import (
|
|
81
|
+
# Radarr/Sonarr/Lidarr client (v3 API)
|
|
82
|
+
ArrApiClient,
|
|
83
|
+
# Prowlarr client (v1 API)
|
|
84
|
+
ProwlarrApiClient,
|
|
85
|
+
# Errors
|
|
86
|
+
ArrApiError,
|
|
87
|
+
ArrApiConnectionError,
|
|
88
|
+
ArrApiResponseError,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
# Connect to Radarr
|
|
92
|
+
with ArrApiClient("http://radarr:7878", api_key) as client:
|
|
93
|
+
profiles = client.get_quality_profiles()
|
|
94
|
+
client.add_root_folder("/movies")
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Reconcilers
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
from charmarr_lib.core import (
|
|
101
|
+
reconcile_download_clients,
|
|
102
|
+
reconcile_media_manager_connections,
|
|
103
|
+
reconcile_root_folder,
|
|
104
|
+
reconcile_external_url,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
# Sync download clients from relations to Radarr
|
|
108
|
+
reconcile_download_clients(
|
|
109
|
+
api_client=arr_client,
|
|
110
|
+
desired_clients=download_client_data_from_relations,
|
|
111
|
+
category="radarr",
|
|
112
|
+
media_manager=MediaManager.RADARR,
|
|
113
|
+
get_secret=lambda sid: model.get_secret(id=sid).get_content(),
|
|
114
|
+
)
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Storage Utilities
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
from charmarr_lib.core import (
|
|
121
|
+
reconcile_storage_volume,
|
|
122
|
+
is_storage_mounted,
|
|
123
|
+
K8sResourceManager,
|
|
124
|
+
ReconcileResult,
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
manager = K8sResourceManager()
|
|
128
|
+
result = reconcile_storage_volume(
|
|
129
|
+
manager=manager,
|
|
130
|
+
statefulset_name="radarr",
|
|
131
|
+
container_name="radarr", # from charmcraft.yaml
|
|
132
|
+
namespace="media",
|
|
133
|
+
pvc_name="media-storage",
|
|
134
|
+
mount_path="/data",
|
|
135
|
+
)
|
|
136
|
+
```
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
charmarr_lib/core/__init__.py,sha256=9iTHLea8kbXNPBOAcBkx4wQK3T3wa5LPAW0TGm9qop4,3230
|
|
2
|
+
charmarr_lib/core/_variant.py,sha256=5SLyJn3fEsbG4K8iAIklwJTSkehkHzpJg-_-RpzkoMg,1356
|
|
3
|
+
charmarr_lib/core/_version.py,sha256=L6cX0RHoWTs-DaGZVEELehp3559HrfGryRfTWHaRXWg,93
|
|
4
|
+
charmarr_lib/core/constants.py,sha256=cn1TUDzQ79bD_olGxnxutKg-ccfhYPOVZa8P8UCdbo4,1260
|
|
5
|
+
charmarr_lib/core/enums.py,sha256=EIhDNYxTH2QNHzlmyTCFysnNIdAuMPff8vIKqcXUAfk,1120
|
|
6
|
+
charmarr_lib/core/_arr/__init__.py,sha256=hS7TGAbWVpyNBX51yLNVetF5-aYgkT_DmOWUPjQ7e1M,1842
|
|
7
|
+
charmarr_lib/core/_arr/_arr_client.py,sha256=ZZCJccFGibRKrt1TW5Romw8GXGTsqAUy7RPhAdw2jKM,4544
|
|
8
|
+
charmarr_lib/core/_arr/_base_client.py,sha256=9qEJHKhu2C_R2PR54MRLejMhOIklPm2WZ6xATxaN3Zw,9831
|
|
9
|
+
charmarr_lib/core/_arr/_config_builders.py,sha256=ON3tyn9bsBhggD-NHzXrWdMYyT643x5xm8Y1in2KlKU,8418
|
|
10
|
+
charmarr_lib/core/_arr/_config_xml.py,sha256=picvSBFoUgACc9A-Gi_P-G3qb5onklwsDAdIOKu08Zk,3510
|
|
11
|
+
charmarr_lib/core/_arr/_protocols.py,sha256=UqnNwSJpbWx29_7K_2svLKClMaj1ccQdjyYjZUKmXZg,1625
|
|
12
|
+
charmarr_lib/core/_arr/_reconcilers.py,sha256=xdoA-tuggTC-HTFM5g5yi_3OpqX6f0CNS8LdwpQbZYA,9041
|
|
13
|
+
charmarr_lib/core/_arr/_recyclarr.py,sha256=Qo55BwgDjfJ3OxQMz9WP98QqY_YlK-qq6j2Di_8AEDM,4666
|
|
14
|
+
charmarr_lib/core/_juju/__init__.py,sha256=c33kTuwUtmauzM9Z2RTRPFcw4d9JhMU__JQoZdjazIA,734
|
|
15
|
+
charmarr_lib/core/_juju/_pebble.py,sha256=2BYTl1bvVJYH_3NIRIeI_hiNjg2TesQtDlKIgYPrfko,3422
|
|
16
|
+
charmarr_lib/core/_juju/_reconciler.py,sha256=8fqmfI-y7oAI8Y9WecKq_tcWRsRW9RnNcXK5kiFkv7g,4780
|
|
17
|
+
charmarr_lib/core/_juju/_secrets.py,sha256=_silLiY3l7hOh5a_QpRH0U86oBqaIFmq6QMel97U7iw,1384
|
|
18
|
+
charmarr_lib/core/_k8s/__init__.py,sha256=Y_UXl8074Cxo4MdHVJRpp9-h-cp7fu32NFeXQAdK0YM,1398
|
|
19
|
+
charmarr_lib/core/_k8s/_hardware.py,sha256=ZAf48vznw5_35w0LaCa24d1RaXiCrQFphIDvetDgqko,6711
|
|
20
|
+
charmarr_lib/core/_k8s/_permission_check.py,sha256=fFW5RMZf1T2wANPBbcSbHqpjbwmjvyIAuetODtnz9NA,9105
|
|
21
|
+
charmarr_lib/core/_k8s/_storage.py,sha256=Z4znpev2wNHG04JvmCi7ekB2kRGxvpLiZXAkYIJkhls,8786
|
|
22
|
+
charmarr_lib/core/interfaces/__init__.py,sha256=tTbQTrYcUCBGVS3qspRpEpUrJwvw_U828Yz7c7D9lkY,2211
|
|
23
|
+
charmarr_lib/core/interfaces/_base.py,sha256=QEOMva4qzEmnm6NivzJFYc5jgFkWwWKbkMaxSFkTLuU,4185
|
|
24
|
+
charmarr_lib/core/interfaces/_download_client.py,sha256=y02oLszgSSoz86-1Oa8n-LbbFsRhSwfTmlAZqt60E10,4304
|
|
25
|
+
charmarr_lib/core/interfaces/_flaresolverr.py,sha256=Jqdw6cS-vBdKcKuK3v4aGB9Ln9VNIRrwZOj-3m6B4BE,2152
|
|
26
|
+
charmarr_lib/core/interfaces/_media_indexer.py,sha256=rEbVXOtdFzi4VWcJ8C7MvLHryy3JBOtbt4DpxKQM5cY,4300
|
|
27
|
+
charmarr_lib/core/interfaces/_media_manager.py,sha256=BDL5gQFWkiq6zjfjgx_jF1x22rlQ5k_qpERAZrrjeLM,4073
|
|
28
|
+
charmarr_lib/core/interfaces/_media_server.py,sha256=vkdVkKeDyWS-mk1KZqtKm4Pca95IP2-dM-drgtOswe8,2432
|
|
29
|
+
charmarr_lib/core/interfaces/_media_storage.py,sha256=affcBNnZ6AssXt_NQDfWD9gzPtnbMYh00Pg-GuG5CzY,3451
|
|
30
|
+
charmarr_lib_core-0.12.2.dist-info/METADATA,sha256=1tOcrh_tWevgvA8HMDYIkrhY2Uzak8Jd5UQ7e8zk0NM,3561
|
|
31
|
+
charmarr_lib_core-0.12.2.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
32
|
+
charmarr_lib_core-0.12.2.dist-info/RECORD,,
|