contentgrid-application-client 0.2.3__tar.gz → 0.2.5__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 (32) hide show
  1. {contentgrid_application_client-0.2.3/src/contentgrid_application_client.egg-info → contentgrid_application_client-0.2.5}/PKG-INFO +2 -2
  2. {contentgrid_application_client-0.2.3 → contentgrid_application_client-0.2.5}/src/contentgrid_application_client/application.py +143 -23
  3. {contentgrid_application_client-0.2.3 → contentgrid_application_client-0.2.5/src/contentgrid_application_client.egg-info}/PKG-INFO +2 -2
  4. contentgrid_application_client-0.2.5/src/contentgrid_application_client.egg-info/requires.txt +1 -0
  5. {contentgrid_application_client-0.2.3 → contentgrid_application_client-0.2.5}/tests/fixtures.py +2 -1
  6. {contentgrid_application_client-0.2.3 → contentgrid_application_client-0.2.5}/tests/test_content.py +4 -2
  7. contentgrid_application_client-0.2.5/tests/test_profile.py +285 -0
  8. contentgrid_application_client-0.2.5/tests/test_relations.py +315 -0
  9. contentgrid_application_client-0.2.3/src/contentgrid_application_client.egg-info/requires.txt +0 -1
  10. contentgrid_application_client-0.2.3/tests/test_profile.py +0 -102
  11. contentgrid_application_client-0.2.3/tests/test_relations.py +0 -92
  12. {contentgrid_application_client-0.2.3 → contentgrid_application_client-0.2.5}/LICENSE +0 -0
  13. {contentgrid_application_client-0.2.3 → contentgrid_application_client-0.2.5}/README.md +0 -0
  14. {contentgrid_application_client-0.2.3 → contentgrid_application_client-0.2.5}/pyproject.toml +0 -0
  15. {contentgrid_application_client-0.2.3 → contentgrid_application_client-0.2.5}/pytest.ini +0 -0
  16. {contentgrid_application_client-0.2.3 → contentgrid_application_client-0.2.5}/requirements.txt +0 -0
  17. {contentgrid_application_client-0.2.3 → contentgrid_application_client-0.2.5}/setup.cfg +0 -0
  18. {contentgrid_application_client-0.2.3 → contentgrid_application_client-0.2.5}/setup.py +0 -0
  19. {contentgrid_application_client-0.2.3 → contentgrid_application_client-0.2.5}/src/contentgrid_application_client/__init__.py +0 -0
  20. {contentgrid_application_client-0.2.3 → contentgrid_application_client-0.2.5}/src/contentgrid_application_client/py.typed +0 -0
  21. {contentgrid_application_client-0.2.3 → contentgrid_application_client-0.2.5}/src/contentgrid_application_client.egg-info/SOURCES.txt +0 -0
  22. {contentgrid_application_client-0.2.3 → contentgrid_application_client-0.2.5}/src/contentgrid_application_client.egg-info/dependency_links.txt +0 -0
  23. {contentgrid_application_client-0.2.3 → contentgrid_application_client-0.2.5}/src/contentgrid_application_client.egg-info/top_level.txt +0 -0
  24. {contentgrid_application_client-0.2.3 → contentgrid_application_client-0.2.5}/tests/example-docs/document.jpg +0 -0
  25. {contentgrid_application_client-0.2.3 → contentgrid_application_client-0.2.5}/tests/example-docs/resume.pdf +0 -0
  26. {contentgrid_application_client-0.2.3 → contentgrid_application_client-0.2.5}/tests/test_collection.py +0 -0
  27. {contentgrid_application_client-0.2.3 → contentgrid_application_client-0.2.5}/tests/test_curies.py +0 -0
  28. {contentgrid_application_client-0.2.3 → contentgrid_application_client-0.2.5}/tests/test_entity_creation.py +0 -0
  29. {contentgrid_application_client-0.2.3 → contentgrid_application_client-0.2.5}/tests/test_hal_form_type_check.py +0 -0
  30. {contentgrid_application_client-0.2.3 → contentgrid_application_client-0.2.5}/tests/test_openapi.py +0 -0
  31. {contentgrid_application_client-0.2.3 → contentgrid_application_client-0.2.5}/tests/test_prompting.py +0 -0
  32. {contentgrid_application_client-0.2.3 → contentgrid_application_client-0.2.5}/tests/test_search.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: contentgrid-application-client
3
- Version: 0.2.3
3
+ Version: 0.2.5
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.3
25
+ Requires-Dist: contentgrid-hal-client==0.2.5
26
26
  Dynamic: license-file
27
27
  Dynamic: requires-dist
28
28
 
@@ -1,10 +1,11 @@
1
1
  from enum import Enum
2
2
  from typing import Any, Generator, List, Optional, Self, Sequence, Union, cast
3
- from contentgrid_hal_client.hal_forms import HALFormsTemplate
3
+ from contentgrid_hal_client.hal_forms import HALFormsPropertyType, HALFormsTemplate
4
4
  import requests
5
5
  import os
6
6
  import logging
7
7
  import mimetypes
8
+ import json
8
9
 
9
10
  from contentgrid_hal_client.exceptions import BadRequest, IncorrectAttributeType, MissingRequiredAttribute, NotFound, MissingHALTemplate
10
11
  from contentgrid_hal_client.hal import CurieRegistry, HALFormsClient, HALLink, HALResponse, InteractiveHALResponse
@@ -131,12 +132,12 @@ class EntityCollection(InteractiveApplicationResponse):
131
132
  return cast(HALLink, self.get_link("profile"))
132
133
 
133
134
  def get_entity_profile(self) -> "EntityProfile":
134
- return self.client.follow_link(self.get_entity_profile_link(), infer_type=EntityProfile)
135
+ return self.client.load_or_fetch_entity_profile(self.get_entity_profile_link())
135
136
 
136
137
  def create_entity(self, attributes: dict, attribute_validation=True) -> "EntityObject":
137
138
  self.client._transform_hal_links_to_uris(attributes=attributes)
138
139
  if attribute_validation:
139
- entity_profile = self.client.follow_link(self.get_entity_profile_link(), infer_type=EntityProfile)
140
+ entity_profile = self.client.load_or_fetch_entity_profile(self.get_entity_profile_link())
140
141
  self.client._validate_form_payload(template=entity_profile.get_template("create-form"), payload=attributes)
141
142
  response = self.client.post(self.get_self_link().uri, json=attributes, headers={"Content-Type": "application/json"})
142
143
  data = self.client._validate_json_response(response)
@@ -251,16 +252,23 @@ class EntityObject(InteractiveApplicationResponse):
251
252
  return entity_profile
252
253
  raise NotFound(f"No entity profile describes {self_link_uri}")
253
254
 
254
- def get_relation_collection(self, relation_name: str, size: int = 20, params: dict = {}):
255
+ def get_relation_collection(self, relation_name: str, size: int = 20, params: dict = {}) -> EntityCollection:
255
256
  params = self.client._add_page_and_size_to_params(size=size, params=params)
256
257
  relation_link = self.get_relation_link(relation_name=relation_name)
257
258
  return self.client.follow_link(relation_link, infer_type=EntityCollection, params=params)
259
+
260
+ def get_related_entity(self, relation_name: str) -> "EntityObject":
261
+ relation_link = self.get_relation_link(relation_name=relation_name)
262
+ return self.client.follow_link(relation_link, infer_type=EntityObject)
263
+
264
+ def set_related_entity(self, relation_name: str, related_entity : "EntityObject") -> None:
265
+ self.put_relation(relation_name=relation_name, related_entity_links=[related_entity], prefix_client_endpoint=True)
258
266
 
