esgvoc 0.1.2__py3-none-any.whl → 0.2.1__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.

Potentially problematic release.


This version of esgvoc might be problematic. Click here for more details.

esgvoc/api/projects.py CHANGED
@@ -8,7 +8,8 @@ from esgvoc.api._utils import (get_universe_session, instantiate_pydantic_term,
8
8
  instantiate_pydantic_terms)
9
9
  from esgvoc.api.report import (ProjectTermError, UniverseTermError,
10
10
  ValidationError, ValidationReport)
11
- from esgvoc.api.search import MatchingTerm, SearchSettings, create_str_comparison_expression
11
+ from esgvoc.api.search import MatchingTerm, SearchSettings, _create_str_comparison_expression
12
+ from esgvoc.api.project_specs import ProjectSpecs
12
13
  from esgvoc.core.db.connection import DBConnection
13
14
  from esgvoc.core.db.models.mixins import TermKind
14
15
  from esgvoc.core.db.models.project import Collection, Project, PTerm
@@ -17,6 +18,24 @@ from pydantic import BaseModel
17
18
  from sqlmodel import Session, and_, select
18
19
 
19
20
 
21
+ # [OPTIMIZATION]
22
+ _VALID_TERM_IN_COLLECTION_CACHE: dict[str, list[MatchingTerm]] = dict()
23
+ _VALID_VALUE_AGAINST_GIVEN_TERM_CACHE: dict[str, list[ValidationError]] = dict()
24
+
25
+
26
+ def get_project_specs(project_id: str) -> ProjectSpecs:
27
+ project_specs = find_project(project_id)
28
+ if not project_specs:
29
+ msg = f'Unable to find project {project_id}'
30
+ raise ValueError(msg)
31
+ try:
32
+ result = ProjectSpecs(**project_specs)
33
+ except Exception as e:
34
+ msg = f'Unable to read specs in project {project_id}'
35
+ raise RuntimeError(msg) from e
36
+ return result
37
+
38
+
20
39
  def _get_project_connection(project_id: str) -> DBConnection|None:
21
40
  return service.state_service.projects[project_id].db_connection
22
41
 
@@ -31,7 +50,7 @@ def _get_project_session_with_exception(project_id: str) -> Session:
31
50
  def _resolve_term(term_composite_part: dict,
32
51
  universe_session: Session,
33
52
  project_session: Session) -> UTerm|PTerm:
34
- '''First find the term in the universe than in the current project'''
53
+ # First find the term in the universe than in the current project
35
54
  term_id = term_composite_part[esgvoc.core.constants.TERM_ID_JSON_KEY]
36
55
  term_type = term_composite_part[esgvoc.core.constants.TERM_TYPE_JSON_KEY]
