contentgrid-application-client 0.2.6__tar.gz → 0.2.7__tar.gz

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 (30) hide show
  1. {contentgrid_application_client-0.2.6/src/contentgrid_application_client.egg-info → contentgrid_application_client-0.2.7}/PKG-INFO +2 -2
  2. {contentgrid_application_client-0.2.6 → contentgrid_application_client-0.2.7}/src/contentgrid_application_client/application.py +86 -16
  3. {contentgrid_application_client-0.2.6 → contentgrid_application_client-0.2.7/src/contentgrid_application_client.egg-info}/PKG-INFO +2 -2
  4. contentgrid_application_client-0.2.7/src/contentgrid_application_client.egg-info/requires.txt +1 -0
  5. {contentgrid_application_client-0.2.6 → contentgrid_application_client-0.2.7}/tests/test_openapi.py +2 -3
  6. {contentgrid_application_client-0.2.6 → contentgrid_application_client-0.2.7}/tests/test_search.py +85 -1
  7. contentgrid_application_client-0.2.6/src/contentgrid_application_client.egg-info/requires.txt +0 -1
  8. {contentgrid_application_client-0.2.6 → contentgrid_application_client-0.2.7}/LICENSE +0 -0
  9. {contentgrid_application_client-0.2.6 → contentgrid_application_client-0.2.7}/README.md +0 -0
  10. {contentgrid_application_client-0.2.6 → contentgrid_application_client-0.2.7}/pyproject.toml +0 -0
  11. {contentgrid_application_client-0.2.6 → contentgrid_application_client-0.2.7}/pytest.ini +0 -0
  12. {contentgrid_application_client-0.2.6 → contentgrid_application_client-0.2.7}/requirements.txt +0 -0
  13. {contentgrid_application_client-0.2.6 → contentgrid_application_client-0.2.7}/setup.cfg +0 -0
  14. {contentgrid_application_client-0.2.6 → contentgrid_application_client-0.2.7}/setup.py +0 -0
  15. {contentgrid_application_client-0.2.6 → contentgrid_application_client-0.2.7}/src/contentgrid_application_client/__init__.py +0 -0
  16. {contentgrid_application_client-0.2.6 → contentgrid_application_client-0.2.7}/src/contentgrid_application_client/py.typed +0 -0
  17. {contentgrid_application_client-0.2.6 → contentgrid_application_client-0.2.7}/src/contentgrid_application_client.egg-info/SOURCES.txt +0 -0
  18. {contentgrid_application_client-0.2.6 → contentgrid_application_client-0.2.7}/src/contentgrid_application_client.egg-info/dependency_links.txt +0 -0
  19. {contentgrid_application_client-0.2.6 → contentgrid_application_client-0.2.7}/src/contentgrid_application_client.egg-info/top_level.txt +0 -0
  20. {contentgrid_application_client-0.2.6 → contentgrid_application_client-0.2.7}/tests/example-docs/document.jpg +0 -0
  21. {contentgrid_application_client-0.2.6 → contentgrid_application_client-0.2.7}/tests/example-docs/resume.pdf +0 -0
  22. {contentgrid_application_client-0.2.6 → contentgrid_application_client-0.2.7}/tests/fixtures.py +0 -0
  23. {contentgrid_application_client-0.2.6 → contentgrid_application_client-0.2.7}/tests/test_collection.py +0 -0
  24. {contentgrid_application_client-0.2.6 → contentgrid_application_client-0.2.7}/tests/test_content.py +0 -0
  25. {contentgrid_application_client-0.2.6 → contentgrid_application_client-0.2.7}/tests/test_curies.py +0 -0
  26. {contentgrid_application_client-0.2.6 → contentgrid_application_client-0.2.7}/tests/test_entity_creation.py +0 -0
  27. {contentgrid_application_client-0.2.6 → contentgrid_application_client-0.2.7}/tests/test_hal_form_type_check.py +0 -0
  28. {contentgrid_application_client-0.2.6 → contentgrid_application_client-0.2.7}/tests/test_profile.py +0 -0
  29. {contentgrid_application_client-0.2.6 → contentgrid_application_client-0.2.7}/tests/test_prompting.py +0 -0
  30. {contentgrid_application_client-0.2.6 → contentgrid_application_client-0.2.7}/tests/test_relations.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: contentgrid-application-client
3
- Version: 0.2.6
3
+ Version: 0.2.7
4
4
  Summary: Python Client for interacting with ContentGrid Applications
5
5
  Author-email: Ranec Belpaire <ranec.belpaire@xenit.eu>
6
6
  License: Copyright 2024 Xenit Solutions
@@ -22,7 +22,7 @@ Classifier: Programming Language :: Python :: 3
22
22
  Requires-Python: >=3.5
