pygeai 0.6.0b14__py3-none-any.whl → 0.7.0b1__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 (216) hide show
  1. pygeai/__init__.py +1 -2
  2. pygeai/_docs/source/content/api_reference/project.rst +392 -0
  3. pygeai/_docs/source/content/authentication.rst +130 -1
  4. pygeai/_docs/source/content/debugger.rst +327 -157
  5. pygeai/_docs/source/pygeai.core.common.rst +8 -0
  6. pygeai/_docs/source/pygeai.tests.auth.rst +56 -0
  7. pygeai/_docs/source/pygeai.tests.cli.rst +8 -0
  8. pygeai/admin/clients.py +1 -3
  9. pygeai/analytics/clients.py +1 -1
  10. pygeai/assistant/clients.py +2 -7
  11. pygeai/assistant/data/clients.py +0 -8
  12. pygeai/assistant/data_analyst/clients.py +0 -2
  13. pygeai/assistant/managers.py +1 -1
  14. pygeai/assistant/rag/clients.py +0 -2
  15. pygeai/assistant/rag/mappers.py +9 -11
  16. pygeai/auth/clients.py +26 -7
  17. pygeai/auth/endpoints.py +2 -1
  18. pygeai/chat/clients.py +2 -2
  19. pygeai/chat/managers.py +1 -1
  20. pygeai/cli/commands/admin.py +13 -25
  21. pygeai/cli/commands/analytics.py +31 -71
  22. pygeai/cli/commands/assistant.py +84 -138
  23. pygeai/cli/commands/auth.py +23 -46
  24. pygeai/cli/commands/base.py +0 -1
  25. pygeai/cli/commands/chat.py +218 -209
  26. pygeai/cli/commands/common.py +5 -5
  27. pygeai/cli/commands/configuration.py +79 -29
  28. pygeai/cli/commands/docs.py +3 -4
  29. pygeai/cli/commands/embeddings.py +13 -19
  30. pygeai/cli/commands/evaluation.py +133 -344
  31. pygeai/cli/commands/feedback.py +7 -15
  32. pygeai/cli/commands/files.py +26 -53
  33. pygeai/cli/commands/gam.py +28 -69
  34. pygeai/cli/commands/lab/ai_lab.py +96 -142
  35. pygeai/cli/commands/lab/common.py +1 -1
  36. pygeai/cli/commands/lab/spec.py +12 -32
  37. pygeai/cli/commands/llm.py +9 -18
  38. pygeai/cli/commands/migrate.py +43 -99
  39. pygeai/cli/commands/organization.py +223 -196
  40. pygeai/cli/commands/rag.py +35 -58
  41. pygeai/cli/commands/rerank.py +21 -25
  42. pygeai/cli/commands/secrets.py +39 -67
  43. pygeai/cli/commands/usage_limits.py +50 -136
  44. pygeai/cli/commands/validators.py +1 -1
  45. pygeai/cli/geai.py +32 -3
  46. pygeai/cli/geai_proxy.py +6 -2
  47. pygeai/cli/install_man.py +1 -1
  48. pygeai/cli/parsers.py +1 -1
  49. pygeai/core/base/clients.py +90 -21
  50. pygeai/core/base/mappers.py +39 -55
  51. pygeai/core/base/session.py +129 -18
  52. pygeai/core/common/config.py +50 -13
  53. pygeai/core/common/constants.py +8 -0
  54. pygeai/core/common/exceptions.py +6 -0
  55. pygeai/core/embeddings/clients.py +0 -1
  56. pygeai/core/embeddings/managers.py +0 -1
  57. pygeai/core/feedback/clients.py +0 -2
  58. pygeai/core/feedback/models.py +1 -1
  59. pygeai/core/files/clients.py +0 -3
  60. pygeai/core/files/managers.py +1 -1
  61. pygeai/core/files/mappers.py +4 -5
  62. pygeai/core/llm/clients.py +0 -1
  63. pygeai/core/models.py +4 -4
  64. pygeai/core/plugins/clients.py +0 -3
  65. pygeai/core/plugins/models.py +2 -2
  66. pygeai/core/rerank/clients.py +0 -2
  67. pygeai/core/secrets/clients.py +0 -2
  68. pygeai/core/services/rest.py +80 -14
  69. pygeai/core/singleton.py +24 -0
  70. pygeai/dbg/__init__.py +2 -2
  71. pygeai/dbg/debugger.py +276 -38
  72. pygeai/evaluation/clients.py +2 -4
  73. pygeai/evaluation/dataset/clients.py +0 -1
  74. pygeai/evaluation/plan/clients.py +0 -2
  75. pygeai/evaluation/result/clients.py +0 -2
  76. pygeai/gam/clients.py +1 -3
  77. pygeai/health/clients.py +1 -3
  78. pygeai/lab/clients.py +0 -1
  79. pygeai/lab/managers.py +0 -1
  80. pygeai/lab/models.py +0 -1
  81. pygeai/lab/strategies/clients.py +1 -2
  82. pygeai/lab/tools/clients.py +2 -2
  83. pygeai/lab/tools/mappers.py +1 -1
  84. pygeai/migration/strategies.py +5 -6
  85. pygeai/migration/tools.py +1 -1
  86. pygeai/organization/clients.py +118 -12
  87. pygeai/organization/endpoints.py +1 -0
  88. pygeai/organization/limits/clients.py +4 -6
  89. pygeai/organization/limits/managers.py +1 -4
  90. pygeai/organization/managers.py +2 -2
  91. pygeai/proxy/config.py +1 -0
  92. pygeai/proxy/managers.py +6 -5
  93. pygeai/tests/admin/test_clients.py +11 -11
  94. pygeai/tests/assistants/rag/test_clients.py +1 -1
  95. pygeai/tests/assistants/rag/test_models.py +1 -2
  96. pygeai/tests/assistants/test_clients.py +1 -1
  97. pygeai/tests/assistants/test_managers.py +1 -3
  98. pygeai/tests/auth/test_cli_configuration.py +252 -0
  99. pygeai/tests/auth/test_client_initialization.py +411 -0
  100. pygeai/tests/auth/test_clients.py +29 -27
  101. pygeai/tests/auth/test_config_manager.py +305 -0
  102. pygeai/tests/auth/test_header_injection.py +294 -0
  103. pygeai/tests/auth/test_oauth.py +3 -1
  104. pygeai/tests/auth/test_session_logging.py +119 -0
  105. pygeai/tests/auth/test_session_validation.py +408 -0
  106. pygeai/tests/auth/test_singleton_reset.py +201 -0
  107. pygeai/tests/chat/test_clients.py +1 -1
  108. pygeai/tests/chat/test_iris.py +1 -1
  109. pygeai/tests/chat/test_ui.py +0 -2
  110. pygeai/tests/cli/commands/lab/test_ai_lab.py +1 -3
  111. pygeai/tests/cli/commands/lab/test_common.py +0 -1
  112. pygeai/tests/cli/commands/test_chat.py +1 -1
  113. pygeai/tests/cli/commands/test_common.py +0 -1
  114. pygeai/tests/cli/commands/test_embeddings.py +2 -2
  115. pygeai/tests/cli/commands/test_evaluation.py +1 -9
  116. pygeai/tests/cli/commands/test_llm.py +1 -1
  117. pygeai/tests/cli/commands/test_migrate.py +1 -1
  118. pygeai/tests/cli/commands/test_rerank.py +0 -1
  119. pygeai/tests/cli/commands/test_secrets.py +1 -1
  120. pygeai/tests/cli/commands/test_show_help.py +0 -1
  121. pygeai/tests/cli/commands/test_validators.py +0 -1
  122. pygeai/tests/cli/test_credentials_flag.py +312 -0
  123. pygeai/tests/cli/test_error_handler.py +0 -1
  124. pygeai/tests/core/base/test_mappers.py +2 -2
  125. pygeai/tests/core/base/test_models.py +4 -4
  126. pygeai/tests/core/common/test_config.py +2 -7
  127. pygeai/tests/core/common/test_decorators.py +0 -1
  128. pygeai/tests/core/embeddings/test_managers.py +1 -1
  129. pygeai/tests/core/feedback/test_clients.py +2 -2
  130. pygeai/tests/core/files/test_clients.py +6 -6
  131. pygeai/tests/core/files/test_models.py +0 -1
  132. pygeai/tests/core/files/test_responses.py +0 -1
  133. pygeai/tests/core/llm/test_clients.py +1 -1
  134. pygeai/tests/core/plugins/test_clients.py +4 -4
  135. pygeai/tests/core/rerank/test_mappers.py +1 -3
  136. pygeai/tests/core/secrets/test_clients.py +2 -3
  137. pygeai/tests/core/services/test_rest.py +10 -10
  138. pygeai/tests/core/utils/test_console.py +0 -1
  139. pygeai/tests/dbg/test_debugger.py +95 -8
  140. pygeai/tests/evaluation/dataset/test_clients.py +24 -27
  141. pygeai/tests/evaluation/plan/test_clients.py +16 -18
  142. pygeai/tests/evaluation/result/test_clients.py +4 -5
  143. pygeai/tests/health/test_clients.py +2 -2
  144. pygeai/tests/integration/lab/agents/test_create_agent.py +1 -3
  145. pygeai/tests/integration/lab/agents/test_get_agent.py +1 -1
  146. pygeai/tests/integration/lab/processes/test_create_process.py +2 -2
  147. pygeai/tests/integration/lab/processes/test_create_task.py +2 -3
  148. pygeai/tests/integration/lab/processes/test_delete_process.py +0 -1
  149. pygeai/tests/integration/lab/processes/test_get_process.py +2 -4
  150. pygeai/tests/integration/lab/processes/test_list_process_instances.py +1 -3
  151. pygeai/tests/integration/lab/processes/test_update_process.py +3 -9
  152. pygeai/tests/integration/lab/reasoning_strategies/test_update_reasoning_strategy.py +1 -2
  153. pygeai/tests/integration/lab/tools/test_delete_tool.py +1 -1
  154. pygeai/tests/integration/lab/tools/test_list_tools.py +1 -1
  155. pygeai/tests/integration/lab/tools/test_update_tool.py +1 -1
  156. pygeai/tests/lab/agents/test_clients.py +17 -17
  157. pygeai/tests/lab/processes/test_clients.py +67 -67
  158. pygeai/tests/lab/processes/test_mappers.py +23 -23
  159. pygeai/tests/lab/spec/test_loader.py +0 -2
  160. pygeai/tests/lab/spec/test_parsers.py +1 -2
  161. pygeai/tests/lab/strategies/test_clients.py +10 -10
  162. pygeai/tests/lab/test_managers.py +3 -5
  163. pygeai/tests/lab/test_mappers.py +1 -4
  164. pygeai/tests/lab/tools/test_clients.py +21 -21
  165. pygeai/tests/lab/tools/test_mappers.py +0 -1
  166. pygeai/tests/organization/limits/test_clients.py +33 -33
  167. pygeai/tests/organization/limits/test_managers.py +7 -7
  168. pygeai/tests/organization/test_clients.py +78 -60
  169. pygeai/tests/proxy/test_clients.py +1 -1
  170. pygeai/tests/proxy/test_integration.py +1 -4
  171. pygeai/tests/proxy/test_managers.py +1 -2
  172. pygeai/tests/proxy/test_servers.py +1 -2
  173. pygeai/tests/snippets/assistants/rag/delete_rag_assistant.py +0 -1
  174. pygeai/tests/snippets/assistants/rag/get_documents.py +0 -1
  175. pygeai/tests/snippets/assistants/rag/get_rag_assistant_data.py +0 -1
  176. pygeai/tests/snippets/chat/get_request_status.py +0 -1
  177. pygeai/tests/snippets/dbg/file_debugging.py +72 -0
  178. pygeai/tests/snippets/dbg/module_debugging.py +60 -0
  179. pygeai/tests/snippets/embeddings/cohere_example.py +2 -2
  180. pygeai/tests/snippets/embeddings/openai_base64_example.py +1 -1
  181. pygeai/tests/snippets/evaluation/dataset/complete_workflow_example.py +8 -8
  182. pygeai/tests/snippets/evaluation/plan/complete_workflow_example.py +5 -5
  183. pygeai/tests/snippets/evaluation/result/complete_workflow_example.py +3 -3
  184. pygeai/tests/snippets/lab/agentic_flow_example_1.py +1 -1
  185. pygeai/tests/snippets/lab/agentic_flow_example_2.py +3 -4
  186. pygeai/tests/snippets/lab/agents/create_agent_with_permissions.py +2 -2
  187. pygeai/tests/snippets/lab/agents/delete_agent.py +1 -2
  188. pygeai/tests/snippets/lab/agents/get_agent.py +1 -1
  189. pygeai/tests/snippets/lab/agents/get_agent_with_new_fields.py +10 -10
  190. pygeai/tests/snippets/lab/agents/get_sharing_link.py +0 -1
  191. pygeai/tests/snippets/lab/agents/list_agents.py +1 -1
  192. pygeai/tests/snippets/lab/agents/publish_agent_revision.py +0 -1
  193. pygeai/tests/snippets/lab/agents/update_agent_properties.py +1 -1
  194. pygeai/tests/snippets/lab/crud_ui.py +3 -5
  195. pygeai/tests/snippets/lab/processes/kbs/get_kb.py +0 -1
  196. pygeai/tests/snippets/lab/processes/kbs/list_kbs.py +0 -1
  197. pygeai/tests/snippets/lab/processes/list_processes.py +1 -1
  198. pygeai/tests/snippets/lab/samples/summarize_files.py +0 -3
  199. pygeai/tests/snippets/lab/strategies/get_reasoning_strategy.py +0 -1
  200. pygeai/tests/snippets/lab/strategies/list_reasoning_strategies.py +1 -1
  201. pygeai/tests/snippets/lab/tools/get_tool.py +1 -1
  202. pygeai/tests/snippets/lab/tools/publish_tool_revision.py +0 -1
  203. pygeai/tests/snippets/lab/tools/set_parameters.py +1 -2
  204. pygeai/tests/snippets/lab/use_cases/c_code_fixer_agent_flow.py +2 -3
  205. pygeai/tests/snippets/lab/use_cases/file_summarizer_example_2.py +1 -1
  206. pygeai/tests/snippets/lab/use_cases/update_cli_expert.py +0 -1
  207. pygeai/tests/snippets/lab/use_cases/update_lab_expert.py +0 -1
  208. pygeai/tests/snippets/lab/use_cases/update_web_designer.py +0 -1
  209. pygeai/tests/snippets/lab/use_cases/update_web_reader.py +0 -1
  210. pygeai/tests/snippets/migrate/orchestrator_examples.py +1 -1
  211. {pygeai-0.6.0b14.dist-info → pygeai-0.7.0b1.dist-info}/METADATA +32 -7
  212. {pygeai-0.6.0b14.dist-info → pygeai-0.7.0b1.dist-info}/RECORD +216 -205
  213. {pygeai-0.6.0b14.dist-info → pygeai-0.7.0b1.dist-info}/WHEEL +0 -0
  214. {pygeai-0.6.0b14.dist-info → pygeai-0.7.0b1.dist-info}/entry_points.txt +0 -0
  215. {pygeai-0.6.0b14.dist-info → pygeai-0.7.0b1.dist-info}/licenses/LICENSE +0 -0
  216. {pygeai-0.6.0b14.dist-info → pygeai-0.7.0b1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,294 @@
1
+ import unittest
2
+ from unittest.mock import patch, MagicMock
3
+
4
+ from pygeai.core.base.clients import BaseClient
5
+ from pygeai.core.services.rest import GEAIApiService
6
+
7
+
8
+ class TestHeaderInjection(unittest.TestCase):
9
+ """
10
+ Tests for proper header injection in API requests.
11
+
12
+ python -m unittest pygeai.tests.auth.test_header_injection.TestHeaderInjection
13
+ """
14
+
15
+ def test_api_key_auth_header_format(self):
16
+ """Test that API key is formatted as Bearer token"""
17
+ client = BaseClient(
18
+ api_key="test_api_key_123",
19
+ base_url="https://api.test.com"
20
+ )
21
+
22
+ headers = client.api_service._add_token_to_headers({})
23
+
24
+ self.assertEqual(headers['Authorization'], 'Bearer test_api_key_123')
25
+
26
+ def test_oauth_auth_header_format(self):
27
+ """Test that OAuth access token is formatted as Bearer token"""
28
+ client = BaseClient(
29
+ base_url="https://api.test.com",
30
+ access_token="oauth_access_token_123",
31
+ project_id="project-456"
32
+ )
33
+
34
+ headers = client.api_service._add_token_to_headers({})
35
+
36
+ self.assertEqual(headers['Authorization'], 'Bearer oauth_access_token_123')
37
+
38
+ def test_oauth_project_id_header_added(self):
39
+ """Test that ProjectId header is added for OAuth"""
40
+ client = BaseClient(
41
+ base_url="https://api.test.com",
42
+ access_token="oauth_token_123",
43
+ project_id="project-456"
44
+ )
45
+
46
+ headers = client.api_service._add_oauth_context_to_headers({})
47
+
48
+ self.assertEqual(headers['ProjectId'], 'project-456')
49
+
50
+ def test_oauth_organization_id_header_added(self):
51
+ """Test that OrganizationId header is added when provided"""
52
+ client = BaseClient(
53
+ base_url="https://api.test.com",
54
+ access_token="oauth_token_123",
55
+ project_id="project-456",
56
+ organization_id="org-789"
57
+ )
58
+
59
+ headers = client.api_service._add_oauth_context_to_headers({})
60
+
61
+ self.assertEqual(headers['ProjectId'], 'project-456')
62
+ self.assertEqual(headers['OrganizationId'], 'org-789')
63
+
64
+ def test_api_key_no_project_id_header(self):
65
+ """Test that ProjectId header is not added for API key auth without project_id"""
66
+ client = BaseClient(
67
+ api_key="api_key_123",
68
+ base_url="https://api.test.com"
69
+ )
70
+
71
+ headers = client.api_service._add_token_to_headers({})
72
+ headers = client.api_service._add_oauth_context_to_headers(headers)
73
+
74
+ self.assertNotIn('ProjectId', headers)
75
+ self.assertNotIn('OrganizationId', headers)
76
+
77
+ def test_headers_preserved_when_adding_auth(self):
78
+ """Test that existing headers are preserved when adding auth headers"""
79
+ client = BaseClient(
80
+ base_url="https://api.test.com",
81
+ access_token="oauth_token_123",
82
+ project_id="project-456"
83
+ )
84
+
85
+ existing_headers = {
86
+ 'Content-Type': 'application/json',
87
+ 'X-Custom-Header': 'custom_value'
88
+ }
89
+
90
+ headers = client.api_service._add_token_to_headers(existing_headers.copy())
91
+ headers = client.api_service._add_oauth_context_to_headers(headers)
92
+
93
+ self.assertEqual(headers['Authorization'], 'Bearer oauth_token_123')
94
+ self.assertEqual(headers['ProjectId'], 'project-456')
95
+ self.assertEqual(headers['Content-Type'], 'application/json')
96
+ self.assertEqual(headers['X-Custom-Header'], 'custom_value')
97
+
98
+ def test_authorization_header_not_duplicated(self):
99
+ """Test that Authorization header is not duplicated if already present"""
100
+ service = GEAIApiService(
101
+ base_url="https://api.test.com",
102
+ token="test_token_123"
103
+ )
104
+
105
+ existing_headers = {'Authorization': 'Bearer existing_token'}
106
+
107
+ headers = service._add_token_to_headers(existing_headers.copy())
108
+
109
+ self.assertEqual(headers['Authorization'], 'Bearer existing_token')
110
+
111
+ def test_project_id_header_case_sensitivity(self):
112
+ """Test that ProjectId header checks for both case variations"""
113
+ service = GEAIApiService(
114
+ base_url="https://api.test.com",
115
+ token="test_token",
116
+ project_id="project-123"
117
+ )
118
+
119
+ headers_lower = {'project-id': 'existing-project'}
120
+ result_lower = service._add_oauth_context_to_headers(headers_lower.copy())
121
+
122
+ self.assertEqual(result_lower['project-id'], 'existing-project')
123
+ self.assertNotIn('ProjectId', result_lower)
124
+
125
+ def test_project_id_header_capital_case(self):
126
+ """Test that ProjectId header is not duplicated with capital case"""
127
+ service = GEAIApiService(
128
+ base_url="https://api.test.com",
129
+ token="test_token",
130
+ project_id="project-123"
131
+ )
132
+
133
+ headers_capital = {'ProjectId': 'existing-project'}
134
+ result_capital = service._add_oauth_context_to_headers(headers_capital.copy())
135
+
136
+ self.assertEqual(result_capital['ProjectId'], 'existing-project')
137
+
138
+ def test_organization_id_header_case_sensitivity(self):
139
+ """Test that OrganizationId header checks for both case variations"""
140
+ service = GEAIApiService(
141
+ base_url="https://api.test.com",
142
+ token="test_token",
143
+ project_id="project-123",
144
+ organization_id="org-456"
145
+ )
146
+
147
+ headers_lower = {'organization-id': 'existing-org'}
148
+ result_lower = service._add_oauth_context_to_headers(headers_lower.copy())
149
+
150
+ self.assertEqual(result_lower['organization-id'], 'existing-org')
151
+ self.assertNotIn('OrganizationId', result_lower)
152
+
153
+ def test_empty_headers_dict_created_if_none(self):
154
+ """Test that empty headers dict is created if None is passed"""
155
+ service = GEAIApiService(
156
+ base_url="https://api.test.com",
157
+ token="test_token",
158
+ project_id="project-123"
159
+ )
160
+
161
+ headers = service._add_oauth_context_to_headers(None)
162
+
163
+ self.assertIsInstance(headers, dict)
164
+ self.assertEqual(headers['ProjectId'], 'project-123')
165
+
166
+ def test_no_project_id_no_header_added(self):
167
+ """Test that no ProjectId header is added when project_id is None"""
168
+ service = GEAIApiService(
169
+ base_url="https://api.test.com",
170
+ token="test_token"
171
+ )
172
+
173
+ headers = service._add_oauth_context_to_headers({})
174
+
175
+ self.assertNotIn('ProjectId', headers)
176
+ self.assertNotIn('project-id', headers)
177
+
178
+ def test_api_key_with_project_id_adds_header(self):
179
+ """Test that ProjectId header is added even with API key auth if project_id is set"""
180
+ client = BaseClient(
181
+ api_key="api_key_123",
182
+ base_url="https://api.test.com",
183
+ project_id="project-456"
184
+ )
185
+
186
+ headers = client.api_service._add_oauth_context_to_headers({})
187
+
188
+ self.assertEqual(headers['ProjectId'], 'project-456')
189
+
190
+
191
+ class TestGEAIApiServiceHeaderPropagation(unittest.TestCase):
192
+ """
193
+ Tests that headers are properly propagated in all HTTP methods.
194
+
195
+ python -m unittest pygeai.tests.auth.test_header_injection.TestGEAIApiServiceHeaderPropagation
196
+ """
197
+
198
+ @patch('pygeai.core.services.rest.req.Session')
199
+ def test_get_request_includes_oauth_headers(self, mock_session_class):
200
+ """Test that GET requests include OAuth headers"""
201
+ mock_session = MagicMock()
202
+ mock_response = MagicMock()
203
+ mock_response.url = "https://api.test.com/endpoint"
204
+ mock_session.get.return_value = mock_response
205
+ mock_session_class.return_value.__enter__.return_value = mock_session
206
+
207
+ service = GEAIApiService(
208
+ base_url="https://api.test.com",
209
+ token="test_token",
210
+ project_id="project-123",
211
+ organization_id="org-456"
212
+ )
213
+
214
+ service.get("endpoint")
215
+
216
+ call_args = mock_session.get.call_args
217
+ headers = call_args[1]['headers']
218
+
219
+ self.assertIn('Authorization', headers)
220
+ self.assertEqual(headers['ProjectId'], 'project-123')
221
+ self.assertEqual(headers['OrganizationId'], 'org-456')
222
+
223
+ @patch('pygeai.core.services.rest.req.Session')
224
+ def test_post_request_includes_oauth_headers(self, mock_session_class):
225
+ """Test that POST requests include OAuth headers"""
226
+ mock_session = MagicMock()
227
+ mock_response = MagicMock()
228
+ mock_response.url = "https://api.test.com/endpoint"
229
+ mock_session.post.return_value = mock_response
230
+ mock_session_class.return_value.__enter__.return_value = mock_session
231
+
232
+ service = GEAIApiService(
233
+ base_url="https://api.test.com",
234
+ token="test_token",
235
+ project_id="project-123"
236
+ )
237
+
238
+ service.post("endpoint", data={"key": "value"})
239
+
240
+ call_args = mock_session.post.call_args
241
+ headers = call_args[1]['headers']
242
+
243
+ self.assertIn('Authorization', headers)
244
+ self.assertEqual(headers['ProjectId'], 'project-123')
245
+
246
+ @patch('pygeai.core.services.rest.req.Session')
247
+ def test_put_request_includes_oauth_headers(self, mock_session_class):
248
+ """Test that PUT requests include OAuth headers"""
249
+ mock_session = MagicMock()
250
+ mock_response = MagicMock()
251
+ mock_response.url = "https://api.test.com/endpoint"
252
+ mock_session.put.return_value = mock_response
253
+ mock_session_class.return_value.__enter__.return_value = mock_session
254
+
255
+ service = GEAIApiService(
256
+ base_url="https://api.test.com",
257
+ token="test_token",
258
+ project_id="project-123"
259
+ )
260
+
261
+ service.put("endpoint", data={"key": "value"})
262
+
263
+ call_args = mock_session.put.call_args
264
+ headers = call_args[1]['headers']
265
+
266
+ self.assertIn('Authorization', headers)
267
+ self.assertEqual(headers['ProjectId'], 'project-123')
268
+
269
+ @patch('pygeai.core.services.rest.req.Session')
270
+ def test_delete_request_includes_oauth_headers(self, mock_session_class):
271
+ """Test that DELETE requests include OAuth headers"""
272
+ mock_session = MagicMock()
273
+ mock_response = MagicMock()
274
+ mock_response.url = "https://api.test.com/endpoint"
275
+ mock_session.delete.return_value = mock_response
276
+ mock_session_class.return_value.__enter__.return_value = mock_session
277
+
278
+ service = GEAIApiService(
279
+ base_url="https://api.test.com",
280
+ token="test_token",
281
+ project_id="project-123"
282
+ )
283
+
284
+ service.delete("endpoint")
285
+
286
+ call_args = mock_session.delete.call_args
287
+ headers = call_args[1]['headers']
288
+
289
+ self.assertIn('Authorization', headers)
290
+ self.assertEqual(headers['ProjectId'], 'project-123')
291
+
292
+
293
+ if __name__ == '__main__':
294
+ unittest.main()
@@ -1,5 +1,4 @@
1
1
  import unittest
2
- from unittest.mock import patch, MagicMock
3
2
 
4
3
  from pygeai.lab.clients import AILabClient
5
4
  from pygeai.evaluation.clients import EvaluationClient
@@ -55,6 +54,7 @@ class TestOAuthAuthentication(unittest.TestCase):
55
54
  )
