pygeai 0.6.0b13__py3-none-any.whl → 0.6.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (217) 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/content/migration.rst +391 -7
  6. pygeai/_docs/source/pygeai.core.common.rst +8 -0
  7. pygeai/_docs/source/pygeai.tests.auth.rst +56 -0
  8. pygeai/_docs/source/pygeai.tests.cli.rst +8 -0
  9. pygeai/admin/clients.py +1 -3
  10. pygeai/analytics/clients.py +1 -1
  11. pygeai/assistant/clients.py +2 -7
  12. pygeai/assistant/data/clients.py +0 -8
  13. pygeai/assistant/data_analyst/clients.py +0 -2
  14. pygeai/assistant/managers.py +1 -1
  15. pygeai/assistant/rag/clients.py +0 -2
  16. pygeai/assistant/rag/mappers.py +9 -11
  17. pygeai/auth/clients.py +26 -7
  18. pygeai/auth/endpoints.py +2 -1
  19. pygeai/chat/clients.py +2 -2
  20. pygeai/chat/managers.py +1 -1
  21. pygeai/cli/commands/admin.py +13 -25
  22. pygeai/cli/commands/analytics.py +56 -88
  23. pygeai/cli/commands/assistant.py +84 -138
  24. pygeai/cli/commands/auth.py +23 -46
  25. pygeai/cli/commands/base.py +0 -1
  26. pygeai/cli/commands/chat.py +218 -209
  27. pygeai/cli/commands/common.py +5 -5
  28. pygeai/cli/commands/configuration.py +79 -29
  29. pygeai/cli/commands/docs.py +3 -4
  30. pygeai/cli/commands/embeddings.py +13 -19
  31. pygeai/cli/commands/evaluation.py +133 -344
  32. pygeai/cli/commands/feedback.py +7 -15
  33. pygeai/cli/commands/files.py +26 -53
  34. pygeai/cli/commands/gam.py +28 -69
  35. pygeai/cli/commands/lab/ai_lab.py +96 -142
  36. pygeai/cli/commands/lab/common.py +1 -1
  37. pygeai/cli/commands/lab/spec.py +12 -32
  38. pygeai/cli/commands/llm.py +9 -18
  39. pygeai/cli/commands/migrate.py +43 -99
  40. pygeai/cli/commands/organization.py +223 -196
  41. pygeai/cli/commands/rag.py +35 -58
  42. pygeai/cli/commands/rerank.py +21 -25
  43. pygeai/cli/commands/secrets.py +39 -67
  44. pygeai/cli/commands/usage_limits.py +50 -136
  45. pygeai/cli/commands/validators.py +1 -1
  46. pygeai/cli/geai.py +32 -3
  47. pygeai/cli/geai_proxy.py +6 -2
  48. pygeai/cli/install_man.py +1 -1
  49. pygeai/cli/parsers.py +1 -1
  50. pygeai/core/base/clients.py +90 -21
  51. pygeai/core/base/mappers.py +39 -55
  52. pygeai/core/base/session.py +134 -22
  53. pygeai/core/common/config.py +50 -13
  54. pygeai/core/common/constants.py +8 -0
  55. pygeai/core/common/exceptions.py +6 -0
  56. pygeai/core/embeddings/clients.py +0 -1
  57. pygeai/core/embeddings/managers.py +0 -1
  58. pygeai/core/feedback/clients.py +0 -2
  59. pygeai/core/feedback/models.py +1 -1
  60. pygeai/core/files/clients.py +0 -3
  61. pygeai/core/files/managers.py +1 -1
  62. pygeai/core/files/mappers.py +4 -5
  63. pygeai/core/llm/clients.py +0 -1
  64. pygeai/core/models.py +4 -4
  65. pygeai/core/plugins/clients.py +0 -3
  66. pygeai/core/plugins/models.py +2 -2
  67. pygeai/core/rerank/clients.py +0 -2
  68. pygeai/core/secrets/clients.py +0 -2
  69. pygeai/core/services/rest.py +80 -14
  70. pygeai/core/singleton.py +24 -0
  71. pygeai/dbg/__init__.py +2 -2
  72. pygeai/dbg/debugger.py +276 -38
  73. pygeai/evaluation/clients.py +2 -4
  74. pygeai/evaluation/dataset/clients.py +0 -1
  75. pygeai/evaluation/plan/clients.py +0 -2
  76. pygeai/evaluation/result/clients.py +0 -2
  77. pygeai/gam/clients.py +1 -3
  78. pygeai/health/clients.py +1 -3
  79. pygeai/lab/clients.py +0 -1
  80. pygeai/lab/managers.py +0 -1
  81. pygeai/lab/models.py +0 -1
  82. pygeai/lab/strategies/clients.py +1 -2
  83. pygeai/lab/tools/clients.py +2 -2
  84. pygeai/lab/tools/mappers.py +1 -1
  85. pygeai/migration/strategies.py +5 -6
  86. pygeai/migration/tools.py +1 -1
  87. pygeai/organization/clients.py +118 -12
  88. pygeai/organization/endpoints.py +1 -0
  89. pygeai/organization/limits/clients.py +4 -6
  90. pygeai/organization/limits/managers.py +1 -4
  91. pygeai/organization/managers.py +2 -2
  92. pygeai/proxy/config.py +1 -0
  93. pygeai/proxy/managers.py +6 -5
  94. pygeai/tests/admin/test_clients.py +11 -11
  95. pygeai/tests/assistants/rag/test_clients.py +1 -1
  96. pygeai/tests/assistants/rag/test_models.py +1 -2
  97. pygeai/tests/assistants/test_clients.py +1 -1
  98. pygeai/tests/assistants/test_managers.py +1 -3
  99. pygeai/tests/auth/test_cli_configuration.py +252 -0
  100. pygeai/tests/auth/test_client_initialization.py +411 -0
  101. pygeai/tests/auth/test_clients.py +29 -27
  102. pygeai/tests/auth/test_config_manager.py +305 -0
  103. pygeai/tests/auth/test_header_injection.py +294 -0
  104. pygeai/tests/auth/test_oauth.py +3 -1
  105. pygeai/tests/auth/test_session_logging.py +119 -0
  106. pygeai/tests/auth/test_session_validation.py +408 -0
  107. pygeai/tests/auth/test_singleton_reset.py +201 -0
  108. pygeai/tests/chat/test_clients.py +1 -1
  109. pygeai/tests/chat/test_iris.py +1 -1
  110. pygeai/tests/chat/test_ui.py +0 -2
  111. pygeai/tests/cli/commands/lab/test_ai_lab.py +1 -3
  112. pygeai/tests/cli/commands/lab/test_common.py +0 -1
  113. pygeai/tests/cli/commands/test_chat.py +1 -1
  114. pygeai/tests/cli/commands/test_common.py +0 -1
  115. pygeai/tests/cli/commands/test_embeddings.py +2 -2
  116. pygeai/tests/cli/commands/test_evaluation.py +1 -9
  117. pygeai/tests/cli/commands/test_llm.py +1 -1
  118. pygeai/tests/cli/commands/test_migrate.py +1 -1
  119. pygeai/tests/cli/commands/test_rerank.py +0 -1
  120. pygeai/tests/cli/commands/test_secrets.py +1 -1
  121. pygeai/tests/cli/commands/test_show_help.py +0 -1
  122. pygeai/tests/cli/commands/test_validators.py +0 -1
  123. pygeai/tests/cli/test_credentials_flag.py +312 -0
  124. pygeai/tests/cli/test_error_handler.py +0 -1
  125. pygeai/tests/core/base/test_mappers.py +2 -2
  126. pygeai/tests/core/base/test_models.py +4 -4
  127. pygeai/tests/core/common/test_config.py +2 -7
  128. pygeai/tests/core/common/test_decorators.py +0 -1
  129. pygeai/tests/core/embeddings/test_managers.py +1 -1
  130. pygeai/tests/core/feedback/test_clients.py +2 -2
  131. pygeai/tests/core/files/test_clients.py +6 -6
  132. pygeai/tests/core/files/test_models.py +0 -1
  133. pygeai/tests/core/files/test_responses.py +0 -1
  134. pygeai/tests/core/llm/test_clients.py +1 -1
  135. pygeai/tests/core/plugins/test_clients.py +4 -4
  136. pygeai/tests/core/rerank/test_mappers.py +1 -3
  137. pygeai/tests/core/secrets/test_clients.py +2 -3
  138. pygeai/tests/core/services/test_rest.py +10 -10
  139. pygeai/tests/core/utils/test_console.py +0 -1
  140. pygeai/tests/dbg/test_debugger.py +95 -8
  141. pygeai/tests/evaluation/dataset/test_clients.py +24 -27
  142. pygeai/tests/evaluation/plan/test_clients.py +16 -18
  143. pygeai/tests/evaluation/result/test_clients.py +4 -5
  144. pygeai/tests/health/test_clients.py +2 -2
  145. pygeai/tests/integration/lab/agents/test_create_agent.py +1 -3
  146. pygeai/tests/integration/lab/agents/test_get_agent.py +1 -1
  147. pygeai/tests/integration/lab/processes/test_create_process.py +2 -2
  148. pygeai/tests/integration/lab/processes/test_create_task.py +2 -3
  149. pygeai/tests/integration/lab/processes/test_delete_process.py +0 -1
  150. pygeai/tests/integration/lab/processes/test_get_process.py +2 -4
  151. pygeai/tests/integration/lab/processes/test_list_process_instances.py +1 -3
  152. pygeai/tests/integration/lab/processes/test_update_process.py +3 -9
  153. pygeai/tests/integration/lab/reasoning_strategies/test_update_reasoning_strategy.py +1 -2
  154. pygeai/tests/integration/lab/tools/test_delete_tool.py +1 -1
  155. pygeai/tests/integration/lab/tools/test_list_tools.py +1 -1
  156. pygeai/tests/integration/lab/tools/test_update_tool.py +1 -1
  157. pygeai/tests/lab/agents/test_clients.py +17 -17
  158. pygeai/tests/lab/processes/test_clients.py +67 -67
  159. pygeai/tests/lab/processes/test_mappers.py +23 -23
  160. pygeai/tests/lab/spec/test_loader.py +0 -2
  161. pygeai/tests/lab/spec/test_parsers.py +1 -2
  162. pygeai/tests/lab/strategies/test_clients.py +10 -10
  163. pygeai/tests/lab/test_managers.py +3 -5
  164. pygeai/tests/lab/test_mappers.py +1 -4
  165. pygeai/tests/lab/tools/test_clients.py +21 -21
  166. pygeai/tests/lab/tools/test_mappers.py +0 -1
  167. pygeai/tests/organization/limits/test_clients.py +33 -33
  168. pygeai/tests/organization/limits/test_managers.py +7 -7
  169. pygeai/tests/organization/test_clients.py +78 -60
  170. pygeai/tests/proxy/test_clients.py +1 -1
  171. pygeai/tests/proxy/test_integration.py +1 -4
  172. pygeai/tests/proxy/test_managers.py +1 -2
  173. pygeai/tests/proxy/test_servers.py +1 -2
  174. pygeai/tests/snippets/assistants/rag/delete_rag_assistant.py +0 -1
  175. pygeai/tests/snippets/assistants/rag/get_documents.py +0 -1
  176. pygeai/tests/snippets/assistants/rag/get_rag_assistant_data.py +0 -1
  177. pygeai/tests/snippets/chat/get_request_status.py +0 -1
  178. pygeai/tests/snippets/dbg/file_debugging.py +72 -0
  179. pygeai/tests/snippets/dbg/module_debugging.py +60 -0
  180. pygeai/tests/snippets/embeddings/cohere_example.py +2 -2
  181. pygeai/tests/snippets/embeddings/openai_base64_example.py +1 -1
  182. pygeai/tests/snippets/evaluation/dataset/complete_workflow_example.py +8 -8
  183. pygeai/tests/snippets/evaluation/plan/complete_workflow_example.py +5 -5
  184. pygeai/tests/snippets/evaluation/result/complete_workflow_example.py +3 -3
  185. pygeai/tests/snippets/lab/agentic_flow_example_1.py +1 -1
  186. pygeai/tests/snippets/lab/agentic_flow_example_2.py +3 -4
  187. pygeai/tests/snippets/lab/agents/create_agent_with_permissions.py +2 -2
  188. pygeai/tests/snippets/lab/agents/delete_agent.py +1 -2
  189. pygeai/tests/snippets/lab/agents/get_agent.py +1 -1
  190. pygeai/tests/snippets/lab/agents/get_agent_with_new_fields.py +10 -10
  191. pygeai/tests/snippets/lab/agents/get_sharing_link.py +0 -1
  192. pygeai/tests/snippets/lab/agents/list_agents.py +1 -1
  193. pygeai/tests/snippets/lab/agents/publish_agent_revision.py +0 -1
  194. pygeai/tests/snippets/lab/agents/update_agent_properties.py +1 -1
  195. pygeai/tests/snippets/lab/crud_ui.py +3 -5
  196. pygeai/tests/snippets/lab/processes/kbs/get_kb.py +0 -1
  197. pygeai/tests/snippets/lab/processes/kbs/list_kbs.py +0 -1
  198. pygeai/tests/snippets/lab/processes/list_processes.py +1 -1
  199. pygeai/tests/snippets/lab/samples/summarize_files.py +0 -3
  200. pygeai/tests/snippets/lab/strategies/get_reasoning_strategy.py +0 -1
  201. pygeai/tests/snippets/lab/strategies/list_reasoning_strategies.py +1 -1
  202. pygeai/tests/snippets/lab/tools/get_tool.py +1 -1
  203. pygeai/tests/snippets/lab/tools/publish_tool_revision.py +0 -1
  204. pygeai/tests/snippets/lab/tools/set_parameters.py +1 -2
  205. pygeai/tests/snippets/lab/use_cases/c_code_fixer_agent_flow.py +2 -3
  206. pygeai/tests/snippets/lab/use_cases/file_summarizer_example_2.py +1 -1
  207. pygeai/tests/snippets/lab/use_cases/update_cli_expert.py +0 -1
  208. pygeai/tests/snippets/lab/use_cases/update_lab_expert.py +0 -1
  209. pygeai/tests/snippets/lab/use_cases/update_web_designer.py +0 -1
  210. pygeai/tests/snippets/lab/use_cases/update_web_reader.py +0 -1
  211. pygeai/tests/snippets/migrate/orchestrator_examples.py +1 -1
  212. {pygeai-0.6.0b13.dist-info → pygeai-0.6.1.dist-info}/METADATA +32 -7
  213. {pygeai-0.6.0b13.dist-info → pygeai-0.6.1.dist-info}/RECORD +217 -206
  214. {pygeai-0.6.0b13.dist-info → pygeai-0.6.1.dist-info}/WHEEL +0 -0
  215. {pygeai-0.6.0b13.dist-info → pygeai-0.6.1.dist-info}/entry_points.txt +0 -0
  216. {pygeai-0.6.0b13.dist-info → pygeai-0.6.1.dist-info}/licenses/LICENSE +0 -0
  217. {pygeai-0.6.0b13.dist-info → pygeai-0.6.1.dist-info}/top_level.txt +0 -0
