pygeai 0.6.0b6__py3-none-any.whl → 0.6.0b10__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.
Files changed (227) hide show
  1. pygeai/_docs/source/conf.py +78 -6
  2. pygeai/_docs/source/content/api_reference/admin.rst +161 -0
  3. pygeai/_docs/source/content/api_reference/assistant.rst +326 -0
  4. pygeai/_docs/source/content/api_reference/auth.rst +379 -0
  5. pygeai/_docs/source/content/api_reference/embeddings.rst +31 -1
  6. pygeai/_docs/source/content/api_reference/evaluation.rst +590 -0
  7. pygeai/_docs/source/content/api_reference/feedback.rst +237 -0
  8. pygeai/_docs/source/content/api_reference/files.rst +592 -0
  9. pygeai/_docs/source/content/api_reference/gam.rst +401 -0
  10. pygeai/_docs/source/content/api_reference/health.rst +58 -0
  11. pygeai/_docs/source/content/api_reference/project.rst +20 -18
  12. pygeai/_docs/source/content/api_reference/proxy.rst +318 -0
  13. pygeai/_docs/source/content/api_reference/rerank.rst +94 -0
  14. pygeai/_docs/source/content/api_reference/secrets.rst +495 -0
  15. pygeai/_docs/source/content/api_reference/usage_limits.rst +390 -0
  16. pygeai/_docs/source/content/api_reference.rst +13 -1
  17. pygeai/_docs/source/content/debugger.rst +376 -83
  18. pygeai/_docs/source/content/migration.rst +528 -0
  19. pygeai/_docs/source/content/modules.rst +1 -1
  20. pygeai/_docs/source/index.rst +59 -7
  21. pygeai/_docs/source/pygeai.auth.rst +29 -0
  22. pygeai/_docs/source/pygeai.cli.commands.rst +16 -0
  23. pygeai/_docs/source/pygeai.cli.rst +8 -0
  24. pygeai/_docs/source/pygeai.core.utils.rst +16 -0
  25. pygeai/_docs/source/pygeai.rst +1 -0
  26. pygeai/_docs/source/pygeai.tests.auth.rst +21 -0
  27. pygeai/_docs/source/pygeai.tests.cli.commands.rst +16 -0
  28. pygeai/_docs/source/pygeai.tests.cli.rst +16 -0
  29. pygeai/_docs/source/pygeai.tests.core.base.rst +8 -0
  30. pygeai/_docs/source/pygeai.tests.core.embeddings.rst +16 -0
  31. pygeai/_docs/source/pygeai.tests.core.files.rst +8 -0
  32. pygeai/_docs/source/pygeai.tests.core.plugins.rst +21 -0
  33. pygeai/_docs/source/pygeai.tests.core.rst +1 -0
  34. pygeai/_docs/source/pygeai.tests.evaluation.dataset.rst +21 -0
  35. pygeai/_docs/source/pygeai.tests.evaluation.plan.rst +21 -0
  36. pygeai/_docs/source/pygeai.tests.evaluation.result.rst +21 -0
  37. pygeai/_docs/source/pygeai.tests.evaluation.rst +20 -0
  38. pygeai/_docs/source/pygeai.tests.integration.lab.processes.rst +8 -0
  39. pygeai/_docs/source/pygeai.tests.organization.rst +8 -0
  40. pygeai/_docs/source/pygeai.tests.rst +2 -0
  41. pygeai/_docs/source/pygeai.tests.snippets.auth.rst +10 -0
  42. pygeai/_docs/source/pygeai.tests.snippets.chat.rst +40 -0
  43. pygeai/_docs/source/pygeai.tests.snippets.dbg.rst +45 -0
  44. pygeai/_docs/source/pygeai.tests.snippets.embeddings.rst +40 -0
  45. pygeai/_docs/source/pygeai.tests.snippets.evaluation.dataset.rst +197 -0
  46. pygeai/_docs/source/pygeai.tests.snippets.evaluation.plan.rst +133 -0
  47. pygeai/_docs/source/pygeai.tests.snippets.evaluation.result.rst +37 -0
  48. pygeai/_docs/source/pygeai.tests.snippets.evaluation.rst +10 -0
  49. pygeai/_docs/source/pygeai.tests.snippets.organization.rst +40 -0
  50. pygeai/_docs/source/pygeai.tests.snippets.rst +2 -0
  51. pygeai/admin/clients.py +12 -32
  52. pygeai/assistant/clients.py +16 -44
  53. pygeai/assistant/data/clients.py +1 -0
  54. pygeai/assistant/data_analyst/clients.py +6 -13
  55. pygeai/assistant/rag/clients.py +24 -67
  56. pygeai/auth/clients.py +88 -14
  57. pygeai/auth/endpoints.py +4 -0
  58. pygeai/chat/clients.py +192 -25
  59. pygeai/chat/endpoints.py +2 -1
  60. pygeai/cli/commands/auth.py +178 -2
  61. pygeai/cli/commands/chat.py +227 -1
  62. pygeai/cli/commands/embeddings.py +56 -8
  63. pygeai/cli/commands/lab/ai_lab.py +0 -2
  64. pygeai/cli/commands/migrate.py +994 -434
  65. pygeai/cli/commands/organization.py +241 -0
  66. pygeai/cli/error_handler.py +116 -0
  67. pygeai/cli/geai.py +28 -10
  68. pygeai/cli/parsers.py +8 -2
  69. pygeai/core/base/clients.py +4 -1
  70. pygeai/core/common/exceptions.py +11 -10
  71. pygeai/core/embeddings/__init__.py +19 -0
  72. pygeai/core/embeddings/clients.py +20 -9
  73. pygeai/core/embeddings/mappers.py +16 -2
  74. pygeai/core/embeddings/responses.py +9 -2
  75. pygeai/core/feedback/clients.py +4 -8
  76. pygeai/core/files/clients.py +10 -25
  77. pygeai/core/files/managers.py +42 -0
  78. pygeai/core/llm/clients.py +11 -26
  79. pygeai/core/models.py +107 -0
  80. pygeai/core/plugins/clients.py +4 -7
  81. pygeai/core/rerank/clients.py +4 -8
  82. pygeai/core/secrets/clients.py +14 -37
  83. pygeai/core/services/rest.py +1 -1
  84. pygeai/core/utils/parsers.py +32 -0
  85. pygeai/core/utils/validators.py +10 -0
  86. pygeai/dbg/__init__.py +3 -0
  87. pygeai/dbg/debugger.py +565 -70
  88. pygeai/evaluation/clients.py +2 -1
  89. pygeai/evaluation/dataset/clients.py +46 -44
  90. pygeai/evaluation/plan/clients.py +28 -26
  91. pygeai/evaluation/result/clients.py +38 -5
  92. pygeai/gam/clients.py +10 -25
  93. pygeai/health/clients.py +4 -7
  94. pygeai/lab/agents/clients.py +21 -54
  95. pygeai/lab/agents/endpoints.py +2 -0
  96. pygeai/lab/clients.py +1 -0
  97. pygeai/lab/models.py +3 -3
  98. pygeai/lab/processes/clients.py +45 -127
  99. pygeai/lab/strategies/clients.py +11 -25
  100. pygeai/lab/tools/clients.py +23 -67
  101. pygeai/lab/tools/endpoints.py +3 -0
  102. pygeai/migration/__init__.py +31 -0
  103. pygeai/migration/strategies.py +404 -155
  104. pygeai/migration/tools.py +170 -3
  105. pygeai/organization/clients.py +135 -51
  106. pygeai/organization/endpoints.py +6 -1
  107. pygeai/organization/limits/clients.py +32 -91
  108. pygeai/organization/managers.py +157 -1
  109. pygeai/organization/mappers.py +76 -2
  110. pygeai/organization/responses.py +25 -1
  111. pygeai/proxy/clients.py +4 -1
  112. pygeai/tests/admin/test_clients.py +16 -11
  113. pygeai/tests/assistants/rag/test_clients.py +35 -23
  114. pygeai/tests/assistants/test_clients.py +22 -15
  115. pygeai/tests/auth/test_clients.py +191 -7
  116. pygeai/tests/chat/test_clients.py +211 -1
  117. pygeai/tests/cli/commands/test_embeddings.py +32 -9
  118. pygeai/tests/cli/commands/test_evaluation.py +7 -0
  119. pygeai/tests/cli/commands/test_migrate.py +112 -243
  120. pygeai/tests/cli/test_error_handler.py +225 -0
  121. pygeai/tests/cli/test_geai_driver.py +154 -0
  122. pygeai/tests/cli/test_parsers.py +5 -5
  123. pygeai/tests/core/embeddings/test_clients.py +144 -0
  124. pygeai/tests/core/embeddings/test_managers.py +171 -0
  125. pygeai/tests/core/embeddings/test_mappers.py +142 -0
  126. pygeai/tests/core/feedback/test_clients.py +2 -0
  127. pygeai/tests/core/files/test_clients.py +1 -0
  128. pygeai/tests/core/llm/test_clients.py +14 -9
  129. pygeai/tests/core/plugins/test_clients.py +5 -3
  130. pygeai/tests/core/rerank/test_clients.py +1 -0
  131. pygeai/tests/core/secrets/test_clients.py +19 -13
  132. pygeai/tests/dbg/test_debugger.py +453 -75
  133. pygeai/tests/evaluation/dataset/test_clients.py +3 -1
  134. pygeai/tests/evaluation/plan/test_clients.py +4 -2
  135. pygeai/tests/evaluation/result/test_clients.py +7 -5
  136. pygeai/tests/gam/test_clients.py +1 -1
  137. pygeai/tests/health/test_clients.py +1 -0
  138. pygeai/tests/lab/agents/test_clients.py +9 -0
  139. pygeai/tests/lab/processes/test_clients.py +36 -0
  140. pygeai/tests/lab/processes/test_mappers.py +3 -0
  141. pygeai/tests/lab/strategies/test_clients.py +14 -9
  142. pygeai/tests/migration/test_strategies.py +45 -218
  143. pygeai/tests/migration/test_tools.py +133 -9
  144. pygeai/tests/organization/limits/test_clients.py +17 -0
  145. pygeai/tests/organization/test_clients.py +206 -1
  146. pygeai/tests/organization/test_managers.py +122 -1
  147. pygeai/tests/proxy/test_clients.py +2 -0
  148. pygeai/tests/proxy/test_integration.py +1 -0
  149. pygeai/tests/snippets/auth/__init__.py +0 -0
  150. pygeai/tests/snippets/chat/chat_completion_with_reasoning_effort.py +18 -0
  151. pygeai/tests/snippets/chat/get_response.py +15 -0
  152. pygeai/tests/snippets/chat/get_response_streaming.py +20 -0
  153. pygeai/tests/snippets/chat/get_response_with_files.py +16 -0
  154. pygeai/tests/snippets/chat/get_response_with_tools.py +36 -0
  155. pygeai/tests/snippets/dbg/__init__.py +0 -0
  156. pygeai/tests/snippets/dbg/basic_debugging.py +32 -0
  157. pygeai/tests/snippets/dbg/breakpoint_management.py +48 -0
  158. pygeai/tests/snippets/dbg/stack_navigation.py +45 -0
  159. pygeai/tests/snippets/dbg/stepping_example.py +40 -0
  160. pygeai/tests/snippets/embeddings/cache_example.py +31 -0
  161. pygeai/tests/snippets/embeddings/cohere_example.py +41 -0
  162. pygeai/tests/snippets/embeddings/openai_base64_example.py +27 -0
  163. pygeai/tests/snippets/embeddings/openai_example.py +30 -0
  164. pygeai/tests/snippets/embeddings/similarity_example.py +42 -0
  165. pygeai/tests/snippets/evaluation/dataset/__init__.py +0 -0
  166. pygeai/tests/snippets/evaluation/dataset/complete_workflow_example.py +195 -0
  167. pygeai/tests/snippets/evaluation/dataset/create_dataset.py +26 -0
  168. pygeai/tests/snippets/evaluation/dataset/create_dataset_from_file.py +11 -0
  169. pygeai/tests/snippets/evaluation/dataset/create_dataset_row.py +17 -0
  170. pygeai/tests/snippets/evaluation/dataset/create_expected_source.py +18 -0
  171. pygeai/tests/snippets/evaluation/dataset/create_filter_variable.py +19 -0
  172. pygeai/tests/snippets/evaluation/dataset/delete_dataset.py +9 -0
  173. pygeai/tests/snippets/evaluation/dataset/delete_dataset_row.py +10 -0
  174. pygeai/tests/snippets/evaluation/dataset/delete_expected_source.py +15 -0
  175. pygeai/tests/snippets/evaluation/dataset/delete_filter_variable.py +15 -0
  176. pygeai/tests/snippets/evaluation/dataset/get_dataset.py +9 -0
  177. pygeai/tests/snippets/evaluation/dataset/get_dataset_row.py +10 -0
  178. pygeai/tests/snippets/evaluation/dataset/get_expected_source.py +15 -0
  179. pygeai/tests/snippets/evaluation/dataset/get_filter_variable.py +15 -0
  180. pygeai/tests/snippets/evaluation/dataset/list_dataset_rows.py +9 -0
  181. pygeai/tests/snippets/evaluation/dataset/list_datasets.py +6 -0
  182. pygeai/tests/snippets/evaluation/dataset/list_expected_sources.py +10 -0
  183. pygeai/tests/snippets/evaluation/dataset/list_filter_variables.py +10 -0
  184. pygeai/tests/snippets/evaluation/dataset/update_dataset.py +15 -0
  185. pygeai/tests/snippets/evaluation/dataset/update_dataset_row.py +20 -0
  186. pygeai/tests/snippets/evaluation/dataset/update_expected_source.py +18 -0
  187. pygeai/tests/snippets/evaluation/dataset/update_filter_variable.py +19 -0
  188. pygeai/tests/snippets/evaluation/dataset/upload_dataset_rows_file.py +10 -0
  189. pygeai/tests/snippets/evaluation/plan/__init__.py +0 -0
  190. pygeai/tests/snippets/evaluation/plan/add_plan_system_metric.py +13 -0
  191. pygeai/tests/snippets/evaluation/plan/complete_workflow_example.py +136 -0
  192. pygeai/tests/snippets/evaluation/plan/create_evaluation_plan.py +24 -0
  193. pygeai/tests/snippets/evaluation/plan/create_rag_evaluation_plan.py +22 -0
  194. pygeai/tests/snippets/evaluation/plan/delete_evaluation_plan.py +9 -0
  195. pygeai/tests/snippets/evaluation/plan/delete_plan_system_metric.py +13 -0
  196. pygeai/tests/snippets/evaluation/plan/execute_evaluation_plan.py +11 -0
  197. pygeai/tests/snippets/evaluation/plan/get_evaluation_plan.py +9 -0
  198. pygeai/tests/snippets/evaluation/plan/get_plan_system_metric.py +13 -0
  199. pygeai/tests/snippets/evaluation/plan/get_system_metric.py +9 -0
  200. pygeai/tests/snippets/evaluation/plan/list_evaluation_plans.py +7 -0
  201. pygeai/tests/snippets/evaluation/plan/list_plan_system_metrics.py +9 -0
  202. pygeai/tests/snippets/evaluation/plan/list_system_metrics.py +7 -0
  203. pygeai/tests/snippets/evaluation/plan/update_evaluation_plan.py +22 -0
  204. pygeai/tests/snippets/evaluation/plan/update_plan_system_metric.py +14 -0
  205. pygeai/tests/snippets/evaluation/result/__init__.py +0 -0
  206. pygeai/tests/snippets/evaluation/result/complete_workflow_example.py +150 -0
  207. pygeai/tests/snippets/evaluation/result/get_evaluation_result.py +26 -0
  208. pygeai/tests/snippets/evaluation/result/list_evaluation_results.py +17 -0
  209. pygeai/tests/snippets/migrate/__init__.py +45 -0
  210. pygeai/tests/snippets/migrate/agent_migration.py +110 -0
  211. pygeai/tests/snippets/migrate/assistant_migration.py +64 -0
  212. pygeai/tests/snippets/migrate/orchestrator_examples.py +179 -0
  213. pygeai/tests/snippets/migrate/process_migration.py +64 -0
  214. pygeai/tests/snippets/migrate/project_migration.py +42 -0
  215. pygeai/tests/snippets/migrate/tool_migration.py +64 -0
  216. pygeai/tests/snippets/organization/create_project.py +2 -2
  217. pygeai/tests/snippets/organization/get_memberships.py +12 -0
  218. pygeai/tests/snippets/organization/get_organization_members.py +6 -0
  219. pygeai/tests/snippets/organization/get_project_members.py +6 -0
  220. pygeai/tests/snippets/organization/get_project_memberships.py +12 -0
  221. pygeai/tests/snippets/organization/get_project_roles.py +6 -0
  222. {pygeai-0.6.0b6.dist-info → pygeai-0.6.0b10.dist-info}/METADATA +1 -1
  223. {pygeai-0.6.0b6.dist-info → pygeai-0.6.0b10.dist-info}/RECORD +227 -124
  224. {pygeai-0.6.0b6.dist-info → pygeai-0.6.0b10.dist-info}/WHEEL +0 -0
  225. {pygeai-0.6.0b6.dist-info → pygeai-0.6.0b10.dist-info}/entry_points.txt +0 -0
  226. {pygeai-0.6.0b6.dist-info → pygeai-0.6.0b10.dist-info}/licenses/LICENSE +0 -0
  227. {pygeai-0.6.0b6.dist-info → pygeai-0.6.0b10.dist-info}/top_level.txt +0 -0