259
- def put_relation(self, relation_name: str, related_entity_links: List[str | HALLink | Self]) -> None:
267
+ def put_relation(self, relation_name: str, related_entity_links: List[str | HALLink | Self], prefix_client_endpoint : bool = True) -> None:
260
268
  set_template = self.get_template(f"set-{relation_name}")
261
269
  if not set_template.contentType or "text/uri-list" not in set_template.contentType:
262
270
  logging.warning(f"Unsuported ContentType not found on set-{relation_name} was {set_template.contentType}")
263
- relation_payload = self._create_text_uri_list_payload(links=related_entity_links)
271
+ relation_payload = self._create_text_uri_list_payload(links=related_entity_links, prefix_client_endpoint=prefix_client_endpoint)
264
272
  response = self.client.get_method_from_string(set_template.method)(set_template.target, headers={"Accept": "*/*", "Content-Type": "text/uri-list"}, data=relation_payload)
265
273
  self.client._validate_non_json_response(response)
266
274
 
@@ -269,11 +277,11 @@ class EntityObject(InteractiveApplicationResponse):
269
277
  response = self.client.get_method_from_string(clear_template.method)(clear_template.target)
270
278
  self.client._validate_non_json_response(response)
271
279
 
272
- def post_relation(self, relation_name: str, related_entity_links: List[str | HALLink | Self]) -> None:
280
+ def post_relation(self, relation_name: str, related_entity_links: List[str | HALLink | Self], prefix_client_endpoint : bool = True) -> None:
273
281
  add_template = self.get_template(f"add-{relation_name}")
274
282
  if not add_template.contentType or "text/uri-list" not in add_template.contentType:
275
283
  logging.warning(f"Unsuported ContentType not found on set-{relation_name} was {add_template.contentType}")
276
- relation_payload = self._create_text_uri_list_payload(links=related_entity_links)
284
+ relation_payload = self._create_text_uri_list_payload(links=related_entity_links, prefix_client_endpoint=prefix_client_endpoint)
277
285
  response = self.client.get_method_from_string(add_template.method)(add_template.target, headers={"Accept": "*/*", "Content-Type": "text/uri-list"}, data=relation_payload)
278
286
  self.client._validate_non_json_response(response)
279
287
 
@@ -312,8 +320,8 @@ class EntityObject(InteractiveApplicationResponse):
312
320
  files.append(self.client.fetch_content_attribute(hal_content_link))
313
321
  return files
314
322
 
315
- def _create_text_uri_list_payload(self, links: Sequence[str | HALLink | HALResponse]) -> str:
316
- uri_list = []
323
+ def _create_text_uri_list_payload(self, links: Sequence[str | HALLink | HALResponse], prefix_client_endpoint : bool = True) -> str:
324
+ uri_list : List[str] = []
317
325
  for link in links:
318
326
  if isinstance(link, HALLink):
319
327
  uri_list.append(link.uri)
@@ -323,7 +331,19 @@ class EntityObject(InteractiveApplicationResponse):
323
331
  uri_list.append(link)
324
332
  else:
325
333
  raise BadRequest(f"Incorrect Link type {type(link)} in uri list payload, allowed types: HALLink, HALResponse or str")
326
- return "\n".join(uri_list)
334
+ if prefix_client_endpoint:
335
+ try:
336
+ client_endpoint = self.client.client_endpoint
337
+ formatted_list = [
338
+ client_endpoint.click(link).to_text()
339
+ for link in uri_list
340
+ ]
341
+ except ValueError as e:
342
+ logging.debug(f"Failed to prepend the client_endpoint in the text-uri-payload {e}")
343
+ formatted_list = uri_list
344
+ else:
345
+ formatted_list = uri_list
346
+ return "\n".join(formatted_list)
327
347
 
328
348
  def prompt(self, include_entity_profile: bool = True, include_relations: bool = True, include_content: bool = True) -> str:
329
349
  """ Generate a prompt string for an entity instance.
@@ -364,8 +384,8 @@ class EntityObject(InteractiveApplicationResponse):
364
384
  lines.append(f" - {content_link.name} (content attribute name): {content_link.uri}")
365
385
  return "\n".join(lines)
366
386
 
367
- class Profile(InteractiveHALResponse):
368
- def __init__(self, data: dict, client: HALFormsClient, curie_registry: CurieRegistry = None) -> None:
387
+ class Profile(InteractiveApplicationResponse):
388
+ def __init__(self, data: dict, client: "ContentGridApplicationClient", curie_registry: CurieRegistry = None) -> None:
369
389
  super().__init__(data, client, curie_registry)
370
390
 
371
391
  def get_entity_links(self) -> List[HALLink]:
@@ -373,12 +393,12 @@ class Profile(InteractiveHALResponse):
373
393
 
374
394
  def get_entity_profiles_generator(self) -> Generator["EntityProfile", None, None]:
375
395
  for entity_profile_link in self.get_entity_links():
376
- yield self.client.follow_link(entity_profile_link, infer_type=EntityProfile)
396
+ yield self.client.load_or_fetch_entity_profile(entity_profile_link)
377
397
 
378
398
  def get_entity_profile(self, singular_entity_name: str) -> "EntityProfile": # type: ignore
379
399
  for entity_profile_link in self.get_entity_links():
380
400
  if entity_profile_link.name == singular_entity_name:
381
- return self.client.follow_link(entity_profile_link, infer_type=EntityProfile)
401
+ return self.client.load_or_fetch_entity_profile(entity_profile_link)
382
402
  raise NotFound(f"Entity Profile {singular_entity_name} does not exist.")
383
403
 
384
404
  def prompt(self) -> str:
@@ -411,7 +431,7 @@ class RelationProfile(InteractiveApplicationResponse):
411
431
  def get_related_entity_profile(self) -> "EntityProfile":
412
432
  related_profile_link = self.get_related_entity_profile_link()
413
433
  if related_profile_link:
414
- return self.client.follow_link(related_profile_link, infer_type=EntityProfile)
434
+ return self.client.load_or_fetch_entity_profile(related_profile_link)
415
435
  else:
416
436
  raise NotFound(f"Related entity profile link not found on relation profile {self.name}")
417
437
 
@@ -483,7 +503,32 @@ class AttributeProfile(InteractiveApplicationResponse):
483
503
 
484
504
  def has_constrained_values(self) -> bool:
485
505
  return self.get_constraint("allowed-values") is not None
486
-
506
+
507
+ def has_allowed_values_constraint(self) -> bool:
508
+ return self.has_constrained_values()
509
+
510
+ def has_unique_constraint(self) -> bool:
511
+ return self.get_constraint("unique") is not None
512
+
513
+ def has_created_by_constraint(self) -> bool:
514
+ return self.get_constraint("created-by") is not None
515
+
516
+ def has_created_date_constraint(self) -> bool:
517
+ return self.get_constraint("created-date") is not None
518
+
519
+ def has_modified_by_constraint(self) -> bool:
520
+ return self.get_constraint("modified-by") is not None
521
+
522
+ def has_modified_date_constraint(self) -> bool:
523
+ return self.get_constraint("modified-date") is not None
524
+
525
+ @property
526
+ def is_audit_attribute(self) -> bool:
527
+ return self.has_created_by_constraint() or \
528
+ self.has_created_date_constraint() or \
529
+ self.has_modified_by_constraint() or \
530
+ self.has_modified_date_constraint()
531
+
487
532
  def get_allowed_values(self) -> Optional[List[str]]:
488
533
  if self.has_constrained_values():
489
534
  return self.get_constraint("allowed-values").values # type: ignore
@@ -809,9 +854,10 @@ class ContentGridApplicationClient(HALFormsClient):
809
854
  client_id: str = None,
810
855
  client_secret: str = None,
811
856
  token: str = None,
812
- attribute_validation: bool = True,
857
+ attribute_validation: bool = False,
813
858
  session_cookie: str = None,
814
859
  pool_maxsize: int = 10,
860
+ use_profile_cache: bool = True
815
861
  ) -> None:
816
862
  logging.info("Initializing ContentGridApplicationClient...")
817
863
  super().__init__(
@@ -825,8 +871,38 @@ class ContentGridApplicationClient(HALFormsClient):
825
871
  pool_maxsize=pool_maxsize,
826
872
  )
827
873
  self.attribute_validation = attribute_validation
828
-
874
+ self.use_profile_cache = use_profile_cache
875
+ self.profile_cache: dict[str, dict[str, Any]] = {}
876
+
877
+ # PROFILE CACHING
878
+ def load_profile_cache(self, url: str, json_data: dict | str) -> None:
879
+ """
880
+ Load JSON data into the profile cache.
881
+
882
+ Args:
883
+ url: The URL to cache the response for (relative to client_endpoint)
884
+ json_data: The JSON data to cache (as dict or JSON string)
885
+ """
886
+ if isinstance(json_data, str):
887
+ json_data = json.loads(json_data)
888
+
889
+ self.profile_cache[url] = cast(dict, json_data)
890
+ logging.info(f"Loaded JSON data into profile cache for {url}")
891
+
892
+ def clear_profile_cache(self):
893
+ logging.info(f"Clearing profile cache... clearing {len(self.profile_cache.keys())} entries")
894
+ self.profile_cache = {}
895
+
829
896
  def get_profile(self) -> Profile:
897
+ if self.use_profile_cache:
898
+ profile_cache_key = self.client_endpoint.click("/profile").normalize().to_text()
899
+ if profile_cache_key in self.profile_cache:
900
+ data = self.profile_cache[profile_cache_key]
901
+ return Profile(data, client=self)
902
+ response = self.get("/profile", headers={"Accept": "application/json"})
903
+ data = self._validate_json_response(response)
904
+ self.profile_cache[profile_cache_key] = data
905
+ return Profile(data, client=self)
830
906
  response = self.get("/profile", headers={"Accept": "application/json"})
831
907
  data = self._validate_json_response(response)
832
908
  return Profile(data, client=self)
@@ -836,6 +912,17 @@ class ContentGridApplicationClient(HALFormsClient):
836
912
 
837
913
  def get_entity_profiles(self) -> List[EntityProfile]:
838
914
  return [entity_profile for entity_profile in self.get_profile().get_entity_profiles_generator()]
915
+
916
+ def load_or_fetch_entity_profile(self, profile_link : HALLink, params: dict={}) -> EntityProfile:
917
+ if self.use_profile_cache:
918
+ if profile_link.uri in self.profile_cache:
919
+ data = self.profile_cache[profile_link.uri]
920
+ return EntityProfile(data=data, client=self)
921
+ response = self.get(profile_link.uri, params=params)
922
+ data = self._validate_json_response(response)
923
+ self.profile_cache[profile_link.uri] = data
924
+ return EntityProfile(data=data, client=self)
925
+ return self.follow_link(profile_link, infer_type=EntityProfile, params=params)
839
926
 
840
927
  def fetch_openapi_yaml(self) -> tuple[str, bytes]:
841
928
  res = self.get("/openapi.yml")
@@ -846,17 +933,19 @@ class ContentGridApplicationClient(HALFormsClient):
846
933
  params = self._add_page_and_size_to_params(size=size, params=params)
847
934
  collection_description = self.get_entity_profile(singular_entity_name=singular_entity_name)._get_collection_describes()
848
935
  if not collection_description:
849
- raise NotFound(f"Collection link nog found on {singular_entity_name} profile")
936
+ raise NotFound(f"Collection link not found on {singular_entity_name} profile")
850
937
  response = self.get(collection_description.uri, params=params)
851
938
  data = self._validate_json_response(response)
852
939
  return EntityCollection(data, client=self)
853
940
 
854
- def create_entity(self, singular_entity_name: str, attributes: dict) -> EntityObject:
941
+ def create_entity(self, singular_entity_name: str, attributes: dict, prefix_client_endpoint : bool = True) -> EntityObject:
855
942
  self._transform_hal_links_to_uris(attributes=attributes)
856
943
  entity_profile = self.get_entity_profile(singular_entity_name=singular_entity_name)
857
944
  create_template = entity_profile.get_template("create-form")
858
945
  if self.attribute_validation:
859
946
  self._validate_form_payload(template=create_template, payload=attributes)
947
+ if prefix_client_endpoint:
948
+ self._prefix_url_in_form_payload(template=create_template, payload=attributes)
860
949
  response = self.get_method_from_string(create_template.method.value)(create_template.target, json=attributes, headers={"Content-Type": "application/json"})
861
950
  data = self._validate_json_response(response)
862
951
  return EntityObject(data=data, client=self)
@@ -894,10 +983,14 @@ class ContentGridApplicationClient(HALFormsClient):
894
983
  def put_on_content_link(self, content_link: HALLink, filepath: str) -> HALLink:
895
984
  if os.path.exists(filepath):
896
985
  filename = filepath.split("/")[-1]
897
- files = {"file": (filename, open(filepath, "rb"), mimetypes.guess_type(filepath)[0])}
986
+ guessed_mimetype = mimetypes.guess_type(filepath)[0]
987
+ files = {"file": (filename, open(filepath, "rb"), guessed_mimetype)}
988
+ logging.debug(f'Predicted mimetype for filepath {filepath}: {guessed_mimetype}')
898
989
  else:
899
990
  raise BadRequest(f"Provided content not found {filepath}")
991
+ logging.debug(f"Content PUT request URL: {content_link.uri}")
900
992
  response = self.put(content_link.uri, files=files) # type: ignore
993
+ logging.debug(f"Content PUT response status: {response.status_code} {response.reason}")
901
994
  self._validate_non_json_response(response=response)
902
995
  return content_link
903
996
 
@@ -991,4 +1084,31 @@ class ContentGridApplicationClient(HALFormsClient):
991
1084
  for hal_property in template.properties:
992
1085
  if hal_property.required:
993
1086
  if hal_property.name not in payload.keys():
994
- raise MissingRequiredAttribute(f"Required attribute {property.name} not present in payload.")
1087
+ raise MissingRequiredAttribute(f"Required attribute {hal_property.name} not present in payload.")
1088
+
1089
+ def _prefix_url_in_form_payload(self, template: HALFormsTemplate, payload: dict) -> None:
1090
+ """Prefix URL properties in the payload with the client_endpoint if needed.
1091
+
1092
+ Modifies the payload in-place, prefixing any URL-type properties with the
1093
+ client_endpoint if they don't already start with it.
1094
+ """
1095
+ client_endpoint = self.client_endpoint
1096
+
1097
+ for key, value in payload.items():
1098
+ try:
1099
+ hal_property = None
1100
+ for prop in template.properties:
1101
+ if prop.name == key:
1102
+ hal_property = prop
1103
+ break
1104
+
1105
+ if hal_property and hal_property.type == HALFormsPropertyType.url:
1106
+ if isinstance(value, list):
1107
+ payload[key] = [
1108
+ client_endpoint.click(v).to_text()
1109
+ for v in value
1110
+ ]
1111
+ elif isinstance(value, str):
1112
+ payload[key] = client_endpoint.click(value).to_text()
1113
+ except Exception as e:
1114
+ logging.debug(f"Fails to handle payload {key} : {value} in prefix url formatting. Error: {e}")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: contentgrid-application-client
3
- Version: 0.2.3
3
+ Version: 0.2.5
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.3
25
+ Requires-Dist: contentgrid-hal-client==0.2.5
26
26
  Dynamic: license-file
27
27
  Dynamic: requires-dist
28
28
 
@@ -30,7 +30,8 @@ def auth_manager() -> ApplicationAuthenticationManager:
30
30
  def cg_client(auth_manager: ApplicationAuthenticationManager) -> ContentGridApplicationClient:
31
31
  return ContentGridApplicationClient(
32
32
  client_endpoint=CONTENTGRID_CLIENT_ENDPOINT,
33
- auth_manager=auth_manager
33
+ auth_manager=auth_manager,
34
+ attribute_validation=True
34
35
  )
35
36
 
36
37
  @fixture
@@ -20,7 +20,8 @@ def test_add_content(
20
20
  content_attribute_name="test",
21
21
  filepath=pdf_file_path,
22
22
  )
23
- except NotFound:
23
+ except NotFound as e:
24
+ logging.exception(e)
24
25
  has_failed = True
25
26
  assert has_failed
26
27
 
@@ -31,7 +32,8 @@ def test_add_content(
31
32
  content_attribute_name="content",
32
33
  filepath="test.pdf",
33
34
  )
34
- except BadRequest:
35
+ except BadRequest as e:
36
+ logging.exception(e)
35
37
  has_failed = True
36
38
  assert has_failed
37
39
 
@@ -0,0 +1,285 @@
1
+ from contentgrid_application_client import ContentGridApplicationClient, Profile, EntityProfile, AttributeProfile
2
+ import pytest
3
+ from contentgrid_hal_client.exceptions import NotFound
4
+ from fixtures import auth_manager, cg_client # noqa: F401
5
+
6
+ def test_get_profile_attributes(cg_client: ContentGridApplicationClient):
7
+ profile = cg_client.get_profile()
8
+ entity_links = profile.get_entity_links()
9
+ assert len(entity_links) > 0
10
+ assert all(link.name for link in entity_links)
11
+ assert all(link.uri for link in entity_links)
12
+
13
+ def test_entity_profile_details(cg_client: ContentGridApplicationClient):
14
+ profile = cg_client.get_profile()
15
+ entity_profile = cg_client.get_entity_profile(profile.get_entity_links()[0].name)
16
+
17
+ # Test basic entity profile properties
18
+ assert isinstance(entity_profile, EntityProfile)
19
+ assert entity_profile.name is not None
20
+ assert entity_profile.title is not None
21
+ assert entity_profile.description is not None
22
+
23
+ def test_attribute_profiles(cg_client: ContentGridApplicationClient):
24
+ profile = cg_client.get_profile()
25
+ entity_profile = cg_client.get_entity_profile(profile.get_entity_links()[0].name)
26
+
27
+ attribute_profiles = entity_profile.get_attribute_profiles()
28
+ if not attribute_profiles:
29
+ pytest.skip(f"Entity {entity_profile.name} has no attribute profiles to test.")
30
+
31
+ # Test first attribute profile properties
32
+ first_attribute = attribute_profiles[0]
33
+ assert isinstance(first_attribute, AttributeProfile)
34
+ assert first_attribute.name is not None
35
+ assert first_attribute.type is not None
36
+ assert isinstance(first_attribute.required, bool)
37
+ assert isinstance(first_attribute.read_only, bool)
38
+
39
+ def test_invalid_entity_profile(cg_client: ContentGridApplicationClient):
40
+ with pytest.raises(NotFound):
41
+ cg_client.get_entity_profile("non_existent_entity")
42
+
43
+ def test_nested_attributes(cg_client: ContentGridApplicationClient):
44
+ profile = cg_client.get_profile()
45
+ # Use a known entity type if available, otherwise the first one
46
+ entity_name = "anonymous-candidate" if "anonymous-candidate" in [link.name for link in profile.get_entity_links()] else profile.get_entity_links()[0].name
47
+ entity_profile = cg_client.get_entity_profile(entity_name)
48
+
49
+ found_nested = False
50
+ for attribute in entity_profile.get_attribute_profiles():
51
+ nested_attributes = attribute.get_nested_attributes()
52
+ if nested_attributes:
53
+ found_nested = True
54
+ assert all(isinstance(nested, AttributeProfile) for nested in nested_attributes)
55
+ assert all(hasattr(nested, 'name') for nested in nested_attributes)
56
+ if not found_nested:
57
+ print(f"INFO: No nested attributes found for entity {entity_name} to test.")
58
+
59
+ def test_content_attribute_detection(cg_client: ContentGridApplicationClient):
60
+ profile = cg_client.get_profile()
61
+ entity_profile = cg_client.get_entity_profile(profile.get_entity_links()[0].name)
62
+
63
+ for attribute in entity_profile.get_attribute_profiles():
64
+ if attribute.is_content_attribute():
65
+ nested_attrs = attribute.get_nested_attributes()
66
+ assert any(attr.name in ["length", "mimetype", "filename"] for attr in nested_attrs)
67
+
68
+ def test_relation_profiles(cg_client: ContentGridApplicationClient):
69
+ profile = cg_client.get_profile()
70
+ entity_profile = cg_client.get_entity_profile("anonymous-candidate")
71
+
72
+ relation_profiles = entity_profile.get_relation_profiles()
73
+ assert len(relation_profiles) >= 0
74
+
75
+ if relation_profiles:
76
+ first_relation = relation_profiles[0]
77
+ assert first_relation.name is not None
78
+ assert first_relation.title is not None
79
+ assert isinstance(first_relation.required, bool)
80
+ assert isinstance(first_relation.many_source_per_target, bool)
81
+ assert isinstance(first_relation.many_target_per_source, bool)
82
+
83
+ def test_get_specific_relation_profile(cg_client: ContentGridApplicationClient):
84
+ profile = cg_client.get_profile()
85
+ entity_profile = cg_client.get_entity_profile("anonymous-candidate")
86
+
87
+ if entity_profile.get_relation_profiles():
88
+ relation_name = entity_profile.get_relation_profiles()[0].name
89
+ relation_profile = entity_profile.get_relation_profile(relation_name)
90
+ assert relation_profile.name == relation_name
91
+
92
+ # Test related entity profile retrieval
93
+ related_entity_profile = relation_profile.get_related_entity_profile()
94
+ assert isinstance(related_entity_profile, EntityProfile)
95
+ assert related_entity_profile.name is not None
96
+
97
+ def test_invalid_relation_profile(cg_client: ContentGridApplicationClient):
98
+ profile = cg_client.get_profile()
99
+ entity_profile = cg_client.get_entity_profile(profile.get_entity_links()[0].name)
100
+
101
+ with pytest.raises(ValueError):
102
+ entity_profile.get_relation_profile("non_existent_relation")
103
+
104
+
105
+ def test_load_profile_cache_with_dict(cg_client: ContentGridApplicationClient):
106
+ """Test loading profile cache with dict data"""
107
+ test_data = {
108
+ "_links": {
109
+ "self": {"href": "/profile/test-entity"}
110
+ },
111
+ "name": "test-entity",
112
+ "description": "Test entity"
113
+ }
114
+
115
+ cg_client.load_profile_cache("/profile/test-entity", test_data)
116
+
117
+ # Verify it's in the cache
118
+ assert "/profile/test-entity" in cg_client.profile_cache
119
+ cached_data = cg_client.profile_cache["/profile/test-entity"]
120
+ assert cached_data == test_data
121
+
122
+ def test_load_profile_cache_with_json_string(cg_client: ContentGridApplicationClient):
123
+ """Test loading profile cache with JSON string"""
124
+ import json
125
+ test_data = {
126
+ "_links": {
127
+ "self": {"href": "/profile/test-entity2"}
128
+ },
129
+ "name": "test-entity2"
130
+ }
131
+ json_string = json.dumps(test_data)
132
+
133
+ cg_client.load_profile_cache("/profile/test-entity2", json_string)
134
+
135
+ # Verify it's in the cache
136
+ assert "/profile/test-entity2" in cg_client.profile_cache
137
+ cached_data = cg_client.profile_cache["/profile/test-entity2"]
138
+ assert cached_data == test_data
139
+
140
+ def test_profile_cache_hit_returns_cached_response(cg_client: ContentGridApplicationClient):
141
+ """Test that cached profile responses are returned on subsequent requests"""
142
+ # First request to populate cache
143
+ profile1 = cg_client.get_profile()
144
+ initial_cache_size = len(cg_client.profile_cache)
145
+
146
+ # Second request should use cache
147
+ profile2 = cg_client.get_profile()
148
+
149
+ # Cache size should not have increased
150
+ assert len(cg_client.profile_cache) == initial_cache_size
151
+
152
+ # Both profiles should have the same data
153
+ assert profile1.data == profile2.data
154
+
155
+ def test_profile_cache_only_affects_profile_endpoints(cg_client: ContentGridApplicationClient):
156
+ """Test that cache only affects profile endpoints"""
157
+ initial_cache_size = len(cg_client.profile_cache)
158
+
159
+ # Get profile - should be cached
160
+ cg_client.get_profile()
161
+
162
+ # Cache should have grown
163
+ assert len(cg_client.profile_cache) > initial_cache_size
164
+
165
+ # Getting entity collection should not add to profile cache
166
+ profile = cg_client.get_profile()
167
+ if profile.get_entity_links():
168
+ entity_name = profile.get_entity_links()[0].name
169
+ entity_profile = cg_client.get_entity_profile(entity_name)
170
+ cache_size_after_entity_profile = len(cg_client.profile_cache)
171
+
172
+ # Getting the entity collection should not be cached
173
+ try:
174
+ cg_client.get_entity_collection(entity_name, size=1)
175
+ except Exception:
176
+ pass # We don't care if this fails, just testing cache behavior
177
+
178
+ # Profile cache size should not change from non-profile requests
179
+ assert len(cg_client.profile_cache) >= cache_size_after_entity_profile
180
+
181
+ def test_profile_cache_caches_entity_profiles(cg_client: ContentGridApplicationClient):
182
+ """Test that entity profile requests are also cached"""
183
+ profile = cg_client.get_profile()
184
+
185
+ if not profile.get_entity_links():
186
+ pytest.skip("No entity profiles available to test caching")
187
+
188
+ entity_name = profile.get_entity_links()[0].name
189
+ initial_cache_size = len(cg_client.profile_cache)
190
+
191
+ # First request
192
+ entity_profile1 = cg_client.get_entity_profile(entity_name)
193
+ cache_size_after_first = len(cg_client.profile_cache)
194
+
195
+ # Cache should have grown (at least entity profile URL)
196
+ assert cache_size_after_first > initial_cache_size
197
+
198
+ # Second request - should hit cache
199
+ entity_profile2 = cg_client.get_entity_profile(entity_name)
200
+
201
+ # Cache size should not change
202
+ assert len(cg_client.profile_cache) == cache_size_after_first
203
+
204
+ # Data should be the same
205
+ assert entity_profile1.name == entity_profile2.name
206
+
207
+ def test_clear_empty_profile_cache(cg_client: ContentGridApplicationClient):
208
+ """Test clearing an empty cache doesn't cause issues"""
209
+ # Clear empty cache
210
+ cg_client.clear_profile_cache()
211
+
212
+ # Verify cache is still empty
213
+ assert len(cg_client.profile_cache) == 0
214
+ assert cg_client.profile_cache == {}
215
+
216
+ def test_clear_profile_cache_removes_all_entries(cg_client: ContentGridApplicationClient):
217
+ """Test that clearing cache removes all cached entries"""
218
+ # Populate cache with some data
219
+ test_data1 = {"_links": {"self": {"href": "/profile/entity1"}}, "name": "entity1"}
220
+ test_data2 = {"_links": {"self": {"href": "/profile/entity2"}}, "name": "entity2"}
221
+
222
+ cg_client.load_profile_cache("/profile/entity1", test_data1)
223
+ cg_client.load_profile_cache("/profile/entity2", test_data2)
224
+
225
+ # Verify entries are in cache
226
+ assert len(cg_client.profile_cache) >= 2
227
+
228
+ # Clear cache
229
+ cg_client.clear_profile_cache()
230
+
231
+ # Verify cache is empty
232
+ assert len(cg_client.profile_cache) == 0
233
+ assert "/profile/entity1" not in cg_client.profile_cache
234
+ assert "/profile/entity2" not in cg_client.profile_cache
235
+
236
+ def test_clear_profile_cache_allows_repopulation(cg_client: ContentGridApplicationClient):
237
+ """Test that cache works normally after being cleared"""
238
+ # Populate cache
239
+ profile1 = cg_client.get_profile()
240
+ assert len(cg_client.profile_cache) > 0
241
+
242
+ # Clear cache
243
+ cg_client.clear_profile_cache()
244
+ assert len(cg_client.profile_cache) == 0
245
+
246
+ # Repopulate cache - should work normally
247
+ profile2 = cg_client.get_profile()
248
+ assert len(cg_client.profile_cache) > 0
249
+
250
+ # Data should be consistent
251
+ assert profile1.data == profile2.data
252
+
253
+ def test_clear_profile_cache_doesnt_affect_caching_flag(cg_client: ContentGridApplicationClient):
254
+ """Test that clearing cache doesn't disable caching"""
255
+ # Check caching is enabled
256
+ assert cg_client.use_profile_cache is True
257
+
258
+ # Load and clear cache
259
+ cg_client.load_profile_cache("/profile/test", {"name": "test"})
260
+ cg_client.clear_profile_cache()
261
+
262
+ # Caching should still be enabled
263
+ assert cg_client.use_profile_cache is True
264
+
265
+ # Cache should still work
266
+ cg_client.get_profile()
267
+ assert len(cg_client.profile_cache) > 0
268
+
269
+ def test_clear_profile_cache_forces_fresh_request(cg_client: ContentGridApplicationClient):
270
+ """Test that clearing cache forces a fresh request on next call"""
271
+ # First request - populates cache
272
+ cg_client.get_profile()
273
+ cache_size_after_first = len(cg_client.profile_cache)
274
+ assert cache_size_after_first > 0
275
+
276
+ # Clear cache
277
+ cg_client.clear_profile_cache()
278
+ assert len(cg_client.profile_cache) == 0
279
+
280
+ # Next request should repopulate cache (new request made)
281
+ cg_client.get_profile()
282
+ cache_size_after_clear = len(cg_client.profile_cache)
283
+
284
+ # Cache should be populated again
285
+ assert cache_size_after_clear > 0
@@ -0,0 +1,315 @@
1
+ from contentgrid_application_client import ContentGridApplicationClient, EntityCollection
2
+ from contentgrid_application_client.application import EntityObject
3
+ from contentgrid_hal_client.exceptions import Forbidden, MissingHALTemplate, NotFound
4
+ from contentgrid_hal_client.hal import HALLink
5
+ from contentgrid_hal_client.hal_forms import HALFormsTemplate, HALFormsProperty, HALFormsPropertyType, HALFormsMethod
6
+ from fixtures import auth_manager, cg_client # noqa: F401
7
+ from unittest.mock import MagicMock
8
+ import pytest
9
+ import hyperlink
10
+ import uuid
11
+
12
+
13
+ class TestCreateTextUriListPayload:
14
+ """Unit tests for _create_text_uri_list_payload method"""
15
+
16
+ @pytest.fixture
17
+ def entity(self):
18
+ """Create an EntityObject with a mock client for testing"""
19
+ mock_client = MagicMock()
20
+ mock_client.client_endpoint = hyperlink.parse("https://api.example.com/").normalize()
21
+ entity = EntityObject.__new__(EntityObject)
22
+ entity.client = mock_client
23
+ return entity
24
+
25
+ def test_uri_list_with_relative_paths(self, entity):
26
+ """Test that relative paths are properly prefixed with client endpoint"""
27
+ result = entity._create_text_uri_list_payload(["/entities/123", "/entities/456"])
28
+
29
+ assert result == "https://api.example.com/entities/123\nhttps://api.example.com/entities/456"
30
+
31
+ def test_uri_list_with_full_urls(self, entity):
32
+ """Test that full URLs matching endpoint are not modified"""
33
+ full_url = "https://api.example.com/entities/123"
34
+ result = entity._create_text_uri_list_payload([full_url])
35
+
36
+ assert result == full_url
37
+
38
+ def test_uri_list_with_hal_link(self, entity):
39
+ """Test that HALLink objects are properly converted"""
40
+ hal_link = HALLink(uri="/entities/123")
41
+ result = entity._create_text_uri_list_payload([hal_link])
42
+
43
+ assert result == "https://api.example.com/entities/123"
44
+
45
+ def test_uri_list_with_hal_link_full_url(self, entity):
46
+ """Test that HALLink with full URL is not double-prefixed"""
47
+ hal_link = HALLink(uri="https://api.example.com/entities/123")
48
+ result = entity._create_text_uri_list_payload([hal_link])
49
+
50
+ assert result == "https://api.example.com/entities/123"
51
+
52
+ def test_uri_list_without_prefix(self, entity):
53
+ """Test that prefix_client_endpoint=False returns URIs unchanged"""
54
+ result = entity._create_text_uri_list_payload(["/entities/123"], prefix_client_endpoint=False)
55
+
56
+ assert result == "/entities/123"
57
+
58
+
59
+ class TestPrefixUrlInFormPayload:
60
+ """Unit tests for _prefix_url_in_form_payload method"""
61
+
62
+ @pytest.fixture
63
+ def client(self):
64
+ """Create a mock ContentGridApplicationClient for testing"""
65
+ mock_client = MagicMock(spec=ContentGridApplicationClient)
66
+ mock_client.client_endpoint = hyperlink.parse("https://api.example.com/").normalize()
67
+ mock_client._prefix_url_in_form_payload = ContentGridApplicationClient._prefix_url_in_form_payload.__get__(mock_client)
68
+ return mock_client
69
+
70
+ def test_prefix_url_property_with_relative_path(self, client):
71
+ """Test that URL properties with relative paths are prefixed"""
72
+ template = HALFormsTemplate(
73
+ method=HALFormsMethod.POST,
74
+ properties=[HALFormsProperty(name="link", type=HALFormsPropertyType.url)]
75
+ )
76
+ payload = {"link": "/entities/123"}
77
+
78
+ client._prefix_url_in_form_payload(template=template, payload=payload)
79
+
80
+ assert payload["link"] == "https://api.example.com/entities/123"
81
+
82
+ def test_prefix_url_property_avoids_double_slashes(self, client):
83
+ """Test that double slashes are avoided"""
84
+ template = HALFormsTemplate(
85
+ method=HALFormsMethod.POST,
86
+ properties=[HALFormsProperty(name="link", type=HALFormsPropertyType.url)]
87
+ )
88
+ payload = {"link": "/entities/123"}
89
+
90
+ client._prefix_url_in_form_payload(template=template, payload=payload)
91
+
92
+ assert "//" not in payload["link"].replace("https://", "")
93
+
94
+ def test_prefix_url_property_with_full_url(self, client):
95
+ """Test that full URLs are not double-prefixed"""
96
+ template = HALFormsTemplate(
97
+ method=HALFormsMethod.POST,
98
+ properties=[HALFormsProperty(name="link", type=HALFormsPropertyType.url)]
99
+ )
100
+ payload = {"link": "https://api.example.com/entities/123"}
101
+
102
+ client._prefix_url_in_form_payload(template=template, payload=payload)
103
+
104
+ assert payload["link"] == "https://api.example.com/entities/123"
105
+
106
+ def test_prefix_url_property_list(self, client):
107
+ """Test that URL properties in lists are prefixed"""
108
+ template = HALFormsTemplate(
109
+ method=HALFormsMethod.POST,
110
+ properties=[HALFormsProperty(name="links", type=HALFormsPropertyType.url)]
111
+ )
112
+ payload = {"links": ["/entities/123", "/entities/456"]}
113
+
114
+ client._prefix_url_in_form_payload(template=template, payload=payload)
115
+
116
+ assert payload["links"] == [
117
+ "https://api.example.com/entities/123",
118
+ "https://api.example.com/entities/456"
119
+ ]
120
+
121
+ def test_non_url_properties_unchanged(self, client):
122
+ """Test that non-URL properties are not modified"""
123
+ template = HALFormsTemplate(
124
+ method=HALFormsMethod.POST,
125
+ properties=[
126
+ HALFormsProperty(name="name", type=HALFormsPropertyType.text),
127
+ HALFormsProperty(name="link", type=HALFormsPropertyType.url)
128
+ ]
129
+ )
130
+ payload = {"name": "/some/path", "link": "/entities/123"}
131
+
132
+ client._prefix_url_in_form_payload(template=template, payload=payload)
133
+
134
+ assert payload["name"] == "/some/path"
135
+ assert payload["link"] == "https://api.example.com/entities/123"
136
+
137
+
138
+ class TestToManyRelations:
139
+ entity_name = "candidate"
140
+ relation_name = "skills"
141
+
142
+ def test_relations(self, cg_client: ContentGridApplicationClient):
143
+ entity = cg_client.create_entity(self.entity_name, {"name": "test"})
144
+ try:
145
+ # fetch related skills
146
+ related_skills = cg_client.get_entity_relation_collection(
147
+ entity.get_self_link(), self.relation_name
148
+ )
149
+
150
+ assert isinstance(related_skills, EntityCollection)
151
+
152
+ # entity just instantiated so no skills
153
+ assert len(related_skills.get_entities()) == 0
154
+
155
+ skills = cg_client.get_entity_collection(singular_entity_name="skill")
156
+
157
+ amt_skills_to_sample = 4
158
+ skills_selection = skills.get_entities()[:amt_skills_to_sample]
159
+ assert len(skills_selection) == amt_skills_to_sample
160
+
161
+ skills_selection[1] = skills_selection[1].get_self_link().uri
162
+ skills_selection[2] = skills_selection[2].get_self_link()
163
+
164
+ cg_client.post_entity_relation(
165
+ entity.get_self_link(),
166
+ relation_name=self.relation_name,
167
+ related_entity_links=skills_selection[:-1],
168
+ )
169
+
170
+ related_skills.refetch()
171
+
172
+ assert len(related_skills.get_entities()) == amt_skills_to_sample - 1
173
+
174
+ # post last skill
175
+ cg_client.post_entity_relation(
176
+ entity_link=entity.get_self_link(),
177
+ relation_name=self.relation_name,
178
+ related_entity_links=[skills_selection[-1]],
179
+ )
180
+
181
+ related_skills = cg_client.get_entity_relation_collection(
182
+ entity_link=entity.get_self_link(), relation_name=self.relation_name
183
+ )
184
+ assert len(related_skills.get_entities()) == amt_skills_to_sample
185
+ finally:
186
+ entity.delete()
187
+
188
+ def test_put_on_to_many_not_supported(self, cg_client: ContentGridApplicationClient):
189
+ entity = cg_client.create_entity(self.entity_name, {"name": "test"})
190
+ try:
191
+ skills = cg_client.get_entity_collection(singular_entity_name="skill")
192
+ skills_selection = skills.get_entities()[:2]
193
+
194
+ # PUT on to-many relations should raise Forbidden
195
+ with pytest.raises(MissingHALTemplate):
196
+ cg_client.put_entity_relation(
197
+ entity.get_self_link(),
198
+ relation_name=self.relation_name,
199
+ related_entity_links=skills_selection,
200
+ )
201
+ finally:
202
+ entity.delete()
203
+
204
+ def test_clear_relations(self, cg_client: ContentGridApplicationClient):
205
+ entity = cg_client.create_entity(self.entity_name, {"name": "test"})
206
+ try:
207
+ # no related exist yet
208
+ entity.clear_relation(relation_name=self.relation_name)
209
+
210
+ skills = cg_client.get_entity_collection(singular_entity_name="skill")
211
+ skills_selection = skills.get_entities()[:1]
212
+ cg_client.post_entity_relation(
213
+ entity.get_self_link(),
214
+ relation_name=self.relation_name,
215
+ related_entity_links=skills_selection,
216
+ )
217
+
218
+ assert len(entity.get_relation_collection(relation_name=self.relation_name).get_entities()) == 1
219
+
220
+ # clear relations again
221
+ entity.clear_relation(relation_name=self.relation_name)
222
+ finally:
223
+ entity.delete()
224
+
225
+
226
+
227
+ class TestToOneRelations:
228
+ candidate_entity_name = "candidate"
229
+ redacted_candidate_entity_name = "anonymous-candidate"
230
+ customer_company_entity_name = "customer-company"
231
+ relation_name = "redacted"
232
+ relation_name2 = "proposal"
233
+
234
+ @pytest.fixture
235
+ def entity(self):
236
+ """Create an EntityObject with a mock client for testing"""
237
+ mock_client = MagicMock()
238
+ entity = EntityObject.__new__(EntityObject)
239
+ entity.client = mock_client
240
+ return entity
241
+
242
+ def test_get_related_entity_follows_relation_link(self, entity):
243
+ """Test that get_related_entity follows the expected relation link"""
244
+ relation_link = HALLink(uri="/entities/456", name="redacted")
245
+ expected_related_entity = MagicMock(spec=EntityObject)
246
+
247
+ entity.get_relation_link = MagicMock(return_value=relation_link)
248
+ entity.client.follow_link.return_value = expected_related_entity
249
+
250
+ result = entity.get_related_entity("redacted")
251
+
252
+ assert result == expected_related_entity
253
+ entity.get_relation_link.assert_called_once_with(relation_name="redacted")
254
+ entity.client.follow_link.assert_called_once_with(relation_link, infer_type=EntityObject)
255
+
256
+ def test_get_related_entity_raises_when_relation_not_found(self, entity):
257
+ """Test that relation lookup errors are propagated"""
258
+ entity.get_relation_link = MagicMock(side_effect=NotFound("missing relation"))
259
+
260
+ with pytest.raises(NotFound):
261
+ entity.get_related_entity("missing-relation")
262
+
263
+ entity.client.follow_link.assert_not_called()
264
+
265
+ def test_get_related_entity_raises_when_related_entity_not_found(self, entity):
266
+ """Test that follow_link NotFound errors are propagated"""
267
+ relation_link = HALLink(uri="/entities/456", name="redacted")
268
+ entity.get_relation_link = MagicMock(return_value=relation_link)
269
+ entity.client.follow_link = MagicMock(side_effect=NotFound("related entity not found"))
270
+
271
+ with pytest.raises(NotFound, match="related entity not found"):
272
+ entity.get_related_entity("redacted")
273
+
274
+ entity.get_relation_link.assert_called_once_with(relation_name="redacted")
275
+ entity.client.follow_link.assert_called_once_with(relation_link, infer_type=EntityObject)
276
+
277
+ def test_get_related_entity_raises_when_access_is_forbidden(self, entity):
278
+ """Test that follow_link Forbidden errors are propagated"""
279
+ relation_link = HALLink(uri="/entities/456", name="redacted")
280
+ entity.get_relation_link = MagicMock(return_value=relation_link)
281
+ entity.client.follow_link = MagicMock(side_effect=Forbidden("access denied"))
282
+
283
+ with pytest.raises(Forbidden, match="access denied"):
284
+ entity.get_related_entity("redacted")
285
+
286
+ entity.get_relation_link.assert_called_once_with(relation_name="redacted")
287
+ entity.client.follow_link.assert_called_once_with(relation_link, infer_type=EntityObject)
288
+
289
+ def test_get_related_entity(self, cg_client: ContentGridApplicationClient):
290
+ entity = cg_client.create_entity(self.candidate_entity_name, attributes={"name": "test"})
291
+ related_entity = None
292
+ new_company = None
293
+ try:
294
+ related_entity = cg_client.create_entity(self.redacted_candidate_entity_name, attributes={"original": entity.get_self_link().uri})
295
+ fetched_related_entity = entity.get_related_entity(self.relation_name)
296
+
297
+ assert isinstance(fetched_related_entity, EntityObject)
298
+ assert fetched_related_entity.get_self_link().uri == related_entity.get_self_link().uri
299
+
300
+ company_name = f"company-{uuid.uuid4().hex[:8]}"
301
+ new_company = cg_client.create_entity(self.customer_company_entity_name, attributes={"name": company_name})
302
+
303
+ related_entity.set_related_entity(relation_name=self.relation_name2, related_entity=new_company)
304
+ fetched_company = related_entity.get_related_entity(self.relation_name2)
305
+
306
+ assert isinstance(fetched_company, EntityObject)
307
+ assert fetched_company.get_self_link().uri == new_company.get_self_link().uri
308
+ finally:
309
+ if new_company is not None:
310
+ new_company.delete()
311
+ if related_entity is not None:
312
+ related_entity.delete()
313
+ entity.delete()
314
+
315
+
@@ -1,102 +0,0 @@
1
- from contentgrid_application_client import ContentGridApplicationClient, Profile, EntityProfile, AttributeProfile
2
- import pytest
3
- from contentgrid_hal_client.exceptions import NotFound
4
- from fixtures import auth_manager, cg_client # noqa: F401
5
-
6
- def test_get_profile_attributes(cg_client: ContentGridApplicationClient):
7
- profile = cg_client.get_profile()
8
- entity_links = profile.get_entity_links()
9
- assert len(entity_links) > 0
10
- assert all(link.name for link in entity_links)
11
- assert all(link.uri for link in entity_links)
12
-
13
- def test_entity_profile_details(cg_client: ContentGridApplicationClient):
14
- profile = cg_client.get_profile()
15
- entity_profile = cg_client.get_entity_profile(profile.get_entity_links()[0].name)
16
-
17
- # Test basic entity profile properties
18
- assert isinstance(entity_profile, EntityProfile)
19
- assert entity_profile.name is not None
20
- assert entity_profile.title is not None
21
- assert entity_profile.description is not None
22
-
23
- def test_attribute_profiles(cg_client: ContentGridApplicationClient):
24
- profile = cg_client.get_profile()
25
- entity_profile = cg_client.get_entity_profile(profile.get_entity_links()[0].name)
26
-
27
- attribute_profiles = entity_profile.get_attribute_profiles()
28
- if not attribute_profiles:
29
- pytest.skip(f"Entity {entity_profile.name} has no attribute profiles to test.")
30
-
31
- # Test first attribute profile properties
32
- first_attribute = attribute_profiles[0]
33
- assert isinstance(first_attribute, AttributeProfile)
34
- assert first_attribute.name is not None
35
- assert first_attribute.type is not None
36
- assert isinstance(first_attribute.required, bool)
37
- assert isinstance(first_attribute.read_only, bool)
38
-
39
- def test_invalid_entity_profile(cg_client: ContentGridApplicationClient):
40
- with pytest.raises(NotFound):
41
- cg_client.get_entity_profile("non_existent_entity")
42
-
43
- def test_nested_attributes(cg_client: ContentGridApplicationClient):
44
- profile = cg_client.get_profile()
45
- # Use a known entity type if available, otherwise the first one
46
- entity_name = "anonymous-candidate" if "anonymous-candidate" in [link.name for link in profile.get_entity_links()] else profile.get_entity_links()[0].name
47
- entity_profile = cg_client.get_entity_profile(entity_name)
48
-
49
- found_nested = False
50
- for attribute in entity_profile.get_attribute_profiles():
51
- nested_attributes = attribute.get_nested_attributes()
52
- if nested_attributes:
53
- found_nested = True
54
- assert all(isinstance(nested, AttributeProfile) for nested in nested_attributes)
55
- assert all(hasattr(nested, 'name') for nested in nested_attributes)
56
- if not found_nested:
57
- print(f"INFO: No nested attributes found for entity {entity_name} to test.")
58
-
59
- def test_content_attribute_detection(cg_client: ContentGridApplicationClient):
60
- profile = cg_client.get_profile()
61
- entity_profile = cg_client.get_entity_profile(profile.get_entity_links()[0].name)
62
-
63
- for attribute in entity_profile.get_attribute_profiles():
64
- if attribute.is_content_attribute():
65
- nested_attrs = attribute.get_nested_attributes()
66
- assert any(attr.name in ["length", "mimetype", "filename"] for attr in nested_attrs)
67
-
68
- def test_relation_profiles(cg_client: ContentGridApplicationClient):
69
- profile = cg_client.get_profile()
70
- entity_profile = cg_client.get_entity_profile("anonymous-candidate")
71
-
72
- relation_profiles = entity_profile.get_relation_profiles()
73
- assert len(relation_profiles) >= 0
74
-
75
- if relation_profiles:
76
- first_relation = relation_profiles[0]
77
- assert first_relation.name is not None
78
- assert first_relation.title is not None
79
- assert isinstance(first_relation.required, bool)
80
- assert isinstance(first_relation.many_source_per_target, bool)
81
- assert isinstance(first_relation.many_target_per_source, bool)
82
-
83
- def test_get_specific_relation_profile(cg_client: ContentGridApplicationClient):
84
- profile = cg_client.get_profile()
85
- entity_profile = cg_client.get_entity_profile("anonymous-candidate")
86
-
87
- if entity_profile.get_relation_profiles():
88
- relation_name = entity_profile.get_relation_profiles()[0].name
89
- relation_profile = entity_profile.get_relation_profile(relation_name)
90
- assert relation_profile.name == relation_name
91
-
92
- # Test related entity profile retrieval
93
- related_entity_profile = relation_profile.get_related_entity_profile()
94
- assert isinstance(related_entity_profile, EntityProfile)
95
- assert related_entity_profile.name is not None
96
-
97
- def test_invalid_relation_profile(cg_client: ContentGridApplicationClient):
98
- profile = cg_client.get_profile()
99
- entity_profile = cg_client.get_entity_profile(profile.get_entity_links()[0].name)
100
-
101
- with pytest.raises(ValueError):
102
- entity_profile.get_relation_profile("non_existent_relation")
@@ -1,92 +0,0 @@
1
- from contentgrid_application_client import ContentGridApplicationClient, EntityCollection
2
- from contentgrid_hal_client.exceptions import Forbidden, MissingHALTemplate
3
- from fixtures import auth_manager, cg_client # noqa: F401
4
- import pytest
5
-
6
-
7
- class TestRelations:
8
- entity_name = "candidate"
9
- relation_name = "skills"
10
-
11
- def test_relations(self, cg_client: ContentGridApplicationClient):
12
- entity = cg_client.create_entity(self.entity_name, {"name": "test"})
13
-
14
- # fetch related skills
15
- related_skills = cg_client.get_entity_relation_collection(
16
- entity.get_self_link(), self.relation_name
17
- )
18
-
19
- assert isinstance(related_skills, EntityCollection)
20
-
21
- # entity just instantiated so no skills
22
- assert len(related_skills.get_entities()) == 0
23
-
24
- skills = cg_client.get_entity_collection(singular_entity_name="skill")
25
-
26
- amt_skills_to_sample = 4
27
- skills_selection = skills.get_entities()[:amt_skills_to_sample]
28
- assert len(skills_selection) == amt_skills_to_sample
29
-
30
- skills_selection[1] = skills_selection[1].get_self_link().uri
31
- skills_selection[2] = skills_selection[2].get_self_link()
32
-
33
- cg_client.post_entity_relation(
34
- entity.get_self_link(),
35
- relation_name=self.relation_name,
36
- related_entity_links=skills_selection[:-1],
37
- )
38
-
39
- related_skills.refetch()
40
-
41
- assert len(related_skills.get_entities()) == amt_skills_to_sample - 1
42
-
43
- # post last skill
44
- cg_client.post_entity_relation(
45
- entity_link=entity.get_self_link(),
46
- relation_name=self.relation_name,
47
- related_entity_links=[skills_selection[-1]],
48
- )
49
-
50
- related_skills = cg_client.get_entity_relation_collection(
51
- entity_link=entity.get_self_link(), relation_name=self.relation_name
52
- )
53
- assert len(related_skills.get_entities()) == amt_skills_to_sample
54
-
55
- entity.delete()
56
-
57
- def test_put_on_to_many_not_supported(self, cg_client: ContentGridApplicationClient):
58
- entity = cg_client.create_entity(self.entity_name, {"name": "test"})
59
-
60
- skills = cg_client.get_entity_collection(singular_entity_name="skill")
61
- skills_selection = skills.get_entities()[:2]
62
-
63
- # PUT on to-many relations should raise Forbidden
64
- with pytest.raises(MissingHALTemplate):
65
- cg_client.put_entity_relation(
66
- entity.get_self_link(),
67
- relation_name=self.relation_name,
68
- related_entity_links=skills_selection,
69
- )
70
-
71
- entity.delete()
72
-
73
- def test_clear_relations(self, cg_client: ContentGridApplicationClient):
74
- entity = cg_client.create_entity(self.entity_name, {"name": "test"})
75
- # no related exist yet
76
- entity.clear_relation(relation_name=self.relation_name)
77
-
78
- skills = cg_client.get_entity_collection(singular_entity_name="skill")
79
- skills_selection = skills.get_entities()[:1]
80
- cg_client.post_entity_relation(
81
- entity.get_self_link(),
82
- relation_name=self.relation_name,
83
- related_entity_links=skills_selection,
84
- )
85
-
86
- assert len(entity.get_relation_collection(relation_name=self.relation_name).get_entities()) == 1
87
-
88
- #clear relations again
89
- entity.clear_relation(relation_name=self.relation_name)
90
-
91
-
92
-