iatoolkit 0.4.2__py3-none-any.whl → 0.66.2__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 (123) hide show
  1. iatoolkit/__init__.py +13 -35
  2. iatoolkit/base_company.py +74 -8
  3. iatoolkit/cli_commands.py +15 -23
  4. iatoolkit/common/__init__.py +0 -0
  5. iatoolkit/common/exceptions.py +46 -0
  6. iatoolkit/common/routes.py +141 -0
  7. iatoolkit/common/session_manager.py +24 -0
  8. iatoolkit/common/util.py +348 -0
  9. iatoolkit/company_registry.py +7 -8
  10. iatoolkit/iatoolkit.py +169 -96
  11. iatoolkit/infra/__init__.py +5 -0
  12. iatoolkit/infra/call_service.py +140 -0
  13. iatoolkit/infra/connectors/__init__.py +5 -0
  14. iatoolkit/infra/connectors/file_connector.py +17 -0
  15. iatoolkit/infra/connectors/file_connector_factory.py +57 -0
  16. iatoolkit/infra/connectors/google_cloud_storage_connector.py +53 -0
  17. iatoolkit/infra/connectors/google_drive_connector.py +68 -0
  18. iatoolkit/infra/connectors/local_file_connector.py +46 -0
  19. iatoolkit/infra/connectors/s3_connector.py +33 -0
  20. iatoolkit/infra/gemini_adapter.py +356 -0
  21. iatoolkit/infra/google_chat_app.py +57 -0
  22. iatoolkit/infra/llm_client.py +429 -0
  23. iatoolkit/infra/llm_proxy.py +139 -0
  24. iatoolkit/infra/llm_response.py +40 -0
  25. iatoolkit/infra/mail_app.py +145 -0
  26. iatoolkit/infra/openai_adapter.py +90 -0
  27. iatoolkit/infra/redis_session_manager.py +122 -0
  28. iatoolkit/locales/en.yaml +144 -0
  29. iatoolkit/locales/es.yaml +140 -0
  30. iatoolkit/repositories/__init__.py +5 -0
  31. iatoolkit/repositories/database_manager.py +110 -0
  32. iatoolkit/repositories/document_repo.py +33 -0
  33. iatoolkit/repositories/llm_query_repo.py +91 -0
  34. iatoolkit/repositories/models.py +336 -0
  35. iatoolkit/repositories/profile_repo.py +123 -0
  36. iatoolkit/repositories/tasks_repo.py +52 -0
  37. iatoolkit/repositories/vs_repo.py +139 -0
  38. iatoolkit/services/__init__.py +5 -0
  39. iatoolkit/services/auth_service.py +193 -0
  40. {services → iatoolkit/services}/benchmark_service.py +6 -6
  41. iatoolkit/services/branding_service.py +149 -0
  42. {services → iatoolkit/services}/dispatcher_service.py +39 -99
  43. {services → iatoolkit/services}/document_service.py +5 -5
  44. {services → iatoolkit/services}/excel_service.py +27 -21
  45. {services → iatoolkit/services}/file_processor_service.py +5 -5
  46. iatoolkit/services/help_content_service.py +30 -0
  47. {services → iatoolkit/services}/history_service.py +8 -16
  48. iatoolkit/services/i18n_service.py +104 -0
  49. {services → iatoolkit/services}/jwt_service.py +18 -27
  50. iatoolkit/services/language_service.py +77 -0
  51. {services → iatoolkit/services}/load_documents_service.py +19 -14
  52. {services → iatoolkit/services}/mail_service.py +5 -5
  53. iatoolkit/services/onboarding_service.py +43 -0
  54. {services → iatoolkit/services}/profile_service.py +155 -89
  55. {services → iatoolkit/services}/prompt_manager_service.py +26 -11
  56. {services → iatoolkit/services}/query_service.py +142 -104
  57. {services → iatoolkit/services}/search_service.py +21 -5
  58. {services → iatoolkit/services}/sql_service.py +24 -6
  59. {services → iatoolkit/services}/tasks_service.py +10 -10
  60. iatoolkit/services/user_feedback_service.py +103 -0
  61. iatoolkit/services/user_session_context_service.py +143 -0
  62. iatoolkit/static/images/fernando.jpeg +0 -0
  63. iatoolkit/static/js/chat_feedback_button.js +80 -0
  64. iatoolkit/static/js/chat_filepond.js +85 -0
  65. iatoolkit/static/js/chat_help_content.js +124 -0
  66. iatoolkit/static/js/chat_history_button.js +112 -0
  67. iatoolkit/static/js/chat_logout_button.js +36 -0
  68. iatoolkit/static/js/chat_main.js +364 -0
  69. iatoolkit/static/js/chat_onboarding_button.js +97 -0
  70. iatoolkit/static/js/chat_prompt_manager.js +94 -0
  71. iatoolkit/static/js/chat_reload_button.js +35 -0
  72. iatoolkit/static/styles/chat_iatoolkit.css +592 -0
  73. iatoolkit/static/styles/chat_modal.css +169 -0
  74. iatoolkit/static/styles/chat_public.css +107 -0
  75. iatoolkit/static/styles/landing_page.css +182 -0
  76. iatoolkit/static/styles/llm_output.css +115 -0
  77. iatoolkit/static/styles/onboarding.css +169 -0
  78. iatoolkit/system_prompts/query_main.prompt +5 -15
  79. iatoolkit/templates/_company_header.html +20 -0
  80. iatoolkit/templates/_login_widget.html +42 -0
  81. iatoolkit/templates/about.html +13 -0
  82. iatoolkit/templates/base.html +65 -0
  83. iatoolkit/templates/change_password.html +66 -0
  84. iatoolkit/templates/chat.html +287 -0
  85. iatoolkit/templates/chat_modals.html +181 -0
  86. iatoolkit/templates/error.html +51 -0
  87. iatoolkit/templates/forgot_password.html +50 -0
  88. iatoolkit/templates/index.html +145 -0
  89. iatoolkit/templates/login_simulation.html +34 -0
  90. iatoolkit/templates/onboarding_shell.html +104 -0
  91. iatoolkit/templates/signup.html +76 -0
  92. iatoolkit/views/__init__.py +5 -0
  93. iatoolkit/views/base_login_view.py +92 -0
  94. iatoolkit/views/change_password_view.py +117 -0
  95. iatoolkit/views/external_login_view.py +73 -0
  96. iatoolkit/views/file_store_api_view.py +65 -0
  97. iatoolkit/views/forgot_password_view.py +72 -0
  98. iatoolkit/views/help_content_api_view.py +54 -0
  99. iatoolkit/views/history_api_view.py +56 -0
  100. iatoolkit/views/home_view.py +61 -0
  101. iatoolkit/views/index_view.py +14 -0
  102. iatoolkit/views/init_context_api_view.py +73 -0
  103. iatoolkit/views/llmquery_api_view.py +57 -0
  104. iatoolkit/views/login_simulation_view.py +81 -0
  105. iatoolkit/views/login_view.py +153 -0
  106. iatoolkit/views/logout_api_view.py +49 -0
  107. iatoolkit/views/profile_api_view.py +46 -0
  108. iatoolkit/views/prompt_api_view.py +37 -0
  109. iatoolkit/views/signup_view.py +94 -0
  110. iatoolkit/views/tasks_api_view.py +72 -0
  111. iatoolkit/views/tasks_review_api_view.py +55 -0
  112. iatoolkit/views/user_feedback_api_view.py +60 -0
  113. iatoolkit/views/verify_user_view.py +62 -0
  114. {iatoolkit-0.4.2.dist-info → iatoolkit-0.66.2.dist-info}/METADATA +2 -2
  115. iatoolkit-0.66.2.dist-info/RECORD +119 -0
  116. {iatoolkit-0.4.2.dist-info → iatoolkit-0.66.2.dist-info}/top_level.txt +0 -1
  117. iatoolkit/system_prompts/arquitectura.prompt +0 -32
  118. iatoolkit-0.4.2.dist-info/RECORD +0 -32
  119. services/__init__.py +0 -5
  120. services/api_service.py +0 -75
  121. services/user_feedback_service.py +0 -67
  122. services/user_session_context_service.py +0 -85
  123. {iatoolkit-0.4.2.dist-info → iatoolkit-0.66.2.dist-info}/WHEEL +0 -0
