data-platform-helpers 0.1.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.
File without changes
@@ -0,0 +1,244 @@
1
+ # Copyright 2024 Canonical Ltd.
2
+ # See LICENSE file for licensing details.
3
+
4
+ """Class used to determine if a relevant version attribute across related applications are valid.
5
+
6
+ There are many potential applications for this. Here are a few examples:
7
+ 1. in a sharded cluster where is is important that the shards and cluster manager have the same
8
+ components.
9
+ 2. kafka connect and kafka broker apps working together and needing to have the same ubnderlying
10
+ version.
11
+
12
+ How to use:
13
+
14
+ 1. in src/charm.py of requirer + provider
15
+ in constructor [REQUIRED]:
16
+ self.version_checker = self.CrossAppVersionChecker(
17
+ self,
18
+ version=x, # can be a revision of a charm, version of a snap, version of a workload, etc
19
+ relations_to_check=[x,y,z],
20
+ # only use if the version doesn't not need to exactly match our current version
21
+ version_validity_range={"x": "<a,>b"})
22
+
23
+ in update status hook [OPTIONAL]:
24
+ if not self.version_checker.are_related_apps_valid():
25
+ logger.debug(
26
+ "Warning relational version check failed, these relations have mismatched versions",
27
+ "%s",
28
+ self.version_checker(self.version_checker.get_invalid_versions())
29
+ )
30
+ # can set status, instruct user to change
31
+
32
+ 2. other areas of the charm (i.e. joined events, action events, etc) [OPTIONAL]:
33
+ if not self.charm.version_checker.are_related_apps_valid():
34
+ # do something - i.e. fail event or log message
35
+
36
+ 3. in upgrade handler of requirer + provider [REQUIRED]:
37
+ if [last unit to upgrade]:
38
+ self.charm.version.set_version_across_all_relations()
39
+
40
+
41
+ """
42
+ import logging
43
+ from typing import Dict, List, Optional, Tuple
44
+
45
+ from ops.charm import CharmBase
46
+ from ops.framework import Object
47
+ from ops.model import Unit
48
+
49
+ logger = logging.getLogger(__name__)
50
+
51
+ VERSION_CONST = "version"
52
+ DEPLOYMENT_TYPE = "deployment"
53
+ PREFIX_DIR = "/var/lib/juju/agents/"
54
+ LOCAL_BUILT_CHARM_PREFIX = "local"
55
+
56
+
57
+ def get_charm_revision(unit: Unit) -> int:
58
+ """Returns the charm revision.
59
+
60
+ TODO: Keep this until ops framework supports: https://github.com/canonical/operator/issues/1255
61
+ """
62
+ file_path = f"{PREFIX_DIR}unit-{unit.name.replace('/','-')}/charm/.juju-charm"
63
+ with open(file_path) as f:
64
+ charm_path = f.read().rstrip()
65
+
66
+ # revision of charm in a locally built chamr is unreliable:
67
+ # https://chat.canonical.com/canonical/pl/ro9935ayxbyyxn9hn6opy4f4xw
68
+ if charm_path.split(":")[0] == LOCAL_BUILT_CHARM_PREFIX:
69
+ logger.debug("Charm is locally built. Cannot determine revision number.")
70
+ return 0
71
+
72
+ # charm_path is of the format ch:amd64/jammy/<charm-name>-<revision number>
73
+ revision = charm_path.split("-")[-1]
74
+ return int(revision)
75
+
76
+
77
+ class CrossAppVersionChecker(Object):
78
+ """Verifies versions across multiple integrated applications."""
79
+
80
+ def __init__(
81
+ self,
82
+ charm: CharmBase,
83
+ version: int,
84
+ relations_to_check: List[str],
85
+ version_validity_range: Optional[Dict] = None,
86
+ ) -> None:
87
+ """Constructor for CrossAppVersionChecker.
88
+
89
+ Args:
90
+ charm: charm to inherit from
91
+ version: (int), the current version of the desired attribute of the charm
92
+ relations_to_check: (List), a list of relations who should have compatible versions
93
+ with the current charm
94
+ version_validity_range: (Optional Dict), a list of ranges for valid version ranges.
95
+ If not provided it is assumed that relations on the provided interface must have
96
+ the same version.
97
+ """
98
+ super().__init__(charm, None)
99
+ self.charm = charm
100
+ # Future PR: upgrade this to a dictionary name versions
101
+ self.version = version
102
+ self.relations_to_check = relations_to_check
103
+
104
+ for rel in relations_to_check:
105
+ self.framework.observe(
106
+ charm.on[rel].relation_created,
107
+ self.set_version_on_relation_created,
108
+ )
109
+
110
+ # this feature has yet to be implemented, MongoDB does not need it and it is unclear if
111
+ # this will be extended to other charms. If this code is extended to other charms and
112
+ # there is a valid usecase we will use the `version_validity_range` variable in the
113
+ # function `get_invalid_versions`
114
+ self.version_validity_range = version_validity_range
115
+
116
+ def get_invalid_versions(self) -> List[Tuple[str, int]]:
117
+ """Returns a list of (app name, version number) pairs, if the version number mismatches.
118
+
119
+ Mismatches are decided based on version_validity_range, if version_validity_range is not
120
+ provided, then the mismatch is expected to match this current app's version number.
121
+
122
+ Raises:
123
+ NoVersionError.
124
+ """
125
+ try:
126
+ invalid_relations = []
127
+ for relation_name in self.relations_to_check:
128
+ for relation in self.charm.model.relations[relation_name]:
129
+ related_version = relation.data[relation.app][VERSION_CONST]
130
+ if int(related_version) != self.version:
131
+ invalid_relations.append((relation.app.name, int(related_version)))
132
+ except KeyError:
133
+ raise NoVersionError(f"Expected {relation.app.name} to have version info.")
134
+
135
+ return invalid_relations
136
+
137
+ def get_version_of_related_app(self, related_app_name: str) -> int:
138
+ """Returns a int for the version of the related app.
139
+
140
+ Raises:
141
+ NoVersionError.
142
+ """
143
+ try:
144
+ for relation_name in self.relations_to_check:
145
+ for rel in self.charm.model.relations[relation_name]:
146
+ if rel.app.name == related_app_name:
147
+ return int(rel.data[rel.app][VERSION_CONST])
148
+ except KeyError:
149
+ pass
150
+
151
+ raise NoVersionError(f"Expected {related_app_name} to have version info.")
152
+
153
+ def get_deployment_prefix(self) -> str:
154
+ """Returns the deployment prefix, indicating if the charm is locally deployred or not.
155
+
156
+ TODO: Keep this until ops framework supports:
157
+ https://github.com/canonical/operator/issues/1255
158
+ """
159
+ file_path = f"{PREFIX_DIR}/unit-{self.charm.unit.name.replace('/','-')}/charm/.juju-charm"
160
+ with open(file_path) as f:
161
+ charm_path = f.read().rstrip()
162
+
163
+ return charm_path.split(":")[0]
164
+
165
+ def are_related_apps_valid(self) -> bool:
166
+ """Returns True if a related app has a version that's incompatible with the current app.
167
+
168
+ Raises:
169
+ NoVersionError.
170
+ """
171
+ return self.get_invalid_versions() == []
172
+
173
+ def set_version_across_all_relations(self) -> None:
174
+ """Sets the version number across all related apps, prvided by relations_to_check."""
175
+ if not self.charm.unit.is_leader():
176
+ return
177
+
178
+ for relation_name in self.relations_to_check:
179
+ for rel in self.charm.model.relations[relation_name]:
180
+ rel.data[self.charm.model.app][VERSION_CONST] = str(self.version)
181
+ rel.data[self.charm.model.app][DEPLOYMENT_TYPE] = str(self.get_deployment_prefix())
182
+
183
+ def set_version_on_related_app(self, relation_name: str, related_app_name: str) -> None:
184
+ """Sets the version number across for a specified relation on a specified app."""
185
+ if not self.charm.unit.is_leader():
186
+ return
187
+
188
+ for rel in self.charm.model.relations[relation_name]:
189
+ if rel.app.name == related_app_name:
190
+ rel.data[self.charm.model.app][VERSION_CONST] = str(self.version)
191
+ rel.data[self.charm.model.app][DEPLOYMENT_TYPE] = str(self.get_deployment_prefix())
192
+
193
+ def set_version_on_relation_created(self, event) -> None:
194
+ """Shares the charm's revision to the newly integrated application.
195
+
196
+ Raises:
197
+ RelationInvalidError
198
+ """
199
+ if event.relation.name not in self.relations_to_check:
200
+ raise RelationInvalidError(
201
+ f"Provided relation: {event.relation.name} not in self.relations_to_check."
202
+ )
203
+
204
+ self.set_version_on_related_app(event.relation.name, event.app.name)
205
+
206
+ def is_integrated_to_locally_built_charm(self) -> bool:
207
+ """Returns a boolean value indicating whether the charm is integrated is a local charm.
208
+
209
+ NOTE: this function ONLY checks relations on the provided interfaces in
210
+ relations_to_check.
211
+ """
212
+ for relation_name in self.relations_to_check:
213
+ for rel in self.charm.model.relations[relation_name]:
214
+ if rel.data[rel.app][DEPLOYMENT_TYPE] == LOCAL_BUILT_CHARM_PREFIX:
215
+ return True
216
+
217
+ return False
218
+
219
+ def is_local_charm(self, app_name: str) -> bool:
220
+ """Returns a boolean value indicating whether the provided app is a local charm."""
221
+ if self.charm.app.name == app_name:
222
+ return self.get_deployment_prefix() == LOCAL_BUILT_CHARM_PREFIX
223
+
224
+ try:
225
+ for relation_name in self.relations_to_check:
226
+ for rel in self.charm.model.relations[relation_name]:
227
+ if rel.app.name == app_name:
228
+ return rel.data[rel.app][DEPLOYMENT_TYPE] == LOCAL_BUILT_CHARM_PREFIX
229
+ except KeyError:
230
+ pass
231
+
232
+ raise NoVersionError(f"Expected {app_name} to have version info.")
233
+
234
+
235
+ class CrossAppVersionCheckerError(Exception):
236
+ """Parent class for errors raised in CrossAppVersionChecker class."""
237
+
238
+
239
+ class RelationInvalidError(CrossAppVersionCheckerError):
240
+ """Raised if a relation is not in the provided set of relations to check."""
241
+
242
+
243
+ class NoVersionError(CrossAppVersionCheckerError):
244
+ """Raised if an application does not contain any version information."""
@@ -0,0 +1,15 @@
1
+ Metadata-Version: 2.1
2
+ Name: data-platform-helpers
3
+ Version: 0.1.0
4
+ Summary:
5
+ Author: Mia Altieri
6
+ Author-email: mgaltier200@gmail.com
7
+ Requires-Python: >=3.10,<4.0
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.10
10
+ Classifier: Programming Language :: Python :: 3.11
11
+ Classifier: Programming Language :: Python :: 3.12
12
+ Requires-Dist: ops (>=2.14.0,<3.0.0)
13
+ Description-Content-Type: text/markdown
14
+
15
+
@@ -0,0 +1,5 @@
1
+ data_platform_helpers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ data_platform_helpers/version_check.py,sha256=qKUBq_o6pn2Bi9a4ZylwQm2U0Ygr-S8kGLZpMn5NzBc,9815
3
+ data_platform_helpers-0.1.0.dist-info/METADATA,sha256=ldZUKhFu73Qudw1QarCRWvtCvnxsvOy1nhRSNUB-F_s,440
4
+ data_platform_helpers-0.1.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
5
+ data_platform_helpers-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: poetry-core 1.9.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any