@@ -1,8 +1,9 @@
1
- from json import JSONDecodeError
2
1
 
3
2
  from pygeai import logger
4
3
  from pygeai.core.base.clients import BaseClient
5
4
  from pygeai.core.common.exceptions import InvalidAPIResponseException
5
+ from pygeai.core.utils.validators import validate_status_code
6
+ from pygeai.core.utils.parsers import parse_json_response
6
7
  from pygeai.organization.limits.endpoints import SET_ORGANIZATION_USAGE_LIMIT_V2, GET_ORGANIZATION_LATEST_USAGE_LIMIT_V2, \
7
8
  GET_ALL_ORGANIZATION_USAGE_LIMITS_V2, DELETE_ORGANIZATION_USAGE_LIMIT_V2, SET_ORGANIZATION_HARD_LIMIT_V2, \
8
9
  SET_ORGANIZATION_SOFT_LIMIT_V2, SET_ORGANIZATION_RENEWAL_STATUS_V2, SET_PROJECT_USAGE_LIMIT_V2, \
@@ -33,12 +34,8 @@ class UsageLimitClient(BaseClient):
33
34
  endpoint=endpoint,
34
35
  data=usage_limit
35
36
  )
36
- try:
37
- result = response.json()
38
- return result
39
- except JSONDecodeError as e:
40
- logger.error(f"Unable to set usage limit for organization '{organization}': JSON parsing error (status {response.status_code}): {e}. Response: {response.text}")
41
- raise InvalidAPIResponseException(f"Unable to set usage limit for organization '{organization}': {response.text}")
37
+ validate_status_code(response)
38
+ return parse_json_response(response, f"set usage limit for organization", organization=organization)
42
39
 