iatoolkit/iatoolkit.py CHANGED
@@ -1,25 +1,25 @@
1
1
  # Copyright (c) 2024 Fernando Libedinsky
2
- # Producto: IAToolkit Core
3
- # Framework opensource para chatbots empresariales con IA
2
+ # Product: IAToolkit
3
+ #
4
+ # IAToolkit is open source software.
4
5
 
5
- from flask import Flask, url_for, current_app
6
+ from flask import Flask, url_for, get_flashed_messages
6
7
  from flask_session import Session
7
8
  from flask_injector import FlaskInjector
8
9
  from flask_bcrypt import Bcrypt
9
10
  from flask_cors import CORS
10
- from common.auth import IAuthentication
11
- from common.util import Utility
12
- from common.exceptions import IAToolkitException
13
- from common.session_manager import SessionManager
11
+ from iatoolkit.common.exceptions import IAToolkitException
14
12
  from urllib.parse import urlparse
15
13
  import redis
16
14
  import logging
17
15
  import os
18
16
  from typing import Optional, Dict, Any
19
- from repositories.database_manager import DatabaseManager
20
- from injector import Binder, singleton, Injector
17
+ from iatoolkit.repositories.database_manager import DatabaseManager
18
+ from werkzeug.middleware.proxy_fix import ProxyFix
19
+ from injector import Binder, Injector, singleton
21
20
  from importlib.metadata import version as _pkg_version, PackageNotFoundError