23
23
  Description-Content-Type: text/markdown
24
24
  License-File: LICENSE
25
- Requires-Dist: contentgrid-hal-client==0.2.6
25
+ Requires-Dist: contentgrid-hal-client==0.2.7
26
26
  Dynamic: license-file
27
27
  Dynamic: requires-dist
28
28
 
@@ -1,3 +1,4 @@
1
+ from abc import ABC, abstractmethod
1
2
  from enum import Enum
2
3
  from typing import Any, Generator, List, Optional, Self, Sequence, Union, cast
3
4
  from contentgrid_hal_client.hal_forms import HALFormsPropertyType, HALFormsTemplate
@@ -58,6 +59,7 @@ class InteractiveApplicationResponse(InteractiveHALResponse):
58
59
  class SearchType(Enum):
59
60
  EXACT_MATCH = "exact-match"
60
61
  PREFIX_MATCH = "prefix-match"
62
+ FULL_TEXT_MATCH = "full-text"
61
63
  LESS_THAN = "less-than"
62
64
  LESS_THAN_OR_EQUAL = "less-than-or-equal"
63
65
  GREATER_THAN = "greater-than"
@@ -478,6 +480,7 @@ class AttributeProfile(InteractiveApplicationResponse):
478
480
  # Initialize private attributes for search options
479
481
  self.__prefix_search_option: Optional[SearchOption] = None
480
482
  self.__exact_search_option: Optional[SearchOption] = None
483
+ self.__full_text_search_option: Optional[SearchOption] = None
481
484
  self.__less_than_search_option: Optional[SearchOption] = None
482
485
  self.__less_than_or_equal_search_option: Optional[SearchOption] = None
483
486
  self.__greater_than_search_option: Optional[SearchOption] = None
@@ -554,6 +557,12 @@ class AttributeProfile(InteractiveApplicationResponse):
554
557
  if self.__prefix_search_option is None:
555
558
  self.__prefix_search_option = self.get_search_option(search_type=SearchType.PREFIX_MATCH)
556
559
  return self.__prefix_search_option
560
+
561
+ @property
562
+ def _full_text_search_option(self) -> Optional[SearchOption]:
563
+ if self.__full_text_search_option is None:
564
+ self.__full_text_search_option = self.get_search_option(search_type=SearchType.FULL_TEXT_MATCH)
565
+ return self.__full_text_search_option
557
566
 
558
567
  @property
559
568
  def _exact_search_option(self) -> Optional[SearchOption]:
@@ -699,32 +708,94 @@ class PageInfo:
699
708
  _search_form_relation_delimiter = "."
700
709
 
701
710
 
702
- class AttributeMatchSearch:
703
- def __init__(self, attribute_name: str, value: Union[float | int | date | datetime | bool | str], prefer_prefix: bool = True, ignore_errors: bool = False) -> None:
711
+ class AttributeSearch(ABC):
712
+ """Abstract base class for attribute search implementations."""
713
+
714
+ def __init__(self, attribute_name: str, ignore_errors: bool = False) -> None:
704
715
  self.attribute_name: str = attribute_name
705
- self.value: Union[float | int | date | datetime | bool | str] = value
706
- self.prefer_prefix: bool = prefer_prefix
707
716
  self.ignore_errors: bool = ignore_errors
717
+
718
+ @abstractmethod
719
+ def get_search_params(self, attribute_profile: AttributeProfile) -> List[SearchParam]:
720
+ """Convert this search specification into search parameters.
721
+
722
+ Args:
723
+ attribute_profile: The attribute profile to validate against and extract search options from.
724
+
725
+ Returns:
726
+ List of SearchParam objects representing the search criteria.
727
+ """
728
+ pass
729
+
730
+
731
+ class AttributeMatchSearch(AttributeSearch):
732
+ def __init__(
733
+ self,
734
+ attribute_name: str,
735
+ value: Union[float | int | date | datetime | bool | str],
736
+ use_prefix: bool = False,
737
+ use_full_text: bool = False,
738
+ use_exact: bool = False,
739
+ ignore_errors: bool = False
740
+ ) -> None:
741
+ super().__init__(attribute_name, ignore_errors)
742
+ self.value: Union[float | int | date | datetime | bool | str] = value
743
+ self.use_prefix: bool = use_prefix
744
+ self.use_full_text: bool = use_full_text
745
+ self.use_exact: bool = use_exact
708
746
 
709
747
  def get_search_params(self, attribute_profile: AttributeProfile) -> List[SearchParam]:
710
748
  search_params = []
711
749
  if self.value is not None:
712
750
  if not self.ignore_errors:
713
- if not attribute_profile.has_search_capability():
751
+ if (
752
+ not attribute_profile.has_search_capability()
753
+ or (not attribute_profile._exact_search_option
754
+ and not attribute_profile._prefix_search_option
755
+ and not attribute_profile._full_text_search_option)
756
+ ):
714
757
  raise BadRequest(f"Attribute {self.attribute_name} does not support search. Available options: {attribute_profile.search_options}")
715
- if not attribute_profile._exact_search_option and not attribute_profile._prefix_search_option:
716
- raise BadRequest(f"Attribute {self.attribute_name} does not support exact search. Available options: {attribute_profile.search_options}")
717
758
 
718
- # Exact or prefix match if possible
719
- if attribute_profile._prefix_search_option and (self.prefer_prefix or not attribute_profile._exact_search_option):
720
- search_params.append(SearchParam(value=self.value, option=attribute_profile._prefix_search_option))
721
- elif attribute_profile._exact_search_option:
722
- search_params.append(SearchParam(value=self.value, option=attribute_profile._exact_search_option))
759
+ # Extract available search options
760
+ full_text_option = attribute_profile._full_text_search_option
761
+ prefix_option = attribute_profile._prefix_search_option
762
+ exact_option = attribute_profile._exact_search_option
763
+
764
+ selected_option = None
765
+
766
+ # Check if any specific search type is explicitly requested
767
+ any_use_flag_set = self.use_prefix or self.use_full_text or self.use_exact
768
+
769
+ if any_use_flag_set:
770
+ # Use explicitly requested search types (in priority order)
771
+ if self.use_full_text and full_text_option:
772
+ selected_option = full_text_option
773
+ elif self.use_prefix and prefix_option:
774
+ selected_option = prefix_option
775
+ elif self.use_exact and exact_option:
776
+ selected_option = exact_option
777
+ else:
778
+ # No specific type requested, use default priority:
779
+ # 1. Full-text match (if available)
780
+ # 2. Prefix match (if available)
781
+ # 3. Exact match (if available)
782
+ # 4. Any available option
783
+ if full_text_option:
784
+ selected_option = full_text_option
785
+ elif prefix_option:
786
+ selected_option = prefix_option
787
+ elif exact_option:
788
+ selected_option = exact_option
789
+ elif attribute_profile.has_search_capability():
790
+ selected_option = attribute_profile.search_options[0]
791
+
792
+ if selected_option:
793
+ search_params.append(SearchParam(value=self.value, option=selected_option))
723
794
 
724
795
  return search_params
725
796
 
726
797
 
727
- class AttributeRangeSearch:
798
+ class AttributeRangeSearch(AttributeSearch):
728
799
  def __init__(
729
800
  self,
730
801
  attribute_name: str,
@@ -733,7 +804,7 @@ class AttributeRangeSearch:
733
804
  include_boundaries: bool = True,
734
805
  ignore_errors: bool = False,
735
806
  ) -> None:
736
- self.attribute_name: str = attribute_name
807
+ super().__init__(attribute_name, ignore_errors)
737
808
 
738
809
  self.min_value: Optional[Union[float | int | str]]
739
810
  if isinstance(min_value, datetime):
@@ -752,7 +823,6 @@ class AttributeRangeSearch:
752
823
  self.max_value = max_value
753
824
 
754
825
  self.include_boundaries: bool = include_boundaries
755
- self.ignore_errors: bool = ignore_errors
756
826
 
757
827
  def get_search_params(self, attribute_profile: AttributeProfile) -> List[SearchParam]:
758
828
  search_params = []
@@ -925,7 +995,7 @@ class ContentGridApplicationClient(HALFormsClient):
925
995
  return self.follow_link(profile_link, infer_type=EntityProfile, params=params)
926
996
 
927
997
  def fetch_openapi_yaml(self) -> tuple[str, bytes]:
928
- res = self.get("/openapi.yml")
998
+ res = self.get("/openapi.yml", headers={"Accept": "application/yaml"})
929
999
  self._validate_non_json_response(res)
930
1000
  return ("openapi.yml", res.content)
931
1001
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: contentgrid-application-client
3
- Version: 0.2.6
3
+ Version: 0.2.7
4
4
  Summary: Python Client for interacting with ContentGrid Applications
5
5
  Author-email: Ranec Belpaire <ranec.belpaire@xenit.eu>
6
6
  License: Copyright 2024 Xenit Solutions
@@ -22,7 +22,7 @@ Classifier: Programming Language :: Python :: 3
22
22
  Requires-Python: >=3.5
23
23
  Description-Content-Type: text/markdown
24
24
  License-File: LICENSE