@@ -1,13 +1,13 @@
1
1
  import sys
2
+ import warnings
2
3
  from typing import Optional
3
4
 
4
5
  from pygeai import logger
5
6
  from pygeai.core.common.config import get_settings
6
- from pygeai.core.common.exceptions import MissingRequirementException
7
+ from pygeai.core.common.constants import AuthType
8
+ from pygeai.core.common.exceptions import MissingRequirementException, MixedAuthenticationException
7
9
  from pygeai.core.singleton import Singleton
8
10
 
9
- settings = get_settings()
10
-
11
11
 
12
12
  _session = None
13
13
 
@@ -15,14 +15,21 @@ _session = None
15
15
  class Session(metaclass=Singleton):
16
16
  """
17
17
  A session to store configuration state required to interact with different resources.
18
-
19
- :param api_key: str - API key to interact with GEAI
18
+
19
+ Authentication Methods:
20
+ - API Key: Use api_key parameter
21
+ - OAuth 2.0: Use access_token and project_id parameters
22
+
23
+ :param api_key: str - API key to interact with GEAI (mutually exclusive with access_token unless allow_mixed_auth=True)
20
24
  :param base_url: str - Base URL of the GEAI instance
21
25
  :param eval_url: Optional[str] - Optional evaluation endpoint URL
22
- :param access_token: Optional[str] - OAuth access token (keyword-only)
23
- :param project_id: Optional[str] - Project ID for OAuth authentication (keyword-only)
26
+ :param access_token: Optional[str] - OAuth 2.0 access token (keyword-only, requires project_id)
27
+ :param project_id: Optional[str] - Project ID for OAuth authentication (keyword-only, requires access_token)
28
+ :param organization_id: Optional[str] - Organization ID for OAuth authentication (keyword-only)
29
+ :param alias: Optional[str] - Alias name for this session
30
+ :param allow_mixed_auth: bool - If True, allow both api_key and access_token (access_token takes precedence). Default False.
24
31
  :return: Session - Instance of the Session class
25
- :raises: ValueError - If required parameters are missing or invalid
32
+ :raises: ValueError - If authentication configuration is invalid
26
33
  """