22
21
 
22
+ IATOOLKIT_VERSION = "0.66.2"
23
23
 
24
24
  # global variable for the unique instance of IAToolkit
25
25
  _iatoolkit_instance: Optional['IAToolkit'] = None
@@ -52,13 +52,11 @@ class IAToolkit:
52
52
  self.app = None
53
53
  self.db_manager = None
54
54
  self._injector = None
55
- self.version = "0.0.0+dev"
55
+ self.version = IATOOLKIT_VERSION # default version
56
56
 
57
57
  @classmethod
58
58
  def get_instance(cls) -> 'IAToolkit':
59
- """
60
- Obtiene la instancia única de IAToolkit
61
- """
59
+ # get the global IAToolkit instance
62
60
  global _iatoolkit_instance
63
61
  if _iatoolkit_instance is None:
64
62
  _iatoolkit_instance = cls()
@@ -90,29 +88,48 @@ class IAToolkit:
90
88
  # and other integrations, as views are handled manually.
91
89
  FlaskInjector(app=self.app, injector=self._injector)
92
90
 
93
- # Step 6: Finalize setup within the application context
91
+ # Step 6: initialize dispatcher and registered companies
92
+ self._init_dispatcher_and_company_instances()
93
+
94
+ # Step 7: Finalize setup within the application context
94
95
  self._setup_redis_sessions()
95
96
  self._setup_cors()
96
97
  self._setup_additional_services()
97
98
  self._setup_cli_commands()
99
+ self._setup_request_globals()
98
100
  self._setup_context_processors()
99
101
 
100
- try:
101
- self.version = _pkg_version("iatoolkit")
102
- except PackageNotFoundError:
103
- pass
102
+ # Step 8: define the download_dir for excel's
103
+ self._setup_download_dir()
104
104
 
105
105
  logging.info(f"🎉 IAToolkit v{self.version} inicializado correctamente")
106
106
  self._initialized = True
107
107
  return self.app
108
108
 
109
109
  def _get_config_value(self, key: str, default=None):
110
- """Obtiene un valor de configuración, primero del dict config, luego de env vars"""
110
+ # get a value from the config dict or the environment variable
111
111
  return self.config.get(key, os.getenv(key, default))
112
112
 
113
+ def _setup_request_globals(self):
114
+ """
115
+ Configures functions to run before each request to set up
116
+ request-global variables, such as language.
117
+ """
118
+ injector = self._injector
119
+
120
+ @self.app.before_request
121
+ def set_request_language():
122
+ """
123
+ Determines and caches the language for the current request in g.lang.
124
+ """
125
+ from iatoolkit.services.language_service import LanguageService
126
+ language_service = injector.get(LanguageService)
127
+ language_service.get_current_language()
128
+
113
129
  def _setup_logging(self):
114
- log_level_str = self._get_config_value('FLASK_ENV', 'production')
115
- log_level = logging.INFO if log_level_str in ('dev', 'development') else logging.WARNING
130
+ # Lee el nivel de log desde una variable de entorno, con 'INFO' como valor por defecto.
131
+ log_level_name = os.getenv('LOG_LEVEL', 'INFO').upper()
132
+ log_level = getattr(logging, log_level_name, logging.INFO)
116
133
 