25
- Requires-Dist: contentgrid-hal-client==0.2.6
25
+ Requires-Dist: contentgrid-hal-client==0.2.7
26
26
  Dynamic: license-file
27
27
  Dynamic: requires-dist
28
28
 
@@ -1,7 +1,6 @@
1
1
  from contentgrid_application_client import ContentGridApplicationClient
2
- from fixtures import auth_manager, cg_client # noqa: F401
3
2
  import os
4
-
3
+ from fixtures import auth_manager, cg_client # noqa: F401
5
4
 
6
5
  def test_fetch_openapi_spec(cg_client: ContentGridApplicationClient):
7
6
  filename, file = cg_client.fetch_openapi_yaml()
@@ -9,4 +8,4 @@ def test_fetch_openapi_spec(cg_client: ContentGridApplicationClient):
9
8
  output_path = os.path.join("output", filename)
10
9
  with open(output_path, 'wb') as f:
11
10
  f.write(file)
12
- assert os.path.exists(output_path)
11
+ assert os.path.exists(output_path)
@@ -258,4 +258,88 @@ def test_conversion_of_datetime():
258
258
  date_range = AttributeRangeSearch("datetime", min_value=datetime(2025,4,24), max_value=datetime(2026, 4, 24))
259
259
  assert isinstance(date_range.min_value, str)
260
260
  assert isinstance(date_range.max_value, str)
261
- assert is_valid_datetime_format(date_range.min_value)
261
+ assert is_valid_datetime_format(date_range.min_value)
262
+
263
+
264
+ def test_full_text_preference_over_prefix(cg_client):
265
+ """Test that full-text search is used when explicitly requested."""
266
+ entity_name = "skill"
267
+
268
+ # Create a search with full-text explicitly requested
269
+ match_search = AttributeMatchSearch(
270
+ attribute_name="name",
271
+ value="machine",
272
+ use_full_text=True
273
+ )
274
+
275
+ entity_search = EntitySearch(
276
+ entity_name=entity_name,
277
+ attribute_searches=[match_search]
278
+ )
279
+
280
+ # Get the entity profile to verify the search option selected
281
+ entity_profile = cg_client.get_entity_profile(entity_name)
282
+ attribute_profile = entity_profile.get_attribute_profile("name")
283
+
284
+ # Get the search params to verify full-text option is used
285
+ search_params = match_search.get_search_params(attribute_profile)
286
+
287
+ assert len(search_params) > 0
288
+ # If full-text is available, it should be selected
289
+ if attribute_profile._full_text_search_option:
290
+ assert search_params[0].option.type.value == "full-text"
291
+
292
+ results = cg_client.execute_entity_search(entity_search=entity_search, nb_results=3)
293
+ assert results is not None
294
+
295
+
296
+ def test_full_text_search_default_behavior(cg_client):
297
+ """Test default behavior which prioritizes full-text, then prefix, then exact match."""
298
+ entity_name = "skill"
299
+
300
+ # Default behavior (no explicit use_* flags) uses priority: full-text > prefix > exact
301
+ match_search = AttributeMatchSearch(
302
+ attribute_name="name",
303
+ value="learning"
304
+ )
305
+
306
+ entity_search = EntitySearch(
307
+ entity_name=entity_name,
308
+ attribute_searches=[match_search]
309
+ )
310
+
311
+ results = cg_client.execute_entity_search(entity_search=entity_search, nb_results=5)
312
+ assert results is not None
313
+ assert len(results) >= 0
314
+
315
+
316
+ def test_exact_match_fallback_when_preferences_disabled(cg_client):
317
+ """Test that exact match is used when explicitly requested."""
318
+ entity_name = "skill"
319
+
320
+ # Create a search with exact match explicitly requested
321
+ match_search = AttributeMatchSearch(
322
+ attribute_name="name",
323
+ value="Python",
324
+ use_exact=True
325
+ )
326
+
327
+ entity_search = EntitySearch(
328
+ entity_name=entity_name,
329
+ attribute_searches=[match_search]
330
+ )
331
+
332
+ # Get the entity profile to verify the search option selected
333
+ entity_profile = cg_client.get_entity_profile(entity_name)
334
+ attribute_profile = entity_profile.get_attribute_profile("name")
335
+
336
+ # Get the search params to verify exact match option is used when explicitly requested
337
+ search_params = match_search.get_search_params(attribute_profile)
338
+
339
+ assert len(search_params) > 0
340
+ # Should use exact match (or case-insensitive match) as explicitly requested
341
+ if attribute_profile._exact_search_option:
342
+ assert search_params[0].option.type.value in ["exact-match", "case-insensitive-match"]
343
+
344
+ results = cg_client.execute_entity_search(entity_search=entity_search, nb_results=3)
345
+ assert results is not None