27
34
 
28
35
  def __init__(
@@ -33,10 +40,34 @@ class Session(metaclass=Singleton):
33
40
  *,
34
41
  access_token: Optional[str] = None,
35
42
  project_id: Optional[str] = None,
43
+ organization_id: Optional[str] = None,
36
44
  alias: Optional[str] = None,
45
+ allow_mixed_auth: bool = True,
37
46
  ):
47
+ # Validate authentication configuration
48
+ if api_key and access_token and not allow_mixed_auth:
49
+ raise MixedAuthenticationException(
50
+ "Cannot specify both 'api_key' and 'access_token'. "
51
+ "Use 'api_key' for API Key authentication or 'access_token' with 'project_id' for OAuth 2.0. "
52
+ "Set allow_mixed_auth=True to allow both (access_token will take precedence)."
53
+ )
54
+
55
+ if access_token and not project_id:
56
+ raise MissingRequirementException(
57
+ "OAuth 2.0 authentication requires 'project_id'. "
58
+ "Provide project_id when using access_token."
59
+ )
60
+
61
+ if project_id and not access_token:
62
+ warnings.warn(
63
+ "project_id provided without access_token. "
64
+ "project_id is only used with OAuth 2.0 authentication.",
65
+ UserWarning
66
+ )
67
+
38
68
  if not api_key and not access_token:
39
- logger.warning("Cannot instantiate session without api_key or access_token")
69
+ logger.warning("No authentication method configured. API calls may fail.")
70
+
40
71
  if not base_url:
41
72
  logger.warning("Cannot instantiate session without base_url")
42
73
 
@@ -45,18 +76,36 @@ class Session(metaclass=Singleton):
45
76
  self.__eval_url = eval_url
46
77
  self.__access_token = access_token
47
78
  self.__project_id = project_id
79
+ self.__organization_id = organization_id
48
80
  self.__alias = alias if alias else "default"
81
+ self.__auth_type = self._determine_auth_type()
49
82
 
50
83
  global _session
51
84
  _session = self
52
85
 
86
+ def _determine_auth_type(self) -> AuthType:
87
+ """Determine the active authentication type based on configuration."""
88
+ if self.__access_token and self.__project_id:
89
+ return AuthType.OAUTH_TOKEN
90
+ elif self.__api_key:
91
+ return AuthType.API_KEY
92
+ else:
93
+ return AuthType.NONE
94
+
95
+ @property
96
+ def auth_type(self) -> AuthType:
97
+ """Get the current authentication type."""
98
+ return self.__auth_type
99
+
53
100
  @property
54
101
  def api_key(self):
55
102
  return self.__api_key
56
103
 
57
104
  @api_key.setter
58
105
  def api_key(self, api_key: str):
106
+ """Set API key and update auth type."""
59
107
  self.__api_key = api_key
108
+ self.__auth_type = self._determine_auth_type()
60
109
 
61
110
  @property
62
111
  def base_url(self):
@@ -80,7 +129,9 @@ class Session(metaclass=Singleton):
80
129
 
81
130
  @access_token.setter
82
131
  def access_token(self, access_token: str):
132
+ """Set access token and update auth type."""
83
133
  self.__access_token = access_token
134
+ self.__auth_type = self._determine_auth_type()
84
135
 
85
136
  @property
86
137
  def project_id(self):
@@ -88,7 +139,17 @@ class Session(metaclass=Singleton):
88
139
 
89
140
  @project_id.setter
90
141
  def project_id(self, project_id: str):
142
+ """Set project ID and update auth type."""
91
143
  self.__project_id = project_id
144
+ self.__auth_type = self._determine_auth_type()
145
+
146
+ @property
147
+ def organization_id(self):
148
+ return self.__organization_id
149
+
150
+ @organization_id.setter
151
+ def organization_id(self, organization_id: str):
152
+ self.__organization_id = organization_id
92
153
 
93
154
  @property
94
155
  def alias(self):
@@ -98,42 +159,77 @@ class Session(metaclass=Singleton):
98
159
  def alias(self, alias: str):
99
160
  self.__alias = alias
100
161
 
162
+ def is_oauth(self) -> bool:
163
+ """Check if session is using OAuth authentication."""
164
+ return self.__auth_type == AuthType.OAUTH_TOKEN
165
+
166
+ def is_api_key(self) -> bool:
167
+ """Check if session is using API key authentication."""
168
+ return self.__auth_type == AuthType.API_KEY
169
+
170
+ def get_active_token(self) -> Optional[str]:
171
+ """Get the active authentication token based on auth type."""
172
+ if self.is_oauth():
173
+ return self.__access_token
174
+ elif self.is_api_key():
175
+ return self.__api_key
176
+ return None
177
+
101
178
 