117
134
  logging.basicConfig(
118
135
  level=log_level,
@@ -123,7 +140,7 @@ class IAToolkit:
123
140
 
124
141
  def _register_routes(self):
125
142
  """Registers routes by passing the configured injector."""
126
- from common.routes import register_views
143
+ from iatoolkit.common.routes import register_views
127
144
 
128
145
  # Pass the injector to the view registration function
129
146
  register_views(self._injector, self.app)
@@ -141,11 +158,21 @@ class IAToolkit:
141
158
  is_https = self._get_config_value('USE_HTTPS', 'false').lower() == 'true'
142
159
  is_dev = self._get_config_value('FLASK_ENV') == 'development'
143
160
 
161
+ # get the iatoolkit domain
162
+ parsed_url = urlparse(os.getenv('IATOOLKIT_BASE_URL'))
163
+ domain = parsed_url.netloc
164
+
165
+ try:
166
+ self.version = _pkg_version("iatoolkit")
167
+ except PackageNotFoundError:
168
+ pass
169
+
170
+
144
171
  self.app.config.update({
145
172
  'VERSION': self.version,
146
173
  'SECRET_KEY': self._get_config_value('FLASK_SECRET_KEY', 'iatoolkit-default-secret'),
147
- 'SESSION_COOKIE_SAMESITE': "None" if is_https else "Lax",
148
- 'SESSION_COOKIE_SECURE': is_https,
174
+ 'SESSION_COOKIE_SAMESITE': "None",
175
+ 'SESSION_COOKIE_SECURE': True,
149
176
  'SESSION_PERMANENT': False,
150
177
  'SESSION_USE_SIGNER': True,
151
178
  'JWT_SECRET_KEY': self._get_config_value('JWT_SECRET_KEY', 'iatoolkit-jwt-secret'),
@@ -153,6 +180,12 @@ class IAToolkit:
153
180
  'JWT_EXPIRATION_SECONDS_CHAT': int(self._get_config_value('JWT_EXPIRATION_SECONDS_CHAT', 3600))
154
181
  })
155
182
 
183
+ if parsed_url.scheme == 'https':
184
+ self.app.config['PREFERRED_URL_SCHEME'] = 'https'
185
+
186
+ # 2. ProxyFix para no tener problemas con iframes y rutas
187
+ self.app.wsgi_app = ProxyFix(self.app.wsgi_app, x_proto=1)
188
+
156
189
  # Configuración para tokenizers en desarrollo
157
190
  if is_dev:
158
191
  os.environ["TOKENIZERS_PARALLELISM"] = "false"
@@ -169,6 +202,15 @@ class IAToolkit:
169
202
  self.db_manager.create_all()
170
203
  logging.info("✅ Base de datos configurada correctamente")
171
204
 
205
+ @self.app.teardown_appcontext
206
+ def remove_session(exception=None):
207
+ """
208
+ Flask calls this after each request.
209
+ It ensures the SQLAlchemy session is properly closed
210
+ and the DB connection is returned to the pool.
211
+ """
212
+ self.db_manager.scoped_session.remove()
213
+
172
214
  def _setup_redis_sessions(self):
173
215
  redis_url = self._get_config_value('REDIS_URL')
174
216
  if not redis_url:
@@ -199,19 +241,19 @@ class IAToolkit:
199
241
 
200
242
  def _setup_cors(self):
201
243
  """🌐 Configura CORS"""
202
- # Origins por defecto para desarrollo
244
+ from iatoolkit.company_registry import get_company_registry
245
+
246
+ # default CORS origin
203
247
  default_origins = [
204
- "http://localhost:5001",
205
- "http://127.0.0.1:5001",
206
248
  os.getenv('IATOOLKIT_BASE_URL')
207
249
  ]
208
250
 
209
- # Obtener origins adicionales desde configuración/env
251
+ # Iterate through the registered company names
210
252
  extra_origins = []
211
- for i in range(1, 11): # Soporte para CORS_ORIGIN_1 a CORS_ORIGIN_10
212
- origin = self._get_config_value(f'CORS_ORIGIN_{i}')
213
- if origin:
214
- extra_origins.append(origin)
253
+ all_company_instances = get_company_registry().get_all_company_instances()
254
+ for company_name, company_instance in all_company_instances.items():
255
+ cors_origin = company_instance.company.parameters.get('cors_origin', [])
256
+ extra_origins += cors_origin
215
257
 
216
258
  all_origins = default_origins + extra_origins
217
259
 
@@ -226,19 +268,17 @@ class IAToolkit:
226
268
 
227
269
  logging.info(f"✅ CORS configurado para: {all_origins}")
228
270
 
229
-
230
271
  def _configure_core_dependencies(self, binder: Binder):
231
272
  """⚙️ Configures all system dependencies."""
232
273
  try:
233
274
  # Core dependencies
234
- binder.bind(Flask, to=self.app, scope=singleton)
275
+ binder.bind(Flask, to=self.app)
235
276
  binder.bind(DatabaseManager, to=self.db_manager, scope=singleton)
236
277
 
237
278
  # Bind all application components by calling the specific methods
238
279
  self._bind_repositories(binder)
239
280
  self._bind_services(binder)
240
281
  self._bind_infrastructure(binder)
241
- self._bind_views(binder)
242
282
 
243
283
  logging.info("✅ Dependencias configuradas correctamente")
244
284
 
@@ -250,11 +290,12 @@ class IAToolkit:
250
290
  )