37
56
  uterms = universe._find_terms_in_data_descriptor(data_descriptor_id=term_type,
@@ -156,9 +175,11 @@ def _valid_value_for_term_composite(value: str,
156
175
 
157
176
  def _create_term_error(value: str, term: UTerm|PTerm) -> ValidationError:
158
177
  if isinstance(term, UTerm):
159
- return UniverseTermError(value, term)
178
+ return UniverseTermError(value=value, term=term.specs, term_kind=term.kind,
179
+ data_descriptor_id=term.data_descriptor.id)
160
180
  else:
161
- return ProjectTermError(value, term)
181
+ return ProjectTermError(value=value, term=term.specs, term_kind=term.kind,
182
+ collection_id=term.collection.id)
162
183
 
163
184
 
164
185
  def _valid_value(value: str,
@@ -184,13 +205,11 @@ def _valid_value(value: str,
184
205
  return result
185
206
 
186
207
 
187
- def _check_and_strip_value(value: str) -> str:
188
- if not value:
208
+ def _check_value(value: str) -> str:
209
+ if not value or value.isspace():
189
210
  raise ValueError('value should be set')
190
- if result:= value.strip():
191
- return result
192
211
  else:
193
- raise ValueError('value should not be empty')
212
+ return value
194
213
 
195
214
 
196
215
  def _search_plain_term_and_valid_value(value: str,
@@ -223,26 +242,33 @@ def _valid_value_against_all_terms_of_collection(value: str,
223
242
 
224
243
 
225
244
  def _valid_value_against_given_term(value: str,
245
+ project_id: str,
226
246
  collection_id: str,
227
247
  term_id: str,
228
248
  universe_session: Session,
229
249
  project_session: Session)\
230
250
  -> list[ValidationError]:
231
- try:
232
- terms = _find_terms_in_collection(collection_id,
233
- term_id,
234
- project_session,
235
- None)
236
- if terms:
237
- term = terms[0]
238
- result = _valid_value(value, term, universe_session, project_session)
239
- else:
240
- raise ValueError(f'unable to find term {term_id} ' +
241
- f'in collection {collection_id}')
242
- except Exception as e:
243
- msg = f'unable to valid term {term_id} ' +\
244
- f'in collection {collection_id}'
245
- raise RuntimeError(msg) from e
251
+ # [OPTIMIZATION]
252
+ key = value + project_id + collection_id + term_id
253
+ if key in _VALID_VALUE_AGAINST_GIVEN_TERM_CACHE:
254
+ result = _VALID_VALUE_AGAINST_GIVEN_TERM_CACHE[key]
255
+ else:
256
+ try:
257
+ terms = _find_terms_in_collection(collection_id,
258
+ term_id,
259
+ project_session,
260
+ None)
261
+ if terms:
262
+ term = terms[0]
263
+ result = _valid_value(value, term, universe_session, project_session)
264
+ else:
265
+ raise ValueError(f'unable to find term {term_id} ' +
266
+ f'in collection {collection_id}')
267
+ except Exception as e:
268
+ msg = f'unable to valid term {term_id} ' +\
269
+ f'in collection {collection_id}'
270
+ raise RuntimeError(msg) from e
271
+ _VALID_VALUE_AGAINST_GIVEN_TERM_CACHE[key] = result
246
272
  return result
247
273
 
248
274
 
@@ -256,14 +282,14 @@ def valid_term(value: str,
256
282
  a report that contains the possible errors.
257
283
 
258
284
  Behavior based on the nature of the term:
259
- - plain term: the function try to match the value on the drs_name field.
260
- - term pattern: the function try to match the value on the pattern field (regex).
261
- - term composite:
262
- - if the composite has got a separator, the function splits the value according to the
263
- separator of the term then it try to match every part of the composite
264
- with every split of the value.
265
- - if the composite hasn't got a separator, the function aggregates the parts of the composite
266
- so as to compare it as a regex to the value.
285
+ - plain term: the function try to match the value on the drs_name field.
286
+ - term pattern: the function try to match the value on the pattern field (regex).
287
+ - term composite:
288
+ - if the composite has got a separator, the function splits the value according to the\
289
+ separator of the term then it try to match every part of the composite\
290
+ with every split of the value.
291
+ - if the composite hasn't got a separator, the function aggregates the parts of the \
292
+ composite so as to compare it as a regex to the value.
267
293
 
268
294
  If any of the provided ids (`project_id`, `collection_id` or `term_id`) is not found,
269
295
  the function raises a ValueError.
@@ -280,12 +306,12 @@ def valid_term(value: str,
280
306
  :rtype: ValidationReport
281
307
  :raises ValueError: If any of the provided ids is not found
282
308
  """
283
- value = _check_and_strip_value(value)
309
+ value = _check_value(value)
284
310
  with get_universe_session() as universe_session, \
285
311
  _get_project_session_with_exception(project_id) as project_session:
286
- errors = _valid_value_against_given_term(value, collection_id, term_id,
312
+ errors = _valid_value_against_given_term(value, project_id, collection_id, term_id,
287
313
  universe_session, project_session)
288
- return ValidationReport(value, errors)
314
+ return ValidationReport(expression=value, errors=errors)
289
315
 
290
316
 
291
317
  def _valid_term_in_collection(value: str,
@@ -294,28 +320,38 @@ def _valid_term_in_collection(value: str,
294
320
  universe_session: Session,
295
321
  project_session: Session) \
296
322
  -> list[MatchingTerm]:
297
- value = _check_and_strip_value(value)
298
- result = list()
299
- collections = _find_collections_in_project(collection_id,
300
- project_session,
301
- None)
302
- if collections:
303
- collection = collections[0]
304
- match collection.term_kind:
305
- case TermKind.PLAIN:
306
- term_id_found = _search_plain_term_and_valid_value(value, collection_id,
307
- project_session)
308
- if term_id_found:
309
- result.append(MatchingTerm(project_id, collection_id, term_id_found))
310
- case _:
311
- term_ids_found = _valid_value_against_all_terms_of_collection(value, collection,
312
- universe_session,
313
- project_session)
314
- for term_id_found in term_ids_found:
315
- result.append(MatchingTerm(project_id, collection_id, term_id_found))
323
+ # [OPTIMIZATION]
324
+ key = value + project_id + collection_id
325
+ if key in _VALID_TERM_IN_COLLECTION_CACHE:
326
+ result = _VALID_TERM_IN_COLLECTION_CACHE[key]
316
327
  else:
317
- msg = f'unable to find collection {collection_id}'
318
- raise ValueError(msg)
328
+ value = _check_value(value)
329
+ result = list()
330
+ collections = _find_collections_in_project(collection_id,
331
+ project_session,
332
+ None)
333
+ if collections:
334
+ collection = collections[0]
335
+ match collection.term_kind:
336
+ case TermKind.PLAIN:
337
+ term_id_found = _search_plain_term_and_valid_value(value, collection_id,
338
+ project_session)
339
+ if term_id_found:
340
+ result.append(MatchingTerm(project_id=project_id,
341
+ collection_id=collection_id,
342
+ term_id=term_id_found))
343
+ case _:
344
+ term_ids_found = _valid_value_against_all_terms_of_collection(value, collection,
345
+ universe_session,
346
+ project_session)
347
+ for term_id_found in term_ids_found:
348
+ result.append(MatchingTerm(project_id=project_id,
349
+ collection_id=collection_id,
350
+ term_id=term_id_found))
351
+ else:
352
+ msg = f'unable to find collection {collection_id}'
353
+ raise ValueError(msg)
354
+ _VALID_TERM_IN_COLLECTION_CACHE[key] = result
319
355
  return result
320
356
 
321
357
 
@@ -328,14 +364,14 @@ def valid_term_in_collection(value: str,
328
364
  returns the terms that the value matches.
329
365
 
330
366
  Behavior based on the nature of the term:
331
- - plain term: the function try to match the value on the drs_name field.
332
- - term pattern: the function try to match the value on the pattern field (regex).
333
- - term composite:
334
- - if the composite has got a separator, the function splits the value according to the
335
- separator of the term then it try to match every part of the composite
336
- with every split of the value.
337
- - if the composite hasn't got a separator, the function aggregates the parts of the composite
338
- so as to compare it as a regex to the value.
367
+ - plain term: the function try to match the value on the drs_name field.
368
+ - term pattern: the function try to match the value on the pattern field (regex).
369
+ - term composite:
370
+ - if the composite has got a separator, the function splits the value according to the \
371
+ separator of the term then it try to match every part of the composite \
372
+ with every split of the value.
373
+ - if the composite hasn't got a separator, the function aggregates the parts of the \
374
+ composite so as to compare it as a regex to the value.
339
375
 
340
376
  If any of the provided ids (`project_id` or `collection_id`) is not found,
341
377
  the function raises a ValueError.
@@ -374,14 +410,14 @@ def valid_term_in_project(value: str, project_id: str) -> list[MatchingTerm]:
374
410
  returns the terms that the value matches.
375
411
 
376
412
  Behavior based on the nature of the term:
377
- - plain term: the function try to match the value on the drs_name field.
378
- - term pattern: the function try to match the value on the pattern field (regex).
379
- - term composite:
380
- - if the composite has got a separator, the function splits the value according to the
381
- separator of the term then it try to match every part of the composite
382
- with every split of the value.
383
- - if the composite hasn't got a separator, the function aggregates the parts of the composite
384
- so as to compare it as a regex to the value.
413
+ - plain term: the function try to match the value on the drs_name field.
414
+ - term pattern: the function try to match the value on the pattern field (regex).
415
+ - term composite:
416
+ - if the composite has got a separator, the function splits the value according to the \
417
+ separator of the term then it try to match every part of the composite \
418
+ with every split of the value.
419
+ - if the composite hasn't got a separator, the function aggregates the parts of the \
420
+ composite so as to compare it as a regex to the value.
385
421
 
386
422
  If the `project_id` is not found, the function raises a ValueError.
387
423
 
@@ -404,14 +440,14 @@ def valid_term_in_all_projects(value: str) -> list[MatchingTerm]:
404
440
  returns the terms that the value matches.
405
441
 
406
442
  Behavior based on the nature of the term:
407
- - plain term: the function try to match the value on the drs_name field.
408
- - term pattern: the function try to match the value on the pattern field (regex).
409
- - term composite:
410
- - if the composite has got a separator, the function splits the value according to the
411
- separator of the term then it try to match every part of the composite
412
- with every split of the value.
413
- - if the composite hasn't got a separator, the function aggregates the parts of the composite
414
- so as to compare it as a regex to the value.
443
+ - plain term: the function try to match the value on the drs_name field.
444
+ - term pattern: the function try to match the value on the pattern field (regex).
445
+ - term composite:
446
+ - if the composite has got a separator, the function splits the value according to the \
447
+ separator of the term then it try to match every part of the composite \
448
+ with every split of the value.
449
+ - if the composite hasn't got a separator, the function aggregates the parts of the \
450
+ composite so as to compare it as a regex to the value.
415
451
 
416
452
  :param value: A value to be validated
417
453
  :type value: str
@@ -431,10 +467,10 @@ def _find_terms_in_collection(collection_id: str,
431
467
  term_id: str,
432
468
  session: Session,
433
469
  settings: SearchSettings|None = None) -> Sequence[PTerm]:
434
- """Settings only apply on the term_id comparison."""
435
- where_expression = create_str_comparison_expression(field=PTerm.id,
436
- value=term_id,
437
- settings=settings)
470
+ # Settings only apply on the term_id comparison.
471
+ where_expression = _create_str_comparison_expression(field=PTerm.id,
472
+ value=term_id,
473
+ settings=settings)
438
474
  statement = select(PTerm).join(Collection).where(Collection.id==collection_id,
439
475
  where_expression)
440
476
  results = session.exec(statement)
@@ -458,9 +494,9 @@ def find_terms_in_collection(project_id:str,
458
494
  the function returns an empty list.
459
495
 
460
496
  Behavior based on search type:
461
- - `EXACT` and absence of `settings`: returns zero or one Pydantic term instance in the list.
462
- - `REGEX`, `LIKE`, `STARTS_WITH` and `ENDS_WITH`: returns zero, one or more
463
- Pydantic term instances in the list.
497
+ - `EXACT` and absence of `settings`: returns zero or one Pydantic term instance in the list.
498
+ - `REGEX`, `LIKE`, `STARTS_WITH` and `ENDS_WITH`: returns zero, one or more \
499
+ Pydantic term instances in the list.
464
500
 
465
501
  :param project_id: A project id
466
502
  :type project_id: str
@@ -486,10 +522,10 @@ def _find_terms_from_data_descriptor_in_project(data_descriptor_id: str,
486
522
  session: Session,
487
523
  settings: SearchSettings|None = None) \
488
524
  -> Sequence[PTerm]:
489
- """Settings only apply on the term_id comparison."""
490
- where_expression = create_str_comparison_expression(field=PTerm.id,
491
- value=term_id,
492
- settings=settings)
525
+ # Settings only apply on the term_id comparison.
526
+ where_expression = _create_str_comparison_expression(field=PTerm.id,
527
+ value=term_id,
528
+ settings=settings)
493
529
  statement = select(PTerm).join(Collection).where(Collection.data_descriptor_id==data_descriptor_id,
494
530
  where_expression)
495
531
  results = session.exec(statement)
@@ -514,10 +550,10 @@ def find_terms_from_data_descriptor_in_project(project_id: str,
514
550
  the function returns an empty list.
515
551
 
516
552
  Behavior based on search type:
517
- - `EXACT` and absence of `settings`: returns zero or one Pydantic term instance and
518
- collection id in the list.
519
- - `REGEX`, `LIKE`, `STARTS_WITH` and `ENDS_WITH`: returns zero, one or more
520
- Pydantic term instances and collection ids in the list.
553
+ - `EXACT` and absence of `settings`: returns zero or one Pydantic term instance and \
554
+ collection id in the list.
555
+ - `REGEX`, `LIKE`, `STARTS_WITH` and `ENDS_WITH`: returns zero, one or more \
556
+ Pydantic term instances and collection ids in the list.
521
557
 
522
558
  :param project_id: A project id
523
559
  :type project_id: str
@@ -527,7 +563,7 @@ def find_terms_from_data_descriptor_in_project(project_id: str,
527
563
  :type term_id: str
528
564
  :param settings: The search settings
529
565
  :type settings: SearchSettings|None
530
- :returns: A list of tuple of Pydantic term instances and related collection ids.
566
+ :returns: A list of tuple of Pydantic term instances and related collection ids. \
531
567
  Returns an empty list if no matches are found.
532
568
  :rtype: list[tuple[BaseModel, str]]
533
569
  """
@@ -548,7 +584,7 @@ def find_terms_from_data_descriptor_in_project(project_id: str,
548
584
  def find_terms_from_data_descriptor_in_all_projects(data_descriptor_id: str,
549
585
  term_id: str,
550
586
  settings: SearchSettings|None = None) \
551
- -> list[tuple[BaseModel, str]]:
587
+ -> list[tuple[list[tuple[BaseModel, str]], str]]:
552
588
  """
553
589
  Finds one or more terms in all projects which are instances of the given data descriptor
554
590
  in the universe, based on the specified search settings, in the given collection of a project.
@@ -561,10 +597,10 @@ def find_terms_from_data_descriptor_in_all_projects(data_descriptor_id: str,
561
597
  the function returns an empty list.
562
598
 
563
599
  Behavior based on search type:
564
- - `EXACT` and absence of `settings`: returns zero or one Pydantic term instance and
565
- collection id in the list.
566
- - `REGEX`, `LIKE`, `STARTS_WITH` and `ENDS_WITH`: returns zero, one or more
567
- Pydantic term instances and collection ids in the list.
600
+ - `EXACT` and absence of `settings`: returns zero or one Pydantic term instance and \
601
+ collection id in the list.
602
+ - `REGEX`, `LIKE`, `STARTS_WITH` and `ENDS_WITH`: returns zero, one or more \
603
+ Pydantic term instances and collection ids in the list.
568
604
 
569
605
  :param data_descriptor_id: A data descriptor
570
606
  :type data_descriptor_id: str
@@ -572,26 +608,27 @@ def find_terms_from_data_descriptor_in_all_projects(data_descriptor_id: str,
572
608
  :type term_id: str
573
609
  :param settings: The search settings
574
610
  :type settings: SearchSettings|None
575
- :returns: A list of tuple of Pydantic term instances and related collection ids.
611
+ :returns: A list of tuple of matching terms with their collection id, per project. \
576
612
  Returns an empty list if no matches are found.
577
- :rtype: list[tuple[BaseModel, str]]
613
+ :rtype: list[tuple[list[tuple[BaseModel, str]], str]]
578
614
  """
579
615
  project_ids = get_all_projects()
580
- result = list()
616
+ result: list[tuple[list[tuple[BaseModel, str]], str]] = list()
581
617
  for project_id in project_ids:
582
- result.extend(find_terms_from_data_descriptor_in_project(project_id,
583
- data_descriptor_id,
584
- term_id,
585
- settings))
618
+ matching_terms = find_terms_from_data_descriptor_in_project(project_id,
619
+ data_descriptor_id,
620
+ term_id,
621
+ settings)
622
+ result.append((matching_terms, project_id))
586
623
  return result
587
624
 
588
625
 
589
626
  def _find_terms_in_project(term_id: str,
590
627
  session: Session,
591
628
  settings: SearchSettings|None) -> Sequence[PTerm]:
592
- where_expression = create_str_comparison_expression(field=PTerm.id,
593
- value=term_id,
594
- settings=settings)
629
+ where_expression = _create_str_comparison_expression(field=PTerm.id,
630
+ value=term_id,
631
+ settings=settings)
595
632
  statement = select(PTerm).where(where_expression)
596
633
  results = session.exec(statement).all()
597
634
  return results
@@ -668,8 +705,7 @@ def get_all_terms_in_collection(project_id: str,
668
705
  :type project_id: str
669
706
  :param collection_id: A collection id
670
707
  :type collection_id: str
671
- :returns: a list of Pydantic term instances.
672
- Returns an empty list if no matches are found.
708
+ :returns: a list of Pydantic term instances. Returns an empty list if no matches are found.
673
709
  :rtype: list[BaseModel]
674
710
  """
675
711
  result = list()
@@ -688,9 +724,9 @@ def _find_collections_in_project(collection_id: str,
688
724
  session: Session,
689
725
  settings: SearchSettings|None) \
690
726
  -> Sequence[Collection]:
691
- where_exp = create_str_comparison_expression(field=Collection.id,
692
- value=collection_id,
693
- settings=settings)
727
+ where_exp = _create_str_comparison_expression(field=Collection.id,
728
+ value=collection_id,
729
+ settings=settings)
694
730
  statement = select(Collection).where(where_exp)
695
731
  results = session.exec(statement)
696
732
  result = results.all()
@@ -713,9 +749,9 @@ def find_collections_in_project(project_id: str,
713
749
  an empty list.
714
750
 
715
751
  Behavior based on search type:
716
- - `EXACT` and absence of `settings`: returns zero or one collection context in the list.
717
- - `REGEX`, `LIKE`, `STARTS_WITH` and `ENDS_WITH`: returns zero, one or more
718
- collection contexts in the list.
752
+ - `EXACT` and absence of `settings`: returns zero or one collection context in the list.
753
+ - `REGEX`, `LIKE`, `STARTS_WITH` and `ENDS_WITH`: returns zero, one or more \
754
+ collection contexts in the list.
719
755
 
720
756
  :param project_id: A project id
721
757
  :type project_id: str
@@ -723,8 +759,7 @@ def find_collections_in_project(project_id: str,
723
759
  :type collection_id: str
724
760
  :param settings: The search settings
725
761
  :type settings: SearchSettings|None
726
- :returns: A list of collection contexts.
727
- Returns an empty list if no matches are found.
762
+ :returns: A list of collection contexts. Returns an empty list if no matches are found.
728
763
  :rtype: list[dict]
729
764
  """
730
765
  result = list()
@@ -753,8 +788,7 @@ def get_all_collections_in_project(project_id: str) -> list[str]:
753
788
 
754
789
  :param project_id: A project id
755
790
  :type project_id: str
756
- :returns: A list of collection ids.
757
- Returns an empty list if no matches are found.
791
+ :returns: A list of collection ids. Returns an empty list if no matches are found.
758
792
  :rtype: list[str]
759
793
  """
760
794
  result = list()
@@ -782,8 +816,7 @@ def get_all_terms_in_project(project_id: str) -> list[BaseModel]:
782
816
 
783
817
  :param project_id: A project id
784
818
  :type project_id: str
785
- :returns: A list of Pydantic term instances.
786
- Returns an empty list if no matches are found.
819
+ :returns: A list of Pydantic term instances. Returns an empty list if no matches are found.
787
820
  :rtype: list[BaseModel]
788
821
  """
789
822
  result = list()
@@ -819,8 +852,7 @@ def find_project(project_id: str) -> dict|None:
819
852
 
820
853
  :param project_id: A project id to be found
821
854
  :type project_id: str
822
- :returns: The specs of the project found.
823
- Returns `None` if no matches are found.
855
+ :returns: The specs of the project found. Returns `None` if no matches are found.
824
856
  :rtype: dict|None
825
857
  """
826
858
  result = None
@@ -848,7 +880,5 @@ if __name__ == "__main__":
848
880
  print('OK')
849
881
  else:
850
882
  print(vr)
851
- from esgvoc.api import BasicValidationErrorVisitor
852
- visitor = BasicValidationErrorVisitor()
853
883
  for error in vr.errors:
854
- print(error.accept(visitor))
884
+ print(error)
esgvoc/api/report.py CHANGED
@@ -1,80 +1,106 @@
1
+ from pydantic import BaseModel, computed_field
1
2
  from abc import ABC, abstractmethod
2
- from typing import Any
3
+ from typing import Any, Protocol
3
4
 
4
5
  import esgvoc.core.constants as api_settings
5
6
  from esgvoc.core.db.models.mixins import TermKind
6
- from esgvoc.core.db.models.project import PTerm
7
- from esgvoc.core.db.models.universe import UTerm
8
7
 
9
8
 
10
- class ValidationErrorVisitor(ABC):
11
- @abstractmethod
9
+ class ValidationErrorVisitor(Protocol):
10
+ """
11
+ Specifications for a term validation error visitor.
12
+ """
12
13
  def visit_universe_term_error(self, error: "UniverseTermError") -> Any:
14
+ """Visit a universe term error."""
13
15
  pass
14
16
 
15
- @abstractmethod
16
17
  def visit_project_term_error(self, error: "ProjectTermError") -> Any:
18
+ """Visit a project term error."""
17
19
  pass
18
20
 
19
21
 
20
- class BasicValidationErrorVisitor(ValidationErrorVisitor):
21
- def visit_universe_term_error(self, error: "UniverseTermError") -> Any:
22
- term_id = error.term[api_settings.TERM_ID_JSON_KEY]
23
- result = f"The term {term_id} from the data descriptor {error.data_descriptor_id} "+\
24
- f"does not validate the given value '{error.value}'"
25
- return result
26
-
27
- def visit_project_term_error(self, error: "ProjectTermError") -> Any:
28
- term_id = error.term[api_settings.TERM_ID_JSON_KEY]
29
- result = f"The term {term_id} from the collection {error.collection_id} "+\
30
- f"does not validate the given value '{error.value}'"
31
- return result
32
-
33
-
34
- class ValidationError(ABC):
35
- def __init__(self,
36
- value: str):
37
- self.value: str = value
22
+ class ValidationError(BaseModel, ABC):
23
+ """
24
+ Generic class for the term validation error.
25
+ """
26
+ value: str
27
+ """The given value that is invalid."""
28
+ term: dict
29
+ """JSON specification of the term."""
30
+ term_kind: TermKind
31
+ """The kind of term."""
38
32
 
39
33
  @abstractmethod
40
34
  def accept(self, visitor: ValidationErrorVisitor) -> Any:
35
+ """
36
+ Accept a validation error visitor.
37
+
38
+ :param visitor: The validation error visitor.
39
+ :type visitor: ValidationErrorVisitor
40
+ :return: Depending on the visitor.
41
+ :rtype: Any
42
+ """
41
43
  pass
42
44
 
43
45
  class UniverseTermError(ValidationError):
44
- def __init__(self,
45
- value: str,
46
- term: UTerm):
47
- super().__init__(value)
48
- self.term: dict = term.specs
49
- self.term_kind: TermKind = term.kind
50
- self.data_descriptor_id: str = term.data_descriptor.id
46
+ """
47
+ A validation error on a term from the universe.
48
+ """
49
+
50
+ data_descriptor_id: str
51
+ """The data descriptor that the term belongs."""
51
52
 
52
53
  def accept(self, visitor: ValidationErrorVisitor) -> Any:
53
54
  return visitor.visit_universe_term_error(self)
55
+
56
+ def __str__(self) -> str:
57
+ term_id = self.term[api_settings.TERM_ID_JSON_KEY]
58
+ result = f"The term {term_id} from the data descriptor {self.data_descriptor_id} "+\
59
+ f"does not validate the given value '{self.value}'"
60
+ return result
61
+ def __repr__(self) -> str:
62
+ return self.__str__()
54
63
 
55
64
 
56
65
  class ProjectTermError(ValidationError):
57
- def __init__(self,
58
- value: str,
59
- term: PTerm):
60
- super().__init__(value)
61
- self.term: dict = term.specs
62
- self.term_kind: TermKind = term.kind
63
- self.collection_id: str = term.collection.id
66
+ """
67
+ A validation error on a term from a project.
68
+ """
69
+
70
+ collection_id: str
71
+ """The collection id that the term belongs"""
64
72
 
65
73
  def accept(self, visitor: ValidationErrorVisitor) -> Any:
66
74
  return visitor.visit_project_term_error(self)
67
-
68
-
69
- class ValidationReport:
70
- def __init__(self,
71
- given_expression: str,
72
- errors: list[ValidationError]):
73
- self.expression: str = given_expression
74
- self.errors: list[ValidationError] = errors
75
- self.nb_errors = len(self.errors) if self.errors else 0
76
- self.validated: bool = False if errors else True
77
- self.message = f"'{self.expression}' has {self.nb_errors} error(s)"
75
+
76
+ def __str__(self) -> str:
77
+ term_id = self.term[api_settings.TERM_ID_JSON_KEY]
78
+ result = f"The term {term_id} from the collection {self.collection_id} "+\
79
+ f"does not validate the given value '{self.value}'"
80
+ return result
81
+ def __repr__(self) -> str:
82
+ return self.__str__()
83
+
84
+
85
+ class ValidationReport(BaseModel):
86
+ """
87
+ Term validation report.
88
+ """
89
+ expression: str
90
+ """The given expression."""
91
+ errors: list[ValidationError]
92
+ """The validation errors."""
93
+ @computed_field # type: ignore
94
+ @property
95
+ def nb_errors(self) -> int:
96
+ """The number of validation errors."""
97
+ return len(self.errors) if self.errors else 0
98
+ @computed_field # type: ignore
99
+ @property
100
+ def validated(self) -> bool:
101
+ """The expression is validated or not."""
102
+ return False if self.errors else True
103
+
78
104
 
79
105
  def __len__(self) -> int:
80
106
  return self.nb_errors
@@ -82,5 +108,7 @@ class ValidationReport:
82
108
  def __bool__(self) -> bool:
83
109
  return self.validated
84
110
 
111
+ def __str__(self) -> str:
112
+ return f"'{self.expression}' has {self.nb_errors} error(s)"
85
113
  def __repr__(self) -> str:
86
- return self.message
114
+ return self.__str__()