102
179
  def get_session(alias: str = None) -> Session:
103
180
  """
104
181
  Session is a singleton object:
105
182
  On the first invocation, returns Session configured with the API KEY and BASE URL corresponding to the
106
183
  alias provided. On the following invocations, it returns the first object instantiated.
184
+
185
+ Loads both API Key and OAuth 2.0 credentials from configuration if available.
107
186
  """
108
187
  try:
188
+ settings = get_settings()
109
189
  global _session
110
190
  if _session is None:
111
191
  if not alias:
112
192
  alias = "default"
113
193
 
114
- _validate_alias(alias)
194
+ _validate_alias(alias, allow_missing_default=True)
195
+
196
+ api_key = settings.get_api_key(alias)
197
+ access_token = settings.get_access_token(alias)
198
+
199
+ # Allow mixed auth for backward compatibility
200
+ # If both are configured, allow mixed (with warning logged in Session)
201
+ allow_mixed = bool(api_key and access_token)
202
+
203
+ if allow_mixed:
204
+ logger.warning(
205
+ f"Both API key and OAuth token configured for alias '{alias}'. "
206
+ "OAuth token will take precedence. Consider using separate aliases."
207
+ )
115
208
 
116
209
  _session = Session(
117
- api_key=settings.get_api_key(alias),
210
+ api_key=api_key,
118
211
  base_url=settings.get_base_url(alias),
119
212
  eval_url=settings.get_eval_url(alias),
120
- access_token=settings.get_access_token(alias),
213
+ access_token=access_token,
121
214
  project_id=settings.get_project_id(alias),
215
+ organization_id=settings.get_organization_id(alias),
122
216
  alias=alias,
217
+ allow_mixed_auth=allow_mixed,
123
218
  )
219
+
124
220
  elif _session is not None and alias:
125
- _validate_alias(alias)
221
+ _validate_alias(alias, allow_missing_default=False)
222
+
223
+ api_key = settings.get_api_key(alias)
224
+ access_token = settings.get_access_token(alias)
126
225
 
127
226
  _session.alias = alias
128
- _session.api_key = settings.get_api_key(alias)
227
+ _session.api_key = api_key
129
228
  _session.base_url = settings.get_base_url(alias)
130
229
  _session.eval_url = settings.get_eval_url(alias)
131
- _session.access_token = settings.get_access_token(alias)
230
+ _session.access_token = access_token
132
231
  _session.project_id = settings.get_project_id(alias)
133
-
134
- if alias:
135
- logger.debug(f"Alias: {alias}")
136
- logger.debug(f"Base URL: {_session.base_url}")
232
+ _session.organization_id = settings.get_organization_id(alias)
137
233
 
138
234
  return _session
139
235
  except ValueError as e:
@@ -141,10 +237,26 @@ def get_session(alias: str = None) -> Session:
141
237
  sys.stdout.write("Warning: API_KEY and/or BASE_URL not set. Please run geai configure to set them up.\n")
142
238
 
143
239
 
144
- def _validate_alias(alias: str):
145
- # Validate alias exists
240
+ def _validate_alias(alias: str, allow_missing_default: bool = False):
241
+ settings = get_settings()
146
242
  available_aliases = settings.list_aliases()
147
243
  if alias not in available_aliases:
244
+ if allow_missing_default and alias == "default":
245
+ return
148
246
  raise MissingRequirementException(
149
247
  f"The profile '{alias}' doesn't exist. Use 'geai configure --list' to see available profiles."
150
- )
248
+ )
249
+
250
+
251
+ def reset_session():
252
+ """
253
+ Reset the session instance. Useful for testing.
254
+
255
+ This clears both the module-level _session variable and the
256
+ Singleton metaclass cache to ensure proper test isolation.
257
+ """
258
+ global _session
259
+ _session = None
260
+
261
+ from pygeai.core.singleton import Singleton
262
+ Singleton.reset_instance(Session)
@@ -1,6 +1,5 @@
1
1
  import os
2
2
  import sys
3
- from functools import lru_cache
4
3
  from pathlib import Path
5
4
  import configparser
6
5
 
@@ -21,9 +20,13 @@ class SettingsManager:
21
20
  GEAI_SETTINGS_DIR = str(SETTINGS_DIR)
22
21
  GEAI_CREDS_FILE = SETTINGS_DIR / "credentials"
23
22
 
24
- def __init__(self):
23
+ def __init__(self, credentials_file: str = None):
25
24
  self.config = configparser.ConfigParser()
26
-
25
+
26
+ if credentials_file:
27
+ self.GEAI_CREDS_FILE = Path(credentials_file)
28
+ logger.debug(f"Using custom credentials file: {self.GEAI_CREDS_FILE}")
29
+
27
30
  if self.GEAI_CREDS_FILE.exists():
28
31
  self.config.read(self.GEAI_CREDS_FILE)
29
32
  else:
@@ -41,14 +44,19 @@ class SettingsManager:
41
44
  return
42
45
 
43
46
  if setting_key not in self.config[alias]:
44
- sys.stdout.write(f"'{setting_key}' not found in alias '{alias}' in the credentials file. Adding empty value.\n")
45
- # SET ADDITIONAL VARS
46
- if setting_key.lower() == "GEAI_API_EVAL_URL".lower():
47
+ logger.debug(f"'{setting_key}' not found in alias '{alias}' in the credentials file.\n")
48
+
49
+ if setting_key.lower() == "geai_api_eval_url" or ("geai_oauth_access_token" in self.config[alias] and setting_key.lower() in ["geai_project_id", "geai_organization_id"]):
50
+ sys.stdout.write(f"'{setting_key}' not found in alias '{alias}' in the credentials file. Adding empty value\n")
51
+
52
+ if setting_key.lower() == "geai_api_eval_url":
47
53
  self.set_eval_url("", alias)