43
40
  def get_organization_latest_usage_limit(self, organization: str) -> dict:
44
41
  """
@@ -49,12 +46,8 @@ class UsageLimitClient(BaseClient):
49
46
  """
50
47
  endpoint = GET_ORGANIZATION_LATEST_USAGE_LIMIT_V2.format(organization=organization)
51
48
  response = self.api_service.get(endpoint=endpoint)
52
- try:
53
- result = response.json()
54
- return result
55
- except JSONDecodeError as e:
56
- logger.error(f"Unable to get latest usage limit for organization '{organization}': JSON parsing error (status {response.status_code}): {e}. Response: {response.text}")
57
- raise InvalidAPIResponseException(f"Unable to get latest usage limit for organization '{organization}': {response.text}")
49
+ validate_status_code(response)
50
+ return parse_json_response(response, f"get latest usage limit for organization", organization=organization)
58
51
 
59
52
  def get_all_usage_limits_from_organization(self, organization: str) -> dict:
60
53
  """
@@ -65,12 +58,8 @@ class UsageLimitClient(BaseClient):
65
58
  """
66
59
  endpoint = GET_ALL_ORGANIZATION_USAGE_LIMITS_V2.format(organization=organization)
67
60
  response = self.api_service.get(endpoint=endpoint)
68
- try:
69
- result = response.json()
70
- return result
71
- except JSONDecodeError as e:
72
- logger.error(f"Unable to get all usage limits for organization '{organization}': JSON parsing error (status {response.status_code}): {e}. Response: {response.text}")
73
- raise InvalidAPIResponseException(f"Unable to get all usage limits for organization '{organization}': {response.text}")
61
+ validate_status_code(response)
62
+ return parse_json_response(response, f"get all usage limits for organization", organization=organization)
74
63
 