56
55
 
57
56
  headers = client.api_service._add_token_to_headers({})
57
+ headers = client.api_service._add_oauth_context_to_headers(headers)
58
58
 
59
59
  self.assertEqual(headers.get('Authorization'), 'Bearer oauth_token_123')
60
60
  self.assertEqual(headers.get('ProjectId'), 'project-456')
@@ -81,6 +81,7 @@ class TestOAuthAuthentication(unittest.TestCase):
81
81
 
82
82
  existing_headers = {'Content-Type': 'application/json', 'Custom-Header': 'value'}
83
83
  headers = client.api_service._add_token_to_headers(existing_headers.copy())
84
+ headers = client.api_service._add_oauth_context_to_headers(headers)
84
85
 
85
86
  self.assertEqual(headers.get('Authorization'), 'Bearer oauth_token_123')
86
87
  self.assertEqual(headers.get('ProjectId'), 'project-456')
@@ -164,6 +165,7 @@ class TestOAuthAuthentication(unittest.TestCase):
164
165
  self.assertEqual(client.api_service.project_id, "project-wins")
165
166
 
166
167
  headers = client.api_service._add_token_to_headers({})
168
+ headers = client.api_service._add_oauth_context_to_headers(headers)
167
169
  self.assertEqual(headers['Authorization'], 'Bearer oauth_token_wins')