48
- if setting_key.lower() == "GEAI_OAUTH_ACCESS_TOKEN".lower():
49
- self.set_access_token("", alias)
50
- if setting_key.lower() == "GEAI_PROJECT_ID".lower():
51
- self.set_project_id("", alias)
54
+
55
+ if "geai_oauth_access_token" in self.config[alias]:
56
+ if setting_key.lower() == "geai_project_id":
57
+ self.set_project_id("", alias)
58
+ if setting_key.lower() == "geai_organization_id":
59
+ self.set_organization_id("", alias)
52
60
 
53
61
  return self.config[alias].get(setting_key, "")
54
62
 
@@ -112,6 +120,16 @@ class SettingsManager:
112
120
  def set_eval_url(self, eval_url, alias: str = "default"):
113
121
  self.set_setting_value("GEAI_API_EVAL_URL", eval_url, alias)
114
122
 
123
+ def get_organization_id(self, alias: str = "default"):
124
+ organization_id = os.environ.get("GEAI_ORGANIZATION_ID") if not alias or alias == "default" else None
125
+ if not organization_id:
126
+ organization_id = self.get_setting_value("GEAI_ORGANIZATION_ID", alias)
127
+
128
+ return organization_id
129
+
130
+ def set_organization_id(self, organization_id, alias: str = "default"):
131
+ self.set_setting_value("GEAI_ORGANIZATION_ID", organization_id, alias)
132
+
115
133
  def list_aliases(self):
116
134
  """Returns a dict of all aliases and their base URLs."""
117
135
  return {
@@ -130,9 +148,28 @@ class SettingsManager:
130
148
  logger.warning(f"Alias '{alias}' not found in the credentials file.")
131
149
 
132
150
 
133
- @lru_cache()
134
- def get_settings():
135
- return SettingsManager()
151
+ _settings_instance = None
152
+
153
+
154
+ def get_settings(credentials_file: str = None):
155
+ """
156
+ Get or create the SettingsManager instance.
157
+
158
+ :param credentials_file: Optional path to a custom credentials file.
159
+ If provided on first call, uses that file.
160
+ Subsequent calls ignore this parameter.
161
+ :return: SettingsManager instance
162
+ """
163
+ global _settings_instance
164
+ if _settings_instance is None:
165
+ _settings_instance = SettingsManager(credentials_file=credentials_file)
166
+ return _settings_instance
167
+
168
+
169
+ def reset_settings():
170
+ """Reset the settings instance. Useful for testing."""
171
+ global _settings_instance
172
+ _settings_instance = None
136
173
 
137
174
 
138
175
  if __name__ == "__main__":
@@ -0,0 +1,8 @@
1
+ from enum import Enum
2
+
3
+
4
+ class AuthType(Enum):
5
+ """Authentication method types for PyGEAI SDK."""
6
+ API_KEY = "api_key"
7
+ OAUTH_TOKEN = "oauth_token"
8
+ NONE = "none"
@@ -122,3 +122,9 @@ class APIResponseError(GEAIException):
122
122
  """Raised when there is an error in the API response"""
123
123
  pass
124
124
 
125
+
126
+ class MixedAuthenticationException(GEAIException):
127
+ """Raised when both API token and Oauth2 authentication are setup for the same profile"""
128
+ pass
129
+
130
+
@@ -1,7 +1,6 @@
1
1
 
2
2
  from pygeai import logger
3
3
  from pygeai.core.base.clients import BaseClient
4
- from pygeai.core.common.exceptions import InvalidAPIResponseException, APIResponseError
5
4
  from pygeai.core.embeddings.endpoints import GENERATE_EMBEDDINGS
6
5
  from pygeai.core.utils.validators import validate_status_code
7
6
  from pygeai.core.utils.parsers import parse_json_response
@@ -1,5 +1,4 @@
1
1
  from pygeai import logger
2
- from pygeai.core.base.mappers import ErrorMapper
3
2
  from pygeai.core.common.exceptions import APIError
4
3
  from pygeai.core.embeddings.clients import EmbeddingsClient
5
4
  from pygeai.core.embeddings.mappers import EmbeddingsResponseMapper
@@ -1,8 +1,6 @@
1
- from typing import Any, Union
2
1
 
3
2
  from pygeai import logger
4
3
  from pygeai.core.base.clients import BaseClient
5
- from pygeai.core.common.exceptions import InvalidAPIResponseException
6
4
  from pygeai.core.feedback.endpoints import SEND_FEEDBACK_V1
7
5
  from pygeai.core.utils.validators import validate_status_code
8
6
  from pygeai.core.utils.parsers import parse_json_response
@@ -1,4 +1,4 @@
1
- from pydantic import BaseModel, Field
1
+ from pydantic import Field
2
2
 
3
3
  from pygeai.core import CustomBaseModel
4
4
 
@@ -1,10 +1,7 @@
1
- import json
2
- from json import JSONDecodeError
3
1
  from pathlib import Path
4
2
 
5
3
  from pygeai import logger
6
4
  from pygeai.core.base.clients import BaseClient
7
- from pygeai.core.common.exceptions import InvalidAPIResponseException
8
5
  from pygeai.core.files.endpoints import UPLOAD_FILE_V1, GET_FILE_V1, DELETE_FILE_V1, GET_FILE_CONTENT_V1, \
9
6
  GET_ALL_FILES_V1
10
7
  from pygeai.core.utils.validators import validate_status_code
@@ -4,7 +4,7 @@ from typing import Optional
4
4
 
5
5
  from pygeai import logger
6
6
  from pygeai.admin.clients import AdminClient
7
- from pygeai.core.base.mappers import ErrorMapper, ResponseMapper
7
+ from pygeai.core.base.mappers import ResponseMapper
8
8
  from pygeai.core.base.responses import EmptyResponse
9
9
  from pygeai.core.files.clients import FileClient
10
10
  from pygeai.core.files.models import UploadFile, File, FileList
@@ -22,14 +22,13 @@ class FileResponseMapper:
22
22
 
23
23
  @classmethod
24
24
  def map_to_file_list(cls, data: dict) -> list[File]:
25
- file_list = list()
26
25
  files = data.get('dataFiles')
26
+
27
27
  if files is not None and any(files):
28
- for file_data in files:
29
- file = cls.map_to_file(file_data)
30
- file_list.append(file)
31
28
 
32
- return file_list
29
+ return [cls.map_to_file(file_data) for file_data in files]
30
+
31
+ return []
33
32
 
34
33
  @classmethod
35
34
  def map_to_file(cls, data: dict) -> File:
@@ -1,7 +1,6 @@
1
1
 
2
2
  from pygeai import logger
3
3
  from pygeai.core.base.clients import BaseClient
4
- from pygeai.core.common.exceptions import InvalidAPIResponseException
5
4
  from pygeai.core.llm.endpoints import GET_PROVIDER_LIST_V2, GET_PROVIDER_DATA_V2, GET_PROVIDER_MODELS_V2, \
6
5
  GET_MODEL_DATA_V2
7
6
  from pygeai.core.utils.validators import validate_status_code
pygeai/core/models.py CHANGED
@@ -185,8 +185,8 @@ class UsageLimit(CustomBaseModel):
185
185
  "subscriptionType": "string", // Subscription type (Freemium, Daily, Weekly, Monthly)
186
186
  "usageUnit": "string", // Usage unit (Requests, Cost)
187
187
  "usedAmount": "number", // Amount used (decimal or scientific notation)
188
- "validFrom": "timestamp", // Start date of the usage limit
189
- "validUntil": "timestamp" // Expiration or renewal date
188
+ "validFrom": "str", // Start date of the usage limit
189
+ "validUntil": "str" // Expiration or renewal date
190
190
  """
191
191
  hard_limit: Optional[float] = Field(None, alias="hardLimit")
192
192
  id: Optional[str] = Field(None, alias="id")
@@ -198,8 +198,8 @@ class UsageLimit(CustomBaseModel):
198
198
  subscription_type: Optional[Literal["Freemium", "Daily", "Weekly", "Monthly"]] = Field(None, alias="subscriptionType")
199
199
  usage_unit: Optional[Literal["Requests", "Cost"]] = Field(None, alias="usageUnit")
200
200
  used_amount: Optional[float] = Field(None, alias="usedAmount")
201
- valid_from: Optional[datetime] = Field(None, alias="validFrom")
202
- valid_until: Optional[datetime] = Field(None, alias="validUntil")
201
+ valid_from: Optional[str] = Field(None, alias="validFrom")
202
+ valid_until: Optional[str] = Field(None, alias="validUntil")
203
203
 
204
204
  def to_dict(self):
205
205
  return self.model_dump(by_alias=True, exclude_none=True)
@@ -1,8 +1,5 @@
1
- from typing import Optional, List, Dict
2
1
 
3
- from pygeai import logger
4
2
  from pygeai.core.base.clients import BaseClient
5
- from pygeai.core.common.exceptions import InvalidAPIResponseException
6
3
  from pygeai.core.plugins.endpoints import LIST_ASSISTANTS_PLUGINS_V1
7
4
  from pygeai.core.utils.validators import validate_status_code
8
5
  from pygeai.core.utils.parsers import parse_json_response
@@ -1,5 +1,5 @@
1
- from pydantic import BaseModel, Field
2
- from typing import Optional, List, Literal
1
+ from pydantic import Field
2
+ from typing import Optional, List
3
3
  from uuid import UUID
4
4
 
5
5
  from pygeai.core import CustomBaseModel
@@ -1,8 +1,6 @@
1
- from typing import Any, Union
2
1
 
3
2
  from pygeai import logger
4
3
  from pygeai.core.base.clients import BaseClient
5
- from pygeai.core.common.exceptions import InvalidAPIResponseException
6
4
  from pygeai.core.rerank.endpoints import RERANK_V1
7
5
  from pygeai.core.utils.validators import validate_status_code
8
6
  from pygeai.core.utils.parsers import parse_json_response
@@ -1,8 +1,6 @@
1
1
  from typing import Optional, List, Dict
2
2
 
3
- from pygeai import logger
4
3
  from pygeai.core.base.clients import BaseClient
5
- from pygeai.core.common.exceptions import InvalidAPIResponseException
6
4
  from pygeai.core.secrets.endpoints import LIST_SECRETS_V1, GET_SECRET_V1, CREATE_SECRET_V1, UPDATE_SECRET_V1, \
7
5
  SET_SECRET_ACCESSES_V1, GET_SECRET_ACCESSES_V1
8
6
  from pygeai.core.utils.validators import validate_status_code
@@ -1,5 +1,5 @@
1
1
  import json
2
- from typing import Optional, List, Dict
2
+ from typing import Optional, Dict
3
3
  import requests as req
4
4
  from pygeai import logger
5
5
  from pygeai.core.common.exceptions import InvalidResponseException
@@ -21,16 +21,13 @@ class ApiService:
21
21
  :param username: str - Username for basic authentication (optional).
22
22
  :param password: str - Password for basic authentication (optional).
23
23
  :param token: str - Bearer token for authentication (optional).
24
- :param project_id: str - Project ID for OAuth authentication (optional, keyword-only).
25
24
  """
26
25
 
27
- def __init__(self, base_url, username: str = None, password: str = None, token: str = None, *,
28
- project_id: str = None):
26
+ def __init__(self, base_url, username: str = None, password: str = None, token: str = None):
29
27
  self._base_url = base_url
30
28
  self._username = username
31
29
  self._password = password
32
30
  self._token = token
33
- self._project_id = project_id
34
31
 
35
32
  @property
36
33
  def base_url(self):
@@ -52,14 +49,6 @@ class ApiService:
52
49
  def token(self, token: str):
53
50
  self._token = token
54
51
 
55
- @property
56
- def project_id(self):
57
- return self._project_id
58
-
59
- @project_id.setter
60
- def project_id(self, project_id: str):
61
- self._project_id = project_id
62
-
63
52
  def get(self, endpoint: str, params: dict = None, headers: dict = None, verify: bool = True):
64
53
  """
65
54
  Sends a GET request to the specified API endpoint.
@@ -418,7 +407,84 @@ class ApiService:
418
407
  if "Authorization" not in headers:
419
408
  headers["Authorization"] = f"Bearer {self.token}"
420
409
 
421
- if self.project_id and "ProjectId" not in headers:
410
+ return headers
411
+
412
+
413
+ class GEAIApiService(ApiService):
414
+ """
415
+ Service for interacting with REST APIs in Globant Enterprise AI.
416
+
417
+ :param base_url: str - The base URL of the API.
418
+ :param username: str - Username for basic authentication (optional).
419
+ :param password: str - Password for basic authentication (optional).
420
+ :param token: str - Bearer token for authentication (optional).
421
+ :param project_id: str - Project ID for OAuth authentication (optional, keyword-only).
422
+ :param organization_id: str - Organization ID for OAuth authentication (optional, keyword-only).
423
+ """
424
+
425
+ def __init__(self, base_url, username: str = None, password: str = None, token: str = None, *,
426
+ project_id: str = None, organization_id: str = None):
427
+ super().__init__(base_url, username, password, token)
428
+ self._project_id = project_id
429
+ self._organization_id = organization_id
430
+
431
+ @property
432
+ def project_id(self):
433
+ return self._project_id
434
+
435
+ @project_id.setter
436
+ def project_id(self, project_id: str):
437
+ self._project_id = project_id
438
+
439
+ @property
440
+ def organization_id(self):
441
+ return self._organization_id
442
+
443
+ @organization_id.setter
444
+ def organization_id(self, organization_id: str):
445
+ self._organization_id = organization_id
446
+
447
+ def get(self, endpoint: str, params: dict = None, headers: dict = None, verify: bool = True):
448
+ headers = self._add_oauth_context_to_headers(headers=headers)
449
+ return super().get(endpoint, params, headers, verify)
450
+
451
+ def post(self, endpoint: str, data: dict, headers: dict = None, verify: bool = True, form: bool = False):
452
+ headers = self._add_oauth_context_to_headers(headers=headers)
453
+ return super().post(endpoint, data, headers, verify, form)
454
+
455
+ def stream_post(self, endpoint: str, data: dict, headers: dict = None, verify: bool = True, form: bool = False):
456
+ headers = self._add_oauth_context_to_headers(headers=headers)
457
+ return super().stream_post(endpoint, data, headers, verify, form)
458
+
459
+ def post_file_binary( self, endpoint: str, headers: dict = None, verify: bool = True, file=None):
460
+ headers = self._add_oauth_context_to_headers(headers=headers)
461
+ return super().post_file_binary(endpoint, headers, verify, file)
462
+
463
+ def post_files_multipart(self, endpoint: str, data: Optional[dict] = None, headers: Optional[dict] = None, verify: bool = True, files: Optional[Dict[str, str]] = None):
464
+ headers = self._add_oauth_context_to_headers(headers=headers)
465
+ return super().post_files_multipart(endpoint, data, headers, verify, files)
466
+
467
+ def put(self, endpoint: str, data: dict, headers: dict = None, verify: bool = True):
468
+ headers = self._add_oauth_context_to_headers(headers=headers)
469
+ return super().put(endpoint, data, headers, verify)
470
+
471
+ def delete(self, endpoint: str, headers: dict = None, data: dict = None, verify: bool = True):
472
+ headers = self._add_oauth_context_to_headers(headers=headers)
473
+ return super().delete(endpoint, headers, data, verify)
474
+
475
+ def _add_oauth_context_to_headers(self, headers: dict = None):
476
+ headers = headers if headers is not None else {}
477
+
478
+ # Add OAuth context headers if available
479
+ if self.project_id and "ProjectId" not in headers and "project-id" not in headers:
422
480
  headers["ProjectId"] = self.project_id
481
+ logger.debug(f"Added Authorization header with ProjectId: {self.project_id}")
482
+
483
+ if self.organization_id and "OrganizationId" not in headers and "organization-id" not in headers:
484
+ headers["OrganizationId"] = self.organization_id
485
+ logger.debug(f"Added OrganizationId header: {self.organization_id}")
486
+
487
+ if not self.project_id and not self.organization_id:
488
+ logger.debug("Added Authorization header")
423
489
 
424
490
  return headers