75
64
  def delete_usage_limit_from_organization(self, organization: str, limit_id: str) -> dict:
76
65
  """
@@ -82,12 +71,8 @@ class UsageLimitClient(BaseClient):
82
71
  """
83
72
  endpoint = DELETE_ORGANIZATION_USAGE_LIMIT_V2.format(organization=organization, id=limit_id)
84
73
  response = self.api_service.delete(endpoint=endpoint)
85
- try:
86
- result = response.json()
87
- return result
88
- except JSONDecodeError as e:
89
- logger.error(f"Unable to delete usage limit with ID '{limit_id}' from organization '{organization}': JSON parsing error (status {response.status_code}): {e}. Response: {response.text}")
90
- raise InvalidAPIResponseException(f"Unable to delete usage limit with ID '{limit_id}' from organization '{organization}': {response.text}")
74
+ validate_status_code(response)
75
+ return parse_json_response(response, f"delete usage limit with ID '{limit_id}' from organization", organization=organization)
91
76
 
92
77
  def set_organization_hard_limit(self, organization: str, limit_id: str, hard_limit: float) -> dict:
93
78
  """
@@ -105,12 +90,8 @@ class UsageLimitClient(BaseClient):
105
90
  "hardLimit": hard_limit
106
91
  }
107
92
  )
108
- try:
109
- result = response.json()
110
- return result
111
- except JSONDecodeError as e:
112
- logger.error(f"Unable to set hard limit for usage limit ID '{limit_id}' in organization '{organization}': JSON parsing error (status {response.status_code}): {e}. Response: {response.text}")
113
- raise InvalidAPIResponseException(f"Unable to set hard limit for usage limit ID '{limit_id}' in organization '{organization}': {response.text}")
93
+ validate_status_code(response)
94
+ return parse_json_response(response, f"set hard limit for usage limit ID '{limit_id}' in organization", organization=organization)
114
95
 
115
96
  def set_organization_soft_limit(self, organization: str, limit_id: str, soft_limit: float) -> dict:
116
97
  """
@@ -128,12 +109,8 @@ class UsageLimitClient(BaseClient):
128
109
  "softLimit": soft_limit
129
110
  }
130
111
  )
131
- try:
132
- result = response.json()
133
- return result
134
- except JSONDecodeError as e:
135
- logger.error(f"Unable to set soft limit for usage limit ID '{limit_id}' in organization '{organization}': JSON parsing error (status {response.status_code}): {e}. Response: {response.text}")
136
- raise InvalidAPIResponseException(f"Unable to set soft limit for usage limit ID '{limit_id}' in organization '{organization}': {response.text}")
112
+ validate_status_code(response)
113
+ return parse_json_response(response, f"set soft limit for usage limit ID '{limit_id}' in organization", organization=organization)
137
114
 
138
115
  def set_organization_renewal_status(self, organization: str, limit_id: str, renewal_status: str) -> dict:
139
116
  """
@@ -151,12 +128,8 @@ class UsageLimitClient(BaseClient):
151
128
  "renewalStatus": renewal_status
152
129
  }
153
130
  )
154
- try:
155
- result = response.json()
156
- return result
157
- except JSONDecodeError as e:
158
- logger.error(f"Unable to set renewal status for usage limit ID '{limit_id}' in organization '{organization}': JSON parsing error (status {response.status_code}): {e}. Response: {response.text}")
159
- raise InvalidAPIResponseException(f"Unable to set renewal status for usage limit ID '{limit_id}' in organization '{organization}': {response.text}")
131
+ validate_status_code(response)
132
+ return parse_json_response(response, f"set renewal status for usage limit ID '{limit_id}' in organization", organization=organization)
160
133
 
161
134
  def set_project_usage_limit(self, organization: str, project: str, usage_limit: dict) -> dict:
162
135
  """
@@ -179,12 +152,8 @@ class UsageLimitClient(BaseClient):
179
152
  endpoint=endpoint,
180
153
  data=usage_limit
181
154
  )
182
- try:
183
- result = response.json()
184
- return result
185
- except JSONDecodeError as e:
186
- logger.error(f"Unable to set usage limit for project '{project}' in organization '{organization}': JSON parsing error (status {response.status_code}): {e}. Response: {response.text}")
187
- raise InvalidAPIResponseException(f"Unable to set usage limit for project '{project}' in organization '{organization}': {response.text}")
155
+ validate_status_code(response)
156
+ return parse_json_response(response, f"set usage limit for project '{project}' in organization", organization=organization)
188
157
 