168
170
  self.assertEqual(headers['ProjectId'], 'project-wins')
169
171
 
@@ -0,0 +1,119 @@
1
+ import unittest
2
+ import logging
3
+ import io
4
+ import tempfile
5
+ import os
6
+
7
+ from pygeai.core.base.session import reset_session, get_session
8
+ from pygeai.core.common.config import reset_settings, get_settings
9
+ from pygeai.chat.clients import ChatClient
10
+ from pygeai.core.llm.clients import LlmClient
11
+ from pygeai.core.embeddings.clients import EmbeddingsClient
12
+
13
+
14
+ class TestSessionAuthenticationLogging(unittest.TestCase):
15
+ """
16
+ Test that authentication method logging happens only when sessions are created or aliases change,
17
+ not every time a client is instantiated.
18
+ """
19
+
20
+ def setUp(self):
21
+ """Set up test fixtures"""
22
+ self._original_geai_logger_handlers = logging.getLogger('geai').handlers.copy()
23
+ self._original_geai_logger_level = logging.getLogger('geai').level
24
+
25
+ # Set up log capture
26
+ self.log_capture = io.StringIO()
27
+ self.handler = logging.StreamHandler(self.log_capture)
28
+ self.handler.setLevel(logging.INFO)
29
+
30
+ # Configure logger
31
+ geai_logger = logging.getLogger('geai')
32
+ geai_logger.handlers = []
33
+ geai_logger.addHandler(self.handler)
34
+ geai_logger.setLevel(logging.INFO)
35
+ geai_logger.propagate = False
36
+
37
+ reset_settings()
38
+ reset_session()
39
+
40
+ def tearDown(self):
41
+ """Clean up test fixtures"""
42
+ # Restore logger
43
+ geai_logger = logging.getLogger('geai')
44
+ geai_logger.handlers = self._original_geai_logger_handlers
45
+ geai_logger.level = self._original_geai_logger_level
46
+
47
+ # Reset BaseClient logging flag
48
+ from pygeai.core.base.clients import BaseClient
49
+ BaseClient._logged_session_config = None
50
+
51
+ reset_settings()
52
+ reset_session()
53
+
54
+ def test_authentication_logged_once_for_multiple_clients(self):
55
+ """Test that authentication is logged only once when creating multiple clients"""
56
+ # Create 5 different clients
57
+ ChatClient()
58
+ LlmClient()
59
+ EmbeddingsClient()
60
+ ChatClient()
61
+ LlmClient()
62
+
63
+ # Get log output
64
+ log_output = self.log_capture.getvalue()
65
+
66
+ # Count authentication log messages
67
+ auth_count = log_output.count('Using API Key authentication') + \
68
+ log_output.count('Using OAuth 2.0 authentication')
69
+
70
+ self.assertEqual(auth_count, 1,
71
+ f"Expected 1 authentication log, got {auth_count}. Log:\n{log_output}")
72
+
73
+ def test_authentication_logged_when_alias_changes(self):
74
+ """Test that authentication is logged when switching between aliases"""
75
+ # Create credentials file with multiple aliases
76
+ with tempfile.NamedTemporaryFile(mode='w', suffix='_credentials', delete=False) as f:
77
+ creds_file = f.name
78
+ f.write('[default]\n')
79
+ f.write('GEAI_API_KEY = default_key\n')
80
+ f.write('GEAI_API_BASE_URL = https://default.example.com\n')
81
+ f.write('\n')
82
+ f.write('[staging]\n')
83
+ f.write('GEAI_API_KEY = staging_key\n')
84
+ f.write('GEAI_API_BASE_URL = https://staging.example.com\n')
85
+ f.write('GEAI_OAUTH_ACCESS_TOKEN = staging_token\n')
86
+ f.write('GEAI_PROJECT_ID = staging_project\n')
87
+
88
+ try:
89
+ reset_settings()
90
+ reset_session()
91
+ get_settings(credentials_file=creds_file)
92
+
93
+ # Use default alias
94
+ get_session('default')
95
+ ChatClient()
96
+ ChatClient()
97
+
98
+ # Switch to staging alias
99
+ get_session('staging')
100
+ ChatClient()
101
+ ChatClient()
102
+
103
+ # Get log output
104
+ log_output = self.log_capture.getvalue()
105
+
106
+ # Should have logged API Key once for default, OAuth once for staging
107
+ api_key_count = log_output.count('Using API Key authentication')
108
+ oauth_count = log_output.count('Using OAuth 2.0 authentication')
109
+
110
+ self.assertEqual(api_key_count, 1,
111
+ f"Expected 1 API Key log, got {api_key_count}")
112
+ self.assertEqual(oauth_count, 1,
113
+ f"Expected 1 OAuth log, got {oauth_count}")
114
+ finally:
115
+ os.unlink(creds_file)
116
+
117
+
118
+ if __name__ == '__main__':
119
+ unittest.main()