@@ -124,12 +124,14 @@ def _on_certificates_removed(self, event: CertificatesRemovedEvent):
124124
125125# Increment this PATCH version before using `charmcraft publish-lib` or reset
126126# to 0 if you are raising the major API version
127- LIBPATCH = 12
127+ LIBPATCH = 13
128128
129129logger = logging .getLogger (__name__ )
130130
131131PYDEPS = ["pydantic" ]
132132
133+ IS_PYDANTIC_V1 = int (pydantic .version .VERSION .split ("." )[0 ]) < 2
134+
133135
134136class TLSCertificatesError (Exception ):
135137 """Base class for custom errors raised by this library."""
@@ -139,10 +141,13 @@ class DataValidationError(TLSCertificatesError):
139141 """Raised when data validation fails."""
140142
141143
142- if int (pydantic .version .VERSION .split ("." )[0 ]) < 2 :
144+ class DatabagModel (pydantic .BaseModel ):
145+ """Base databag model.
146+
147+ Supports both pydantic v1 and v2.
148+ """
143149
144- class DatabagModel (pydantic .BaseModel ): # type: ignore
145- """Base databag model."""
150+ if IS_PYDANTIC_V1 :
146151
147152 class Config :
148153 """Pydantic config."""
@@ -155,122 +160,117 @@ class Config:
155160
156161 _NEST_UNDER = None
157162
158- @classmethod
159- def load (cls , databag : MutableMapping ):
160- """Load this model from a Juju databag."""
161- if cls ._NEST_UNDER :
162- return cls .parse_obj (json .loads (databag [cls ._NEST_UNDER ]))
163-
164- try :
165- data = {
166- k : json .loads (v )
167- for k , v in databag .items ()
168- # Don't attempt to parse model-external values
169- if k in {f .alias for f in cls .__fields__ .values ()}
170- }
171- except json .JSONDecodeError as e :
172- msg = f"invalid databag contents: expecting json. { databag } "
173- logger .error (msg )
174- raise DataValidationError (msg ) from e
175-
176- try :
177- return cls .parse_raw (json .dumps (data )) # type: ignore
178- except pydantic .ValidationError as e :
179- msg = f"failed to validate databag: { databag } "
180- logger .debug (msg , exc_info = True )
181- raise DataValidationError (msg ) from e
182-
183- def dump (self , databag : Optional [MutableMapping ] = None , clear : bool = True ):
184- """Write the contents of this model to Juju databag.
185-
186- :param databag: the databag to write the data to.
187- :param clear: ensure the databag is cleared before writing it.
188- """
189- if clear and databag :
190- databag .clear ()
191-
192- if databag is None :
193- databag = {}
194-
195- if self ._NEST_UNDER :
196- databag [self ._NEST_UNDER ] = self .json (by_alias = True , exclude_defaults = False )
197- return databag
198-
199- dct = json .loads (self .json (by_alias = True , exclude_defaults = False ))
200- databag .update ({k : json .dumps (v ) for k , v in dct .items ()})
163+ model_config = pydantic .ConfigDict (
164+ # tolerate additional keys in databag
165+ extra = "ignore" ,
166+ # Allow instantiating this class by field name (instead of forcing alias).
167+ populate_by_name = True ,
168+ # Custom config key: whether to nest the whole datastructure (as json)
169+ # under a field or spread it out at the toplevel.
170+ _NEST_UNDER = None ,
171+ ) # type: ignore
172+ """Pydantic config."""
173+
174+ @classmethod
175+ def load (cls , databag : MutableMapping ):
176+ """Load this model from a Juju databag."""
177+ if IS_PYDANTIC_V1 :
178+ return cls ._load_v1 (databag )
179+ nest_under = cls .model_config .get ("_NEST_UNDER" )
180+ if nest_under :
181+ return cls .model_validate (json .loads (databag [nest_under ]))
182+
183+ try :
184+ data = {
185+ k : json .loads (v )
186+ for k , v in databag .items ()
187+ # Don't attempt to parse model-external values
188+ if k in {(f .alias or n ) for n , f in cls .model_fields .items ()}
189+ }
190+ except json .JSONDecodeError as e :
191+ msg = f"invalid databag contents: expecting json. { databag } "
192+ logger .error (msg )
193+ raise DataValidationError (msg ) from e
194+
195+ try :
196+ return cls .model_validate_json (json .dumps (data ))
197+ except pydantic .ValidationError as e :
198+ msg = f"failed to validate databag: { databag } "
199+ logger .debug (msg , exc_info = True )
200+ raise DataValidationError (msg ) from e
201+
202+ @classmethod
203+ def _load_v1 (cls , databag : MutableMapping ):
204+ """Load implementation for pydantic v1."""
205+ if cls ._NEST_UNDER :
206+ return cls .parse_obj (json .loads (databag [cls ._NEST_UNDER ]))
207+
208+ try :
209+ data = {
210+ k : json .loads (v )
211+ for k , v in databag .items ()
212+ # Don't attempt to parse model-external values
213+ if k in {f .alias for f in cls .__fields__ .values ()}
214+ }
215+ except json .JSONDecodeError as e :
216+ msg = f"invalid databag contents: expecting json. { databag } "
217+ logger .error (msg )
218+ raise DataValidationError (msg ) from e
219+
220+ try :
221+ return cls .parse_raw (json .dumps (data )) # type: ignore
222+ except pydantic .ValidationError as e :
223+ msg = f"failed to validate databag: { databag } "
224+ logger .debug (msg , exc_info = True )
225+ raise DataValidationError (msg ) from e
226+
227+ def dump (self , databag : Optional [MutableMapping ] = None , clear : bool = True ):
228+ """Write the contents of this model to Juju databag.
201229
230+ Args:
231+ databag: The databag to write to.
232+ clear: Whether to clear the databag before writing.
233+
234+ Returns:
235+ MutableMapping: The databag.
236+ """
237+ if IS_PYDANTIC_V1 :
238+ return self ._dump_v1 (databag , clear )
239+ if clear and databag :
240+ databag .clear ()
241+
242+ if databag is None :
243+ databag = {}
244+ nest_under = self .model_config .get ("_NEST_UNDER" )
245+ if nest_under :
246+ databag [nest_under ] = self .model_dump_json (
247+ by_alias = True ,
248+ # skip keys whose values are default
249+ exclude_defaults = True ,
250+ )
202251 return databag
203252
204- else :
205-
206- class DatabagModel (pydantic .BaseModel ):
207- """Base databag model."""
208-
209- model_config = pydantic .ConfigDict (
210- # tolerate additional keys in databag
211- extra = "ignore" ,
212- # Allow instantiating this class by field name (instead of forcing alias).
213- populate_by_name = True ,
214- # Custom config key: whether to nest the whole datastructure (as json)
215- # under a field or spread it out at the toplevel.
216- _NEST_UNDER = None ,
217- ) # type: ignore
218- """Pydantic config."""
219-
220- @classmethod
221- def load (cls , databag : MutableMapping ):
222- """Load this model from a Juju databag."""
223- nest_under = cls .model_config .get ("_NEST_UNDER" )
224- if nest_under :
225- return cls .model_validate (json .loads (databag [nest_under ]))
226-
227- try :
228- data = {
229- k : json .loads (v )
230- for k , v in databag .items ()
231- # Don't attempt to parse model-external values
232- if k in {(f .alias or n ) for n , f in cls .model_fields .items ()}
233- }
234- except json .JSONDecodeError as e :
235- msg = f"invalid databag contents: expecting json. { databag } "
236- logger .error (msg )
237- raise DataValidationError (msg ) from e
238-
239- try :
240- return cls .model_validate_json (json .dumps (data ))
241- except pydantic .ValidationError as e :
242- msg = f"failed to validate databag: { databag } "
243- logger .debug (msg , exc_info = True )
244- raise DataValidationError (msg ) from e
245-
246- def dump (self , databag : Optional [MutableMapping ] = None , clear : bool = True ):
247- """Write the contents of this model to Juju databag.
248-
249- Args:
250- databag: The databag to write to.
251- clear: Whether to clear the databag before writing.
252-
253- Returns:
254- MutableMapping: The databag.
255- """
256- if clear and databag :
257- databag .clear ()
258-
259- if databag is None :
260- databag = {}
261- nest_under = self .model_config .get ("_NEST_UNDER" )
262- if nest_under :
263- databag [nest_under ] = self .model_dump_json (
264- by_alias = True ,
265- # skip keys whose values are default
266- exclude_defaults = True ,
267- )
268- return databag
253+ dct = self .model_dump (mode = "json" , by_alias = True , exclude_defaults = False )
254+ databag .update ({k : json .dumps (v ) for k , v in dct .items ()})
255+ return databag
256+
257+ def _dump_v1 (self , databag : Optional [MutableMapping ] = None , clear : bool = True ):
258+ """Dump implementation for pydantic v1."""
259+ if clear and databag :
260+ databag .clear ()
269261
270- dct = self .model_dump (mode = "json" , by_alias = True , exclude_defaults = False )
271- databag .update ({k : json .dumps (v ) for k , v in dct .items ()})
262+ if databag is None :
263+ databag = {}
264+
265+ if self ._NEST_UNDER :
266+ databag [self ._NEST_UNDER ] = self .json (by_alias = True , exclude_defaults = False )
272267 return databag
273268
269+ dct = json .loads (self .json (by_alias = True , exclude_defaults = False ))
270+ databag .update ({k : json .dumps (v ) for k , v in dct .items ()})
271+
272+ return databag
273+
274274
275275class ProviderApplicationData (DatabagModel ):
276276 """Provider App databag model."""
@@ -337,10 +337,16 @@ def add_certificates(self, certificates: Set[str], relation_id: Optional[int] =
337337 return
338338 relations = self ._get_active_relations (relation_id )
339339 if not relations :
340- logger .warning (
341- "At least 1 matching relation ID not found with the relation name '%s'" ,
342- self .relationship_name ,
343- )
340+ if relation_id is not None :
341+ logger .warning (
342+ "At least 1 matching relation ID not found with the relation name '%s'" ,
343+ self .relationship_name ,
344+ )
345+ else :
346+ logger .debug (
347+ "No active relations found with the relation name '%s'" ,
348+ self .relationship_name ,
349+ )
344350 return
345351
346352 for relation in relations :
@@ -364,10 +370,16 @@ def remove_all_certificates(self, relation_id: Optional[int] = None) -> None:
364370 return
365371 relations = self ._get_active_relations (relation_id )
366372 if not relations :
367- logger .warning (
368- "At least 1 matching relation ID not found with the relation name '%s'" ,
369- self .relationship_name ,
370- )
373+ if relation_id is not None :
374+ logger .warning (
375+ "At least 1 matching relation ID not found with the relation name '%s'" ,
376+ self .relationship_name ,
377+ )
378+ else :
379+ logger .debug (
380+ "No active relations found with the relation name '%s'" ,
381+ self .relationship_name ,
382+ )
371383 return
372384
373385 for relation in relations :
@@ -394,10 +406,16 @@ def remove_certificate(
394406 return
395407 relations = self ._get_active_relations (relation_id )
396408 if not relations :
397- logger .warning (
398- "At least 1 matching relation ID not found with the relation name '%s'" ,
399- self .relationship_name ,
400- )
409+ if relation_id is not None :
410+ logger .warning (
411+ "At least 1 matching relation ID not found with the relation name '%s'" ,
412+ self .relationship_name ,
413+ )
414+ else :
415+ logger .debug (
416+ "No active relations found with the relation name '%s'" ,
417+ self .relationship_name ,
418+ )
401419 return
402420
403421 for relation in relations :
0 commit comments