251
291
 
252
292
  def _bind_repositories(self, binder: Binder):
253
- from repositories.document_repo import DocumentRepo
254
- from repositories.profile_repo import ProfileRepo
255
- from repositories.llm_query_repo import LLMQueryRepo
256
- from repositories.vs_repo import VSRepo
257
- from repositories.tasks_repo import TaskRepo
293
+ from iatoolkit.repositories.document_repo import DocumentRepo
294
+ from iatoolkit.repositories.profile_repo import ProfileRepo
295
+ from iatoolkit.repositories.llm_query_repo import LLMQueryRepo
296
+
297
+ from iatoolkit.repositories.vs_repo import VSRepo
298
+ from iatoolkit.repositories.tasks_repo import TaskRepo
258
299
 
259
300
  binder.bind(DocumentRepo, to=DocumentRepo)
260
301
  binder.bind(ProfileRepo, to=ProfileRepo)
@@ -263,17 +304,20 @@ class IAToolkit:
263
304
  binder.bind(TaskRepo, to=TaskRepo)
264
305
 
265
306
  def _bind_services(self, binder: Binder):
266
- from services.query_service import QueryService
267
- from services.tasks_service import TaskService
268
- from services.benchmark_service import BenchmarkService
269
- from services.document_service import DocumentService
270
- from services.prompt_manager_service import PromptService
271
- from services.excel_service import ExcelService
272
- from services.mail_service import MailService
273
- from services.load_documents_service import LoadDocumentsService
274
- from services.profile_service import ProfileService
275
- from services.jwt_service import JWTService
276
- from services.dispatcher_service import Dispatcher
307
+ from iatoolkit.services.query_service import QueryService
308
+ from iatoolkit.services.tasks_service import TaskService
309
+ from iatoolkit.services.benchmark_service import BenchmarkService
310
+ from iatoolkit.services.document_service import DocumentService
311
+ from iatoolkit.services.prompt_manager_service import PromptService
312
+ from iatoolkit.services.excel_service import ExcelService
313
+ from iatoolkit.services.mail_service import MailService
314
+ from iatoolkit.services.load_documents_service import LoadDocumentsService
315
+ from iatoolkit.services.profile_service import ProfileService
316
+ from iatoolkit.services.jwt_service import JWTService
317
+ from iatoolkit.services.dispatcher_service import Dispatcher
318
+ from iatoolkit.services.branding_service import BrandingService
319
+ from iatoolkit.services.i18n_service import I18nService
320
+ from iatoolkit.services.language_service import LanguageService
277
321
 
278
322
  binder.bind(QueryService, to=QueryService)
279
323
  binder.bind(TaskService, to=TaskService)
@@ -285,41 +329,42 @@ class IAToolkit:
285
329
  binder.bind(LoadDocumentsService, to=LoadDocumentsService)
286
330
  binder.bind(ProfileService, to=ProfileService)
287
331
  binder.bind(JWTService, to=JWTService)
288
- binder.bind(Dispatcher, to=Dispatcher, scope=singleton)
332
+ binder.bind(Dispatcher, to=Dispatcher)
333
+ binder.bind(BrandingService, to=BrandingService)
334
+ binder.bind(I18nService, to=I18nService)
335
+ binder.bind(LanguageService, to=LanguageService)
289
336
 
290
337
  def _bind_infrastructure(self, binder: Binder):
291
- from infra.llm_client import llmClient
292
- from infra.llm_proxy import LLMProxy
293
- from infra.google_chat_app import GoogleChatApp
294
- from infra.mail_app import MailApp
295
-
296
- binder.bind(LLMProxy, to=LLMProxy, scope=singleton)
297
- binder.bind(llmClient, to=llmClient, scope=singleton)
338
+ from iatoolkit.infra.llm_client import llmClient
339
+ from iatoolkit.infra.llm_proxy import LLMProxy
340
+ from iatoolkit.infra.google_chat_app import GoogleChatApp
341
+ from iatoolkit.infra.mail_app import MailApp
342
+ from iatoolkit.services.auth_service import AuthService
343
+ from iatoolkit.common.util import Utility
344
+
345
+ binder.bind(LLMProxy, to=LLMProxy)
346
+ binder.bind(llmClient, to=llmClient)
298
347
  binder.bind(GoogleChatApp, to=GoogleChatApp)
299
348
  binder.bind(MailApp, to=MailApp)
300
- binder.bind(IAuthentication, to=IAuthentication)
349
+ binder.bind(AuthService, to=AuthService)
301
350
  binder.bind(Utility, to=Utility)
302
351
 
303
- def _bind_views(self, binder: Binder):
304
- """Vincula las vistas después de que el injector ha sido creado"""
305
- from views.llmquery_view import LLMQueryView
306
- from views.home_view import HomeView
307
- from views.chat_view import ChatView
308
- from views.change_password_view import ChangePasswordView
352
+ def _setup_additional_services(self):
353
+ Bcrypt(self.app)
309
354
 
310
- binder.bind(HomeView, to=HomeView)
311
- binder.bind(ChatView, to=ChatView)
312
- binder.bind(ChangePasswordView, to=ChangePasswordView)
313
- binder.bind(LLMQueryView, to=LLMQueryView)
355
+ def _init_dispatcher_and_company_instances(self):
356
+ from iatoolkit.company_registry import get_company_registry
357
+ from iatoolkit.services.dispatcher_service import Dispatcher
314
358
 
315
- logging.info("✅ Views configuradas correctamente")
359
+ # instantiate all the registered companies
360
+ get_company_registry().instantiate_companies(self._injector)
316
361
 
317
- def _setup_additional_services(self):
318
- Bcrypt(self.app)
362
+ # use the dispatcher to start the execution of every company
363
+ dispatcher = self._injector.get(Dispatcher)
364
+ dispatcher.start_execution()
319
365
 
320
366
  def _setup_cli_commands(self):
321
367
  from iatoolkit.cli_commands import register_core_commands
322
- from services.dispatcher_service import Dispatcher
323
368
  from iatoolkit.company_registry import get_company_registry
324
369
 
325
370
  # 1. Register core commands
@@ -328,16 +373,10 @@ class IAToolkit:
328
373
 
329
374
  # 2. Register company-specific commands
330
375
  try:
331
- # Get the dispatcher, which holds the company instances
332
- dispatcher = self.get_injector().get(Dispatcher)
333
- registry = get_company_registry()
334
-
335
376
  # Iterate through the registered company names
336
- for company_name in registry.get_registered_companies():
337
- company_instance = dispatcher.get_company_instance(company_name)
338
- if company_instance:
339
- company_instance.register_cli_commands(self.app)
340
- logging.info(f"✅ Comandos CLI para la compañía '{company_name}' registrados.")
377
+ all_company_instances = get_company_registry().get_all_company_instances()
378
+ for company_name, company_instance in all_company_instances.items():
379
+ company_instance.register_cli_commands(self.app)
341
380
 
342
381
  except Exception as e:
343
382
  logging.error(f"❌ Error durante el registro de comandos de compañías: {e}")
@@ -346,27 +385,46 @@ class IAToolkit:
346
385
  # Configura context processors para templates
347
386
  @self.app.context_processor
348
387
  def inject_globals():
388
+ from iatoolkit.common.session_manager import SessionManager
389
+ from iatoolkit.services.profile_service import ProfileService
390
+ from iatoolkit.services.i18n_service import I18nService
391
+
392
+ # Get services from the injector
393
+ profile_service = self._injector.get(ProfileService)
394
+ i18n_service = self._injector.get(I18nService)
395
+
396
+ # The 't' function wrapper no longer needs to determine the language itself.
397
+ # It will be automatically handled by the refactored I18nService.
398
+ def translate_for_template(key: str, **kwargs):
399
+ return i18n_service.t(key, **kwargs)
400
+
401
+ # Get user profile if a session exists
402
+ user_profile = profile_service.get_current_session_info().get('profile', {})
403
+
349
404
  return {
350
405
  'url_for': url_for,
351
406
  'iatoolkit_version': self.version,
352
407
  'app_name': 'IAToolkit',
353
- 'user': SessionManager.get('user'),
354
- 'user_company': SessionManager.get('company_short_name'),
408
+ 'user_identifier': SessionManager.get('user_identifier'),
409
+ 'company_short_name': SessionManager.get('company_short_name'),
410
+ 'user_is_local': user_profile.get('user_is_local'),
411
+ 'user_email': user_profile.get('user_email'),
412
+ 'iatoolkit_base_url': os.environ.get('IATOOLKIT_BASE_URL', ''),
413
+ 'flashed_messages': get_flashed_messages(with_categories=True),
414
+ 't': translate_for_template
355
415
  }
356
416
 
357
417
  def _get_default_static_folder(self) -> str:
358
418
  try:
359
419
  current_dir = os.path.dirname(os.path.abspath(__file__)) # .../src/iatoolkit
360
- src_dir = os.path.dirname(current_dir) # .../src
361
- return os.path.join(src_dir, "static")
420
+ return os.path.join(current_dir, "static")
362
421
  except:
363
422
  return 'static'
364
423
 
365
424
  def _get_default_template_folder(self) -> str:
366
425
  try:
367
426
  current_dir = os.path.dirname(os.path.abspath(__file__)) # .../src/iatoolkit
368
- src_dir = os.path.dirname(current_dir) # .../src
369
- return os.path.join(src_dir, "templates")
427
+ return os.path.join(current_dir, "templates")
370
428
  except:
371
429
  return 'templates'
372
430
 
@@ -380,7 +438,7 @@ class IAToolkit:
380
438
  return self._injector
381
439
 
382
440
  def get_dispatcher(self):
383
- from services.dispatcher_service import Dispatcher
441
+ from iatoolkit.services.dispatcher_service import Dispatcher
384
442
  if not self._injector:
385
443
  raise IAToolkitException(
386
444
  IAToolkitException.ErrorType.CONFIG_ERROR,
@@ -396,18 +454,33 @@ class IAToolkit:
396
454
  )
397
455
  return self.db_manager
398
456
 
457
+ def _setup_download_dir(self):
458
+ # 1. set the default download directory
459
+ default_download_dir = os.path.join(os.getcwd(), 'iatoolkit-downloads')
460
+
461
+ # 3. if user specified one, use it
462
+ download_dir = self.app.config.get('IATOOLKIT_DOWNLOAD_DIR', default_download_dir)
463
+
464
+ # 3. save it in the app config
465
+ self.app.config['IATOOLKIT_DOWNLOAD_DIR'] = download_dir
466
+
467
+ # 4. make sure the directory exists
468
+ try:
469
+ os.makedirs(download_dir, exist_ok=True)
470
+ except OSError as e:
471
+ raise IAToolkitException(
472
+ IAToolkitException.ErrorType.CONFIG_ERROR,
473
+ "No se pudo crear el directorio de descarga. Verifique que el directorio existe y tenga permisos de escritura."
474
+ )
475
+ logging.info(f"✅ download dir created in: {download_dir}")
476
+
399
477
 
400
478
  def current_iatoolkit() -> IAToolkit:
401
479
  return IAToolkit.get_instance()
402
480
 
403
- # 🚀 Función de conveniencia para inicialización rápida
481
+ # Función de conveniencia para inicialización rápida
404
482
  def create_app(config: Optional[Dict[str, Any]] = None) -> Flask:
405
483
  toolkit = IAToolkit(config)
406
484
  toolkit.create_iatoolkit()
407
485
 
408
486
  return toolkit.app