189
158
  def get_all_usage_limits_from_project(self, organization: str, project: str) -> dict:
190
159
  """
@@ -196,12 +165,8 @@ class UsageLimitClient(BaseClient):
196
165
  """
197
166
  endpoint = GET_ALL_PROJECT_USAGE_LIMIT_V2.format(organization=organization, project=project)
198
167
  response = self.api_service.get(endpoint=endpoint)
199
- try:
200
- result = response.json()
201
- return result
202
- except JSONDecodeError as e:
203
- logger.error(f"Unable to get all usage limits for project '{project}' in organization '{organization}': JSON parsing error (status {response.status_code}): {e}. Response: {response.text}")
204
- raise InvalidAPIResponseException(f"Unable to get all usage limits for project '{project}' in organization '{organization}': {response.text}")
168
+ validate_status_code(response)
169
+ return parse_json_response(response, f"get all usage limits for project '{project}' in organization", organization=organization)
205
170
 
206
171
  def get_latest_usage_limit_from_project(self, organization: str, project: str) -> dict:
207
172
  """
@@ -213,12 +178,8 @@ class UsageLimitClient(BaseClient):
213
178
  """
214
179
  endpoint = GET_LATEST_PROJECT_USAGE_LIMIT_V2.format(organization=organization, project=project)
215
180
  response = self.api_service.get(endpoint=endpoint)
216
- try:
217
- result = response.json()
218
- return result
219
- except JSONDecodeError as e:
220
- logger.error(f"Unable to get latest usage limit for project '{project}' in organization '{organization}': JSON parsing error (status {response.status_code}): {e}. Response: {response.text}")
221
- raise InvalidAPIResponseException(f"Unable to get latest usage limit for project '{project}' in organization '{organization}': {response.text}")
181
+ validate_status_code(response)
182
+ return parse_json_response(response, f"get latest usage limit for project '{project}' in organization", organization=organization)
222
183
 
223
184
  def get_active_usage_limit_from_project(self, organization: str, project: str) -> dict:
224
185
  """
@@ -230,12 +191,8 @@ class UsageLimitClient(BaseClient):
230
191
  """
231
192
  endpoint = GET_PROJECT_ACTIVE_USAGE_LIMIT_V2.format(organization=organization, project=project)
232
193
  response = self.api_service.get(endpoint=endpoint)
233
- try:
234
- result = response.json()
235
- return result
236
- except JSONDecodeError as e:
237
- logger.error(f"Unable to get active usage limit for project '{project}' in organization '{organization}': JSON parsing error (status {response.status_code}): {e}. Response: {response.text}")
238
- raise InvalidAPIResponseException(f"Unable to get active usage limit for project '{project}' in organization '{organization}': {response.text}")
194
+ validate_status_code(response)
195
+ return parse_json_response(response, f"get active usage limit for project '{project}' in organization", organization=organization)
239
196
 
240
197
  def delete_usage_limit_from_project(self, organization: str, project: str, limit_id: str) -> dict:
241
198
  """
@@ -248,12 +205,8 @@ class UsageLimitClient(BaseClient):
248
205
  """
249
206
  endpoint = DELETE_PROJECT_USAGE_LIMIT_V2.format(organization=organization, project=project, id=limit_id)
250
207
  response = self.api_service.delete(endpoint=endpoint)
251
- try:
252
- result = response.json()
253
- return result
254
- except JSONDecodeError as e:
255
- logger.error(f"Unable to delete usage limit with ID '{limit_id}' for project '{project}' in organization '{organization}': JSON parsing error (status {response.status_code}): {e}. Response: {response.text}")
256
- raise InvalidAPIResponseException(f"Unable to delete usage limit with ID '{limit_id}' for project '{project}' in organization '{organization}': {response.text}")
208
+ validate_status_code(response)
209
+ return parse_json_response(response, f"delete usage limit with ID '{limit_id}' for project '{project}' in organization", organization=organization)
257
210
 
258
211
  def set_hard_limit_for_active_usage_limit_from_project(
259
212
  self,
@@ -278,12 +231,8 @@ class UsageLimitClient(BaseClient):
278
231
  "hardLimit": hard_limit
279
232
  }
280
233
  )
281
- try:
282
- result = response.json()
283
- return result
284
- except JSONDecodeError as e:
285
- logger.error(f"Unable to set hard limit for usage limit ID '{limit_id}' for project '{project}' in organization '{organization}': JSON parsing error (status {response.status_code}): {e}. Response: {response.text}")
286
- raise InvalidAPIResponseException(f"Unable to set hard limit for usage limit ID '{limit_id}' for project '{project}' in organization '{organization}': {response.text}")
234
+ validate_status_code(response)
235
+ return parse_json_response(response, f"set hard limit for usage limit ID '{limit_id}' for project '{project}' in organization", organization=organization)
287
236
 
288
237
  def set_soft_limit_for_active_usage_limit_from_project(
289
238
  self,
@@ -308,12 +257,8 @@ class UsageLimitClient(BaseClient):
308
257
  "softLimit": soft_limit
309
258
  }
310
259
  )
311
- try:
312
- result = response.json()
313
- return result
314
- except JSONDecodeError as e:
315
- logger.error(f"Unable to set soft limit for usage limit ID '{limit_id}' for project '{project}' in organization '{organization}': JSON parsing error (status {response.status_code}): {e}. Response: {response.text}")
316
- raise InvalidAPIResponseException(f"Unable to set soft limit for usage limit ID '{limit_id}' for project '{project}' in organization '{organization}': {response.text}")
260
+ validate_status_code(response)
261
+ return parse_json_response(response, f"set soft limit for usage limit ID '{limit_id}' for project '{project}' in organization", organization=organization)
317
262
 
318
263
  def set_project_renewal_status(self, organization: str, project: str, limit_id: str, renewal_status: str) -> dict:
319
264
  """
@@ -332,9 +277,5 @@ class UsageLimitClient(BaseClient):
332
277
  "renewalStatus": renewal_status
333
278
  }
334
279
  )
335
- try:
336
- result = response.json()
337
- return result
338
- except JSONDecodeError as e:
339
- logger.error(f"Unable to set renewal status for usage limit ID '{limit_id}' for project '{project}' in organization '{organization}': JSON parsing error (status {response.status_code}): {e}. Response: {response.text}")
340
- raise InvalidAPIResponseException(f"Unable to set renewal status for usage limit ID '{limit_id}' for project '{project}' in organization '{organization}': {response.text}")
280
+ validate_status_code(response)
281
+ return parse_json_response(response, f"set renewal status for usage limit ID '{limit_id}' for project '{project}' in organization", organization=organization)
@@ -6,7 +6,8 @@ from pygeai.core.base.responses import EmptyResponse
6
6
  from pygeai.organization.clients import OrganizationClient
7
7
  from pygeai.organization.mappers import OrganizationResponseMapper
8
8
  from pygeai.organization.responses import AssistantListResponse, ProjectListResponse, ProjectDataResponse, \
9
- ProjectTokensResponse, ProjectItemListResponse
9
+ ProjectTokensResponse, ProjectItemListResponse, MembershipsResponse, ProjectMembershipsResponse, \
10
+ ProjectRolesResponse, ProjectMembersResponse, OrganizationMembersResponse
10
11
  from pygeai.core.common.exceptions import APIError
11
12
 
12
13
 
@@ -247,3 +248,158 @@ class OrganizationManager:
247
248
 
248
249
  result = OrganizationResponseMapper.map_to_item_list_response(response_data)
249
250
  return result
251
+
252
+ def get_memberships(
253
+ self,
254
+ email: str = None,
255
+ start_page: int = 1,
256
+ page_size: int = 20,
257
+ order_key: str = None,
258
+ order_direction: str = "desc",
259
+ role_types: str = None
260
+ ) -> MembershipsResponse:
261
+ """
262
+ Retrieves a list of Organizations and Projects a user belongs to with their Roles.
263
+
264
+ This method calls `OrganizationClient.get_memberships` to fetch membership data
265
+ and maps the response using `OrganizationResponseMapper.map_to_memberships_response`.
266
+
267
+ :param email: str, optional - The email address of the user to search for (case-insensitive).
268
+ :param start_page: int - The page number for pagination (default is 1).
269
+ :param page_size: int - The number of items per page (default is 20).
270
+ :param order_key: str, optional - Field for sorting. Only 'organizationName' is supported.
271
+ :param order_direction: str - Sort direction: 'asc' or 'desc' (default is 'desc').
272
+ :param role_types: str, optional - Comma-separated list of role types: 'backend', 'frontend' (case-insensitive).
273
+ :return: MembershipsResponse - The mapped response containing organizations and projects with roles.
274
+ :raises APIError: If the API returns errors.
275
+ """
276
+ response_data = self.__organization_client.get_memberships(
277
+ email=email,
278
+ start_page=start_page,
279
+ page_size=page_size,
280
+ order_key=order_key,
281
+ order_direction=order_direction,
282
+ role_types=role_types
283
+ )
284
+ if ErrorHandler.has_errors(response_data):
285
+ error = ErrorHandler.extract_error(response_data)
286
+ logger.error(f"Error received while retrieving memberships: {error}")
287
+ raise APIError(f"Error received while retrieving memberships: {error}")
288
+
289
+ result = OrganizationResponseMapper.map_to_memberships_response(response_data)
290
+ return result
291
+
292
+ def get_project_memberships(
293
+ self,
294
+ email: str = None,
295
+ start_page: int = 1,
296
+ page_size: int = 20,
297
+ order_key: str = None,
298
+ order_direction: str = "desc",
299
+ role_types: str = None
300
+ ) -> ProjectMembershipsResponse:
301
+ """
302
+ Retrieves a list of Projects and Roles for a user within a specific Organization.
303
+
304
+ This method calls `OrganizationClient.get_project_memberships` to fetch project membership data
305
+ and maps the response using `OrganizationResponseMapper.map_to_project_memberships_response`.
306
+
307
+ :param email: str, optional - The email address of the user to search for (case-insensitive).
308
+ :param start_page: int - The page number for pagination (default is 1).
309
+ :param page_size: int - The number of items per page (default is 20).
310
+ :param order_key: str, optional - Field for sorting. Only 'projectName' is supported.
311
+ :param order_direction: str - Sort direction: 'asc' or 'desc' (default is 'desc').
312
+ :param role_types: str, optional - Comma-separated list of role types: 'backend', 'frontend' (case-insensitive).
313
+ :return: ProjectMembershipsResponse - The mapped response containing projects with roles.
314
+ :raises APIError: If the API returns errors.
315
+ """
316
+ response_data = self.__organization_client.get_project_memberships(
317
+ email=email,
318
+ start_page=start_page,
319
+ page_size=page_size,
320
+ order_key=order_key,
321
+ order_direction=order_direction,
322
+ role_types=role_types
323
+ )
324
+ if ErrorHandler.has_errors(response_data):
325
+ error = ErrorHandler.extract_error(response_data)
326
+ logger.error(f"Error received while retrieving project memberships: {error}")
327
+ raise APIError(f"Error received while retrieving project memberships: {error}")
328
+
329
+ result = OrganizationResponseMapper.map_to_project_memberships_response(response_data)
330
+ return result
331
+
332
+ def get_project_roles(
333
+ self,
334
+ project_id: str
335
+ ) -> ProjectRolesResponse:
336
+ """
337
+ Retrieves all Roles supported by a specific Project.
338
+
339
+ This method calls `OrganizationClient.get_project_roles` to fetch project roles
340
+ and maps the response using `OrganizationResponseMapper.map_to_project_roles_response`.
341
+
342
+ :param project_id: str - The unique identifier of the project.
343
+ :return: ProjectRolesResponse - The mapped response containing the list of roles.
344
+ :raises APIError: If the API returns errors.
345
+ """
346
+ response_data = self.__organization_client.get_project_roles(
347
+ project_id=project_id
348
+ )
349
+ if ErrorHandler.has_errors(response_data):
350
+ error = ErrorHandler.extract_error(response_data)
351
+ logger.error(f"Error received while retrieving project roles: {error}")
352
+ raise APIError(f"Error received while retrieving project roles: {error}")
353
+
354
+ result = OrganizationResponseMapper.map_to_project_roles_response(response_data)
355
+ return result
356
+
357
+ def get_project_members(
358
+ self,
359
+ project_id: str
360
+ ) -> ProjectMembersResponse:
361
+ """
362
+ Retrieves all members and their Roles for a specific Project.
363
+
364
+ This method calls `OrganizationClient.get_project_members` to fetch project members
365
+ and maps the response using `OrganizationResponseMapper.map_to_project_members_response`.
366
+
367
+ :param project_id: str - The unique identifier of the project.
368
+ :return: ProjectMembersResponse - The mapped response containing members with their roles.
369
+ :raises APIError: If the API returns errors.
370
+ """
371
+ response_data = self.__organization_client.get_project_members(
372
+ project_id=project_id
373
+ )
374
+ if ErrorHandler.has_errors(response_data):
375
+ error = ErrorHandler.extract_error(response_data)
376
+ logger.error(f"Error received while retrieving project members: {error}")
377
+ raise APIError(f"Error received while retrieving project members: {error}")
378
+
379
+ result = OrganizationResponseMapper.map_to_project_members_response(response_data)
380
+ return result
381
+
382
+ def get_organization_members(
383
+ self,
384
+ organization_id: str
385
+ ) -> OrganizationMembersResponse:
386
+ """
387
+ Retrieves all members and their Roles for a specific Organization.
388
+
389
+ This method calls `OrganizationClient.get_organization_members` to fetch organization members
390
+ and maps the response using `OrganizationResponseMapper.map_to_organization_members_response`.
391
+
392
+ :param organization_id: str - The unique identifier of the organization.
393
+ :return: OrganizationMembersResponse - The mapped response containing members with their roles.
394
+ :raises APIError: If the API returns errors.
395
+ """
396
+ response_data = self.__organization_client.get_organization_members(
397
+ organization_id=organization_id
398
+ )
399
+ if ErrorHandler.has_errors(response_data):
400
+ error = ErrorHandler.extract_error(response_data)
401
+ logger.error(f"Error received while retrieving organization members: {error}")
402
+ raise APIError(f"Error received while retrieving organization members: {error}")
403
+
404
+ result = OrganizationResponseMapper.map_to_organization_members_response(response_data)
405
+ return result
@@ -1,7 +1,8 @@
1
1
  from pygeai.core.base.mappers import ModelMapper
2
- from pygeai.core.models import Assistant, Project
2
+ from pygeai.core.models import Assistant, Project, Role, Member, OrganizationMembership, ProjectMembership
3
3
  from pygeai.organization.responses import AssistantListResponse, ProjectListResponse, ProjectDataResponse, \
4
- ProjectTokensResponse, ProjectItemListResponse
4
+ ProjectTokensResponse, ProjectItemListResponse, MembershipsResponse, ProjectMembershipsResponse, \
5
+ ProjectRolesResponse, ProjectMembersResponse, OrganizationMembersResponse
5
6
 
6
7
 
7
8
  class OrganizationResponseMapper:
@@ -76,3 +77,76 @@ class OrganizationResponseMapper:
76
77
  items=item_list
77
78
  )
78
79
 
80
+ @classmethod
81
+ def map_to_memberships_response(cls, data: dict) -> MembershipsResponse:
82
+ count = data.get("count", 0)
83
+ pages = data.get("pages", 0)
84
+ organizations_data = data.get("organizations", [])
85
+ organizations = []
86
+
87
+ for org_data in organizations_data:
88
+ org = OrganizationMembership.model_validate(org_data)
89
+ organizations.append(org)
90
+
91
+ return MembershipsResponse(
92
+ count=count,
93
+ pages=pages,
94
+ organizations=organizations
95
+ )
96
+
97
+ @classmethod
98
+ def map_to_project_memberships_response(cls, data: dict) -> ProjectMembershipsResponse:
99
+ count = data.get("count", 0)
100
+ pages = data.get("pages", 0)
101
+ projects_data = data.get("projects", [])
102
+ projects = []
103
+
104
+ for project_data in projects_data:
105
+ project = ProjectMembership.model_validate(project_data)
106
+ projects.append(project)
107
+
108
+ return ProjectMembershipsResponse(
109
+ count=count,
110
+ pages=pages,
111
+ projects=projects
112
+ )
113
+
114
+ @classmethod
115
+ def map_to_project_roles_response(cls, data: dict) -> ProjectRolesResponse:
116
+ roles_data = data.get("roles", [])
117
+ roles = []
118
+
119
+ for role_data in roles_data:
120
+ role = Role.model_validate(role_data)
121
+ roles.append(role)
122
+
123
+ return ProjectRolesResponse(
124
+ roles=roles
125
+ )
126
+
127
+ @classmethod
128
+ def map_to_project_members_response(cls, data: dict) -> ProjectMembersResponse:
129
+ members_data = data.get("members", [])
130
+ members = []
131
+
132
+ for member_data in members_data:
133
+ member = Member.model_validate(member_data)
134
+ members.append(member)
135
+
136
+ return ProjectMembersResponse(
137
+ members=members
138
+ )
139
+
140
+ @classmethod
141
+ def map_to_organization_members_response(cls, data: dict) -> OrganizationMembersResponse:
142
+ members_data = data.get("members", [])
143
+ members = []
144
+
145
+ for member_data in members_data:
146
+ member = Member.model_validate(member_data)
147
+ members.append(member)
148
+
149
+ return OrganizationMembersResponse(
150
+ members=members
151
+ )
152
+
@@ -1,7 +1,7 @@
1
1
  from pydantic.main import BaseModel
2
2
 
3
3
  from pygeai.core.models import Assistant, Project, ProjectToken, \
4
- RequestItem
4
+ RequestItem, Role, Member, OrganizationMembership, ProjectMembership
5
5
 
6
6
 
7
7
  class AssistantListResponse(BaseModel):
@@ -45,3 +45,27 @@ class ProjectItemListResponse(BaseModel):
45
45
  if self.items is None:
46
46
  self.items = []
47
47
  self.items.append(item)
48
+
49
+
50
+ class MembershipsResponse(BaseModel):
51
+ count: int
52
+ pages: int
53
+ organizations: list[OrganizationMembership]
54
+
55
+
56
+ class ProjectMembershipsResponse(BaseModel):
57
+ count: int
58
+ pages: int
59
+ projects: list[ProjectMembership]
60
+
61
+
62
+ class ProjectRolesResponse(BaseModel):
63
+ roles: list[Role]
64
+
65
+
66
+ class ProjectMembersResponse(BaseModel):
67
+ members: list[Member]
68
+
69
+
70
+ class OrganizationMembersResponse(BaseModel):
71
+ members: list[Member]
pygeai/proxy/clients.py CHANGED
@@ -4,6 +4,8 @@ import uuid
4
4
  import requests
5
5
  from urllib3.exceptions import MaxRetryError
6
6
  from pygeai.proxy.tool import ProxiedTool
7
+ from pygeai.core.utils.validators import validate_status_code
8
+ from pygeai.core.utils.parsers import parse_json_response
7
9
 
8
10
 
9
11
  @dataclass
@@ -150,7 +152,8 @@ class ProxyClient:
150
152
  response = self.session.request(method, url, **kwargs)
151
153
  response.raise_for_status()
152
154
  try:
153
- return response.json()
155
+ validate_status_code(response)
156
+ return parse_json_response(response, "unknown operation")
154
157
  except ValueError:
155
158
  return response.text
156
159
  except requests.exceptions.Timeout:
@@ -3,7 +3,7 @@ from unittest.mock import patch, MagicMock
3
3
  from json import JSONDecodeError
4
4
 
5
5
  from pygeai.admin.clients import AdminClient
6
- from pygeai.core.common.exceptions import InvalidAPIResponseException
6
+ from pygeai.core.common.exceptions import InvalidAPIResponseException, APIResponseError, APIResponseError
7
7
 
8
8
 
9
9
  class TestAdminClient(unittest.TestCase):
@@ -18,6 +18,7 @@ class TestAdminClient(unittest.TestCase):
18
18
  @patch('pygeai.core.services.rest.ApiService.get')
19
19
  def test_validate_api_token_success(self, mock_get):
20
20
  self.mock_response.json.return_value = {"organizationId": "org-123", "projectId": "proj-123"}
21
+ self.mock_response.status_code = 200
21
22
  mock_get.return_value = self.mock_response
22
23
 
23
24
  result = self.client.validate_api_token()
@@ -32,13 +33,14 @@ class TestAdminClient(unittest.TestCase):
32
33
  self.mock_response.text = "Invalid response"
33
34
  mock_get.return_value = self.mock_response
34
35
 
35
- with self.assertRaises(InvalidAPIResponseException) as context:
36
+ with self.assertRaises(APIResponseError) as context:
36
37
  self.client.validate_api_token()
37
- self.assertIn("Unable to validate API token", str(context.exception))
38
+ self.assertIn("API returned an error", str(context.exception)) # "Unable to validate API token", str(context.exception))
38
39
 
39
40
  @patch('pygeai.core.services.rest.ApiService.get')
40
41
  def test_get_authorized_organizations_success(self, mock_get):
41
42
  self.mock_response.json.return_value = {"organizations": ["org1", "org2"]}
43
+ self.mock_response.status_code = 200
42
44
  mock_get.return_value = self.mock_response
43
45
 
44
46
  result = self.client.get_authorized_organizations()
@@ -53,13 +55,14 @@ class TestAdminClient(unittest.TestCase):
53
55
  self.mock_response.text = "Invalid response"
54
56
  mock_get.return_value = self.mock_response
55
57
 
56
- with self.assertRaises(InvalidAPIResponseException) as context:
58
+ with self.assertRaises(APIResponseError) as context:
57
59
  self.client.get_authorized_organizations()
58
- self.assertIn("Unable to retrieve authorized organizations", str(context.exception))
60
+ self.assertIn("API returned an error", str(context.exception)) # "Unable to retrieve authorized organizations", str(context.exception))
59
61
 
60
62
  @patch('pygeai.core.services.rest.ApiService.get')
61
63
  def test_get_authorized_projects_by_organization_success(self, mock_get):
62
64
  self.mock_response.json.return_value = {"projects": ["proj1", "proj2"]}
65
+ self.mock_response.status_code = 200
63
66
  mock_get.return_value = self.mock_response
64
67
 
65
68
  result = self.client.get_authorized_projects_by_organization("org-123")
@@ -76,13 +79,14 @@ class TestAdminClient(unittest.TestCase):
76
79
  self.mock_response.text = "Invalid response"
77
80
  mock_get.return_value = self.mock_response
78
81
 
79
- with self.assertRaises(InvalidAPIResponseException) as context:
82
+ with self.assertRaises(APIResponseError) as context:
80
83
  self.client.get_authorized_projects_by_organization("org-123")
81
- self.assertIn("Unable to retrieve authorized projects for organization", str(context.exception))
84
+ self.assertIn("API returned an error", str(context.exception)) # "Unable to retrieve authorized projects for organization", str(context.exception))
82
85
 
83
86
  @patch('pygeai.core.services.rest.ApiService.get')
84
87
  def test_get_project_visibility_success(self, mock_get):
85
88
  self.mock_response.json.return_value = {}
89
+ self.mock_response.status_code = 200
86
90
  mock_get.return_value = self.mock_response
87
91
 
88
92
  result = self.client.get_project_visibility(
@@ -105,13 +109,14 @@ class TestAdminClient(unittest.TestCase):
105
109
  self.mock_response.text = "Forbidden"
106
110
  mock_get.return_value = self.mock_response
107
111
 
108
- with self.assertRaises(InvalidAPIResponseException) as context:
112
+ with self.assertRaises(APIResponseError) as context:
109
113
  self.client.get_project_visibility("org-123", "proj-456", "token-789")
110
- self.assertIn("Unable to retrieve project visibility", str(context.exception))
114
+ self.assertIn("API returned an error", str(context.exception)) # "Unable to retrieve project visibility", str(context.exception))
111
115
 
112
116
  @patch('pygeai.core.services.rest.ApiService.get')
113
117
  def test_get_project_api_token_success(self, mock_get):
114
118
  self.mock_response.json.return_value = {"apiToken": "api-token-123"}
119
+ self.mock_response.status_code = 200
115
120
  mock_get.return_value = self.mock_response
116
121
 
117
122
  result = self.client.get_project_api_token(
@@ -134,9 +139,9 @@ class TestAdminClient(unittest.TestCase):
134
139
  self.mock_response.text = "Unauthorized"
135
140
  mock_get.return_value = self.mock_response
136
141
 
137
- with self.assertRaises(InvalidAPIResponseException) as context:
142
+ with self.assertRaises(APIResponseError) as context:
138
143
  self.client.get_project_api_token("org-123", "proj-456", "token-789")
139
- self.assertIn("Unable to retrieve project API token", str(context.exception))
144
+ self.assertIn("API returned an error", str(context.exception)) # "Unable to retrieve project API token", str(context.exception))
140
145
 
141
146
 
142
147
  if __name__ == '__main__':