409
-
410
- if __name__ == "__main__":
411
- app = create_app()
412
- if app:
413
- app.run(debug=True)
@@ -0,0 +1,5 @@
1
+ # Copyright (c) 2024 Fernando Libedinsky
2
+ # Product: IAToolkit
3
+ #
4
+ # IAToolkit is open source software.
5
+
@@ -0,0 +1,140 @@
1
+ # Copyright (c) 2024 Fernando Libedinsky
2
+ # Product: IAToolkit
3
+ #
4
+ # IAToolkit is open source software.
5
+
6
+ from iatoolkit.common.exceptions import IAToolkitException
7
+ from injector import inject
8
+ # call_service.py
9
+ import requests
10
+ from typing import Optional, Dict, Any, Tuple, Union
11
+ from requests import RequestException
12
+
13
+ class CallServiceClient:
14
+ @inject
15
+ def __init__(self):
16
+ self.headers = {'Content-Type': 'application/json'}
17
+
18
+ def _merge_headers(self, extra: Optional[Dict[str, str]]) -> Dict[str, str]:
19
+ if not extra:
20
+ return dict(self.headers)
21
+ merged = dict(self.headers)
22
+ merged.update(extra)
23
+ return merged
24
+
25
+ def _normalize_timeout(self, timeout: Union[int, float, Tuple[int, int], Tuple[float, float]]) -> Tuple[float, float]:
26
+ # Si pasan un solo número → (connect, read) = (10, timeout)
27
+ if isinstance(timeout, (int, float)):
28
+ return (10, float(timeout))
29
+ return (float(timeout[0]), float(timeout[1]))
30
+
31
+ def _deserialize_response(self, response) -> Tuple[Any, int]:
32
+ try:
33
+ return response.json(), response.status_code
34
+ except ValueError:
35
+ # No es JSON → devolver texto
36
+ return response.text, response.status_code
37
+
38
+ def get(
39
+ self,
40
+ endpoint: str,
41
+ params: Optional[Dict[str, Any]] = None,
42
+ headers: Optional[Dict[str, str]] = None,
43
+ timeout: Union[int, float, Tuple[int, int]] = 10
44
+ ):
45
+ try:
46
+ response = requests.get(
47
+ endpoint,
48
+ params=params,
49
+ headers=self._merge_headers(headers),
50
+ timeout=self._normalize_timeout(timeout)
51
+ )
52
+ except RequestException as e:
53
+ raise IAToolkitException(IAToolkitException.ErrorType.REQUEST_ERROR, str(e))
54
+ return self._deserialize_response(response)
55
+
56
+ def post(
57
+ self,
58
+ endpoint: str,
59
+ json_dict: Optional[Dict[str, Any]] = None,
60
+ params: Optional[Dict[str, Any]] = None,
61
+ headers: Optional[Dict[str, str]] = None,
62
+ timeout: Union[int, float, Tuple[int, int]] = 10
63
+ ):
64
+ try:
65
+ response = requests.post(
66
+ endpoint,
67
+ params=params,
68
+ json=json_dict,
69
+ headers=self._merge_headers(headers),
70
+ timeout=self._normalize_timeout(timeout)
71
+ )
72
+ except RequestException as e:
73
+ raise IAToolkitException(IAToolkitException.ErrorType.REQUEST_ERROR, str(e))
74
+ return self._deserialize_response(response)
75
+
76
+ def put(
77
+ self,
78
+ endpoint: str,
79
+ json_dict: Optional[Dict[str, Any]] = None,
80
+ params: Optional[Dict[str, Any]] = None,
81
+ headers: Optional[Dict[str, str]] = None,
82
+ timeout: Union[int, float, Tuple[int, int]] = 10
83
+ ):
84
+ try:
85
+ response = requests.put(
86
+ endpoint,
87
+ params=params,
88
+ json=json_dict,
89
+ headers=self._merge_headers(headers),
90
+ timeout=self._normalize_timeout(timeout)
91
+ )
92
+ except RequestException as e:
93
+ raise IAToolkitException(IAToolkitException.ErrorType.REQUEST_ERROR, str(e))
94
+ return self._deserialize_response(response)
95
+
96
+ def delete(
97
+ self,
98
+ endpoint: str,
99
+ json_dict: Optional[Dict[str, Any]] = None,
100
+ params: Optional[Dict[str, Any]] = None,
101
+ headers: Optional[Dict[str, str]] = None,
102
+ timeout: Union[int, float, Tuple[int, int]] = 10
103
+ ):
104
+ try:
105
+ response = requests.delete(
106
+ endpoint,
107
+ params=params,
108
+ json=json_dict,
109
+ headers=self._merge_headers(headers),
110
+ timeout=self._normalize_timeout(timeout)
111
+ )
112
+ except RequestException as e:
113
+ raise IAToolkitException(IAToolkitException.ErrorType.REQUEST_ERROR, str(e))
114
+ return self._deserialize_response(response)
115
+
116
+ def post_files(
117
+ self,
118
+ endpoint: str,
119
+ data: Dict[str, Any],
120
+ params: Optional[Dict[str, Any]] = None,
121
+ headers: Optional[Dict[str, str]] = None,
122
+ timeout: Union[int, float, Tuple[int, int]] = 10
123
+ ):
124
+ # Para multipart/form-data no imponemos Content-Type por defecto
125
+ merged_headers = dict(self.headers)
126
+ merged_headers.pop('Content-Type', None)
127
+ if headers:
128
+ merged_headers.update(headers)
129
+
130
+ try:
131
+ response = requests.post(
132
+ endpoint,
133
+ params=params,
134
+ files=data,
135
+ headers=merged_headers,
136
+ timeout=self._normalize_timeout(timeout)
137
+ )
138
+ except RequestException as e:
139
+ raise IAToolkitException(IAToolkitException.ErrorType.REQUEST_ERROR, str(e))
140
+ return self._deserialize_response(response)
@@ -0,0 +1,5 @@
1
+ # Copyright (c) 2024 Fernando Libedinsky
2
+ # Product: IAToolkit
3
+ #
4
+ # IAToolkit is open source software.
5
+
@@ -0,0 +1,17 @@
1
+ # Copyright (c) 2024 Fernando Libedinsky
2
+ # Product: IAToolkit
3
+ #
4
+ # IAToolkit is open source software.
5
+
6
+ from abc import ABC, abstractmethod
7
+ from typing import List
8
+
9
+
10
+ class FileConnector(ABC):
11
+ @abstractmethod
12
+ def list_files(self) -> List[str]:
13
+ pass
14
+
15
+ @abstractmethod
16
+ def get_file_content(self, file_path: str) -> bytes:
17
+ pass