iatoolkit 0.56.0__py3-none-any.whl → 0.57.0__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.

Potentially problematic release.


This version of iatoolkit might be problematic. Click here for more details.

iatoolkit/iatoolkit.py CHANGED
@@ -19,7 +19,7 @@ from werkzeug.middleware.proxy_fix import ProxyFix
19
19
  from injector import Binder, singleton, Injector
20
20
  from importlib.metadata import version as _pkg_version, PackageNotFoundError
21
21
 
22
- IATOOLKIT_VERSION = "0.56.0"
22
+ IATOOLKIT_VERSION = "0.57.0"
23
23
 
24
24
  # global variable for the unique instance of IAToolkit
25
25
  _iatoolkit_instance: Optional['IAToolkit'] = None
@@ -3,9 +3,10 @@
3
3
  #
4
4
  # IAToolkit is open source software.
5
5
 
6
- from sqlalchemy import Column, Integer, String, DateTime, Enum, Text, JSON, Boolean, ForeignKey, Table
6
+ from sqlalchemy import Column, Integer, BigInteger, String, DateTime, Enum, Text, JSON, Boolean, ForeignKey, Table
7
7
  from sqlalchemy.orm import DeclarativeBase
8
8
  from sqlalchemy.orm import relationship, class_mapper, declarative_base
9
+ from sqlalchemy.sql import func
9
10
  from datetime import datetime
10
11
  from pgvector.sqlalchemy import Vector
11
12
  from enum import Enum as PyEnum
@@ -307,3 +308,27 @@ class Prompt(Base):
307
308
 
308
309
  company = relationship("Company", back_populates="prompts")
309
310
  category = relationship("PromptCategory", back_populates="prompts")
311
+
312
+ class AccessLog(Base):
313
+ # Modelo ORM para registrar cada intento de acceso a la plataforma.
314
+ __tablename__ = 'iat_access_log'
315
+
316
+ id = Column(BigInteger, primary_key=True)
317
+
318
+ timestamp = Column(DateTime(timezone=True), server_default=func.now(), nullable=False, index=True)
319
+ company_short_name = Column(String(100), nullable=False, index=True)
320
+ user_identifier = Column(String(255), index=True)
321
+
322
+ # Cómo y el Resultado
323
+ auth_type = Column(String(20), nullable=False) # 'local', 'external_api', 'redeem_token', etc.
324
+ outcome = Column(String(10), nullable=False) # 'success' o 'failure'
325
+ reason_code = Column(String(50)) # Causa de fallo, ej: 'INVALID_CREDENTIALS'
326
+
327
+ # Contexto de la Petición
328
+ source_ip = Column(String(45), nullable=False)
329
+ user_agent_hash = Column(String(16)) # Hash corto del User-Agent
330
+ request_path = Column(String(255), nullable=False)
331
+
332
+ def __repr__(self):
333
+ return (f"<AccessLog(id={self.id}, company='{self.company_short_name}', "
334
+ f"user='{self.user_identifier}', outcome='{self.outcome}')>")
@@ -6,8 +6,12 @@
6
6
  from flask import request
7
7
  from injector import inject
8
8
  from iatoolkit.services.profile_service import ProfileService
9
+ from iatoolkit.services.jwt_service import JWTService
10
+ from iatoolkit.repositories.database_manager import DatabaseManager
11
+ from iatoolkit.repositories.models import AccessLog
9
12
  from flask import request
10
13
  import logging
14
+ import hashlib
11
15
 
12
16
 
13
17
  class AuthService:
@@ -17,11 +21,75 @@ class AuthService:
17
21
  """
18
22
 
19
23
  @inject
20
- def __init__(self, profile_service: ProfileService):
21
- """
22
- Injects ProfileService to access session information and validate API keys.
23
- """
24
+ def __init__(self, profile_service: ProfileService,
25
+ jwt_service: JWTService,
26
+ db_manager: DatabaseManager
27
+ ):
24
28
  self.profile_service = profile_service
29
+ self.jwt_service = jwt_service
30
+ self.db_manager = db_manager
31
+
32
+ def login_local_user(self, company_short_name: str, email: str, password: str) -> dict:
33
+ # try to autenticate a local user, register the event and return the result
34
+ auth_response = self.profile_service.login(
35
+ company_short_name=company_short_name,
36
+ email=email,
37
+ password=password
38
+ )
39
+
40
+ if not auth_response.get('success'):
41
+ self.log_access(
42
+ company_short_name=company_short_name,
43
+ user_identifier=email,
44
+ auth_type='local',
45
+ outcome='failure',
46
+ reason_code='INVALID_CREDENTIALS',
47
+ )
48
+ else:
49
+ self.log_access(
50
+ company_short_name=company_short_name,
51
+ auth_type='local',
52
+ outcome='success',
53
+ user_identifier=auth_response.get('user_identifier')
54
+ )
55
+
56
+ return auth_response
57
+
58
+ def redeem_token_for_session(self, company_short_name: str, token: str) -> dict:
59
+ # redeem a token for a session, register the event and return the result
60
+ payload = self.jwt_service.validate_chat_jwt(token)
61
+
62
+ if not payload:
63
+ self.log_access(
64
+ company_short_name=company_short_name,
65
+ auth_type='redeem_token',
66
+ outcome='failure',
67
+ reason_code='JWT_INVALID'
68
+ )
69
+ return {'success': False, 'error': 'Token inválido o expirado'}
70
+
71
+ # 2. if token is valid, extract the user_identifier
72
+ user_identifier = payload.get('user_identifier')
73
+ try:
74
+ # create the Flask session
75
+ self.profile_service.set_session_for_user(company_short_name, user_identifier)
76
+ self.log_access(
77
+ company_short_name=company_short_name,
78
+ auth_type='redeem_token',
79
+ outcome='success',
80
+ user_identifier=user_identifier
81
+ )
82
+ return {'success': True, 'user_identifier': user_identifier}
83
+ except Exception as e:
84
+ logging.error(f"Error al crear la sesión desde token para {user_identifier}: {e}")
85
+ self.log_access(
86
+ company_short_name=company_short_name,
87
+ auth_type='redeem_token',
88
+ outcome='failure',
89
+ reason_code='SESSION_CREATION_FAILED',
90
+ user_identifier=user_identifier
91
+ )
92
+ return {'success': False, 'error': 'No se pudo crear la sesión del usuario'}
25
93
 
26
94
  def verify(self) -> dict:
27
95
  """
@@ -74,4 +142,40 @@ class AuthService:
74
142
  # --- Failure: No valid credentials found ---
75
143
  logging.info(f"Authentication required. No session cookie or API Key provided.")
76
144
  return {"success": False, "error": "Authentication required. No session cookie or API Key provided.",
77
- "status_code": 402}
145
+ "status_code": 402}
146
+
147
+ def log_access(self,
148
+ company_short_name: str,
149
+ auth_type: str,
150
+ outcome: str,
151
+ user_identifier: str = None,
152
+ reason_code: str = None):
153
+ """
154
+ Registra un intento de acceso en la base de datos.
155
+ Es "best-effort" y no debe interrumpir el flujo de autenticación.
156
+ """
157
+ session = self.db_manager.scoped_session()
158
+ try:
159
+ # Capturar datos del contexto de la petición de Flask
160
+ source_ip = request.headers.get('X-Forwarded-For', request.remote_addr)
161
+ path = request.path
162
+ ua = request.headers.get('User-Agent', '')
163
+ ua_hash = hashlib.sha256(ua.encode()).hexdigest()[:16] if ua else None
164
+
165
+ # Crear la entrada de log
166
+ log_entry = AccessLog(
167
+ company_short_name=company_short_name,
168
+ user_identifier=user_identifier,
169
+ auth_type=auth_type,
170
+ outcome=outcome,
171
+ reason_code=reason_code,
172
+ source_ip=source_ip,
173
+ user_agent_hash=ua_hash,
174
+ request_path=path,
175
+ )
176
+ session.add(log_entry)
177
+ session.commit()
178
+
179
+ except Exception as e:
180
+ logging.error(f"Fallo al escribir en AccessLog: {e}", exc_info=False)
181
+ session.rollback()
@@ -8,6 +8,7 @@ from flask.views import MethodView
8
8
  from flask import render_template, url_for
9
9
  from injector import inject
10
10
  from iatoolkit.services.profile_service import ProfileService
11
+ from iatoolkit.services.auth_service import AuthService
11
12
  from iatoolkit.services.query_service import QueryService
12
13
  from iatoolkit.services.branding_service import BrandingService
13
14
  from iatoolkit.services.onboarding_service import OnboardingService
@@ -23,6 +24,7 @@ class BaseLoginView(MethodView):
23
24
  @inject
24
25
  def __init__(self,
25
26
  profile_service: ProfileService,
27
+ auth_service: AuthService,
26
28
  jwt_service: JWTService,
27
29
  branding_service: BrandingService,
28
30
  prompt_service: PromptService,
@@ -30,6 +32,7 @@ class BaseLoginView(MethodView):
30
32
  query_service: QueryService
31
33
  ):
32
34
  self.profile_service = profile_service
35
+ self.auth_service = auth_service
33
36
  self.jwt_service = jwt_service
34
37
  self.branding_service = branding_service
35
38
  self.prompt_service = prompt_service
@@ -6,45 +6,18 @@
6
6
  import os
7
7
  import logging
8
8
  from flask import request, jsonify
9
- from flask.views import MethodView
10
9
  from injector import inject
11
- from iatoolkit.services.auth_service import AuthService
12
10
  from iatoolkit.views.base_login_view import BaseLoginView
13
11
 
14
12
  # Importar los servicios que necesita la clase base
15
13
  from iatoolkit.services.profile_service import ProfileService
16
14
  from iatoolkit.services.jwt_service import JWTService
17
- from iatoolkit.services.branding_service import BrandingService
18
- from iatoolkit.services.onboarding_service import OnboardingService
19
- from iatoolkit.services.query_service import QueryService
20
- from iatoolkit.services.prompt_manager_service import PromptService
21
15
 
22
16
  class ExternalLoginView(BaseLoginView):
23
17
  """
24
18
  Handles login for external users via API.
25
19
  Authenticates and then delegates the path decision (fast/slow) to the base class.
26
20
  """
27
- @inject
28
- def __init__(self,
29
- iauthentication: AuthService,
30
- jwt_service: JWTService,
31
- profile_service: ProfileService,
32
- branding_service: BrandingService,
33
- prompt_service: PromptService,
34
- onboarding_service: OnboardingService,
35
- query_service: QueryService):
36
- # Pass the dependencies for the base class to its __init__
37
- super().__init__(
38
- profile_service=profile_service,
39
- jwt_service=jwt_service,
40
- branding_service=branding_service,
41
- onboarding_service=onboarding_service,
42
- query_service=query_service,
43
- prompt_service=prompt_service,
44
- )
45
- # Handle the dependency specific to this child class
46
- self.iauthentication = iauthentication
47
-
48
21
  def post(self, company_short_name: str):
49
22
  data = request.get_json()
50
23
  if not data or 'user_identifier' not in data:
@@ -59,9 +32,9 @@ class ExternalLoginView(BaseLoginView):
59
32
  return jsonify({"error": "missing user_identifier"}), 404
60
33
 
61
34
  # 1. Authenticate the API call.
62
- iaut = self.iauthentication.verify()
63
- if not iaut.get("success"):
64
- return jsonify(iaut), 401
35
+ auth_response = self.auth_service.verify()
36
+ if not auth_response.get("success"):
37
+ return jsonify(auth_response), 401
65
38
 
66
39
  # 2. Create the external user session.
67
40
  self.profile_service.create_external_user_session(company, user_identifier)
@@ -74,37 +47,21 @@ class ExternalLoginView(BaseLoginView):
74
47
  return jsonify({"error": f"Internal server error while starting chat. {str(e)}"}), 500
75
48
 
76
49
 
77
- class RedeemTokenApiView(MethodView):
50
+ class RedeemTokenApiView(BaseLoginView):
78
51
  # this endpoint is only used ONLY by chat_main.js to redeem a chat token
79
- @inject
80
- def __init__(self,
81
- profile_service: ProfileService,
82
- jwt_service: JWTService):
83
- self.profile_service = profile_service
84
- self.jwt_service = jwt_service
85
-
86
52
  def post(self, company_short_name: str):
87
53
  data = request.get_json()
88
54
  if not data or 'token' not in data:
89
55
  return jsonify({"error": "Falta token de validación"}), 400
90
56
 
91
- # 1. validate the token
57
+ # get the token and validate with auth service
92
58
  token = data.get('token')
93
- payload = self.jwt_service.validate_chat_jwt(token)
94
- if not payload:
95
- logging.warning("Intento de canjear un token inválido o expirado.")
96
- return {"error": "Token inválido o expirado."}, 401
97
-
98
- # 2. if token is valid, extract the user_identifier
99
- user_identifier = payload.get('user_identifier')
100
-
101
- try:
102
- # 3. create the Flask session
103
- self.profile_service.set_session_for_user(company_short_name, user_identifier)
104
- logging.info(f"Token de sesión canjeado exitosamente para {user_identifier}.")
59
+ redeem_result = self.auth_service.redeem_token_for_session(
60
+ company_short_name=company_short_name,
61
+ token=token
62
+ )
105
63
 
106
- return {"status": "ok"}, 200
64
+ if not redeem_result['success']:
65
+ return {"error": redeem_result['error']}, 401
107
66
 
108
- except Exception as e:
109
- logging.error(f"Error al crear la sesión desde token para {user_identifier}: {e}")
110
- return {"error": "No se pudo crear la sesión del usuario."}, 500
67
+ return {"status": "ok"}, 200
@@ -7,6 +7,7 @@ from flask.views import MethodView
7
7
  from flask import request, redirect, render_template, url_for
8
8
  from injector import inject
9
9
  from iatoolkit.services.profile_service import ProfileService
10
+ from iatoolkit.services.auth_service import AuthService
10
11
  from iatoolkit.services.jwt_service import JWTService
11
12
  from iatoolkit.services.query_service import QueryService
12
13
  from iatoolkit.services.prompt_manager_service import PromptService
@@ -21,7 +22,6 @@ class LoginView(BaseLoginView):
21
22
  Handles login for local users.
22
23
  Authenticates and then delegates the path decision (fast/slow) to the base class.
23
24
  """
24
-
25
25
  def post(self, company_short_name: str):
26
26
  company = self.profile_service.get_company_by_short_name(company_short_name)
27
27
  if not company:
@@ -30,8 +30,8 @@ class LoginView(BaseLoginView):
30
30
  email = request.form.get('email')
31
31
  password = request.form.get('password')
32
32
 
33
- # 1. Authenticate user and create the session for internal user
34
- auth_response = self.profile_service.login(
33
+ # 1. Authenticate internal user
34
+ auth_response = self.auth_service.login_local_user(
35
35
  company_short_name=company_short_name,
36
36
  email=email,
37
37
  password=password
@@ -67,6 +67,7 @@ class FinalizeContextView(MethodView):
67
67
  @inject
68
68
  def __init__(self,
69
69
  profile_service: ProfileService,
70
+ auth_service: AuthService,
70
71
  query_service: QueryService,
71
72
  prompt_service: PromptService,
72
73
  branding_service: BrandingService,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: iatoolkit
3
- Version: 0.56.0
3
+ Version: 0.57.0
4
4
  Summary: IAToolkit
5
5
  Author: Fernando Libedinsky
6
6
  License-Expression: MIT
@@ -2,7 +2,7 @@ iatoolkit/__init__.py,sha256=4PWjMJjktixtrxF6BY405qyA50Sv967kEP2x-oil6qk,1120
2
2
  iatoolkit/base_company.py,sha256=nfF-G0h63jy3Qh9kCnvx8Ozx76IjG2p7a34HpweWhOk,4608
3
3
  iatoolkit/cli_commands.py,sha256=G5L9xQXZ0lVFXQWBaE_KEZHyfuiT6PL1nTQRoSdnBzc,2302
4
4
  iatoolkit/company_registry.py,sha256=tduqt3oV8iDX_IB1eA7KIgvIxE4edTcy-3qZIXh3Lzw,2549
5
- iatoolkit/iatoolkit.py,sha256=ve63cb-1L-m_RLPsU7KRG9IPhhMREb1fDQjqOIgHx9w,17583
5
+ iatoolkit/iatoolkit.py,sha256=tFx-D1Q0usCdOc3vSNdtz94mTCWhUGcyRkDISI2QMMs,17583
6
6
  iatoolkit/common/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
7
  iatoolkit/common/exceptions.py,sha256=EXx40n5htp7UiOM6P1xfJ9U6NMcADqm62dlFaKz7ICU,1154
8
8
  iatoolkit/common/routes.py,sha256=c8kRk8pEK_nV-2P1LiYXRERmW_QoW21Hk6CZkkizv6Y,6419
@@ -29,12 +29,12 @@ iatoolkit/repositories/__init__.py,sha256=5JqK9sZ6jBuK83zDQokUhxQ0wuJJJ9DXB8pYCL
29
29
  iatoolkit/repositories/database_manager.py,sha256=QgV8hNnVv9RmeOvUdomdj_mfk0bf3Rl8Ti41a-5zIAY,3700
30
30
  iatoolkit/repositories/document_repo.py,sha256=Y7bF1kZB1HWJsAGjWdF7P2aVYeTYNufq9ngQXp7mDkY,1124
31
31
  iatoolkit/repositories/llm_query_repo.py,sha256=YT_t7cYGQk8rwzH_17-28aTzO-e2jUfa2rvXy8tugvA,3612
32
- iatoolkit/repositories/models.py,sha256=3YbIJXNMZiTkMbPyiSOiyzqUKEQF0JIfN4VSWYzwr44,13146
32
+ iatoolkit/repositories/models.py,sha256=qM95kiKm_92JoPNXiwMT4HTjr5BjalnrDiatG3Rte-M,14300
33
33
  iatoolkit/repositories/profile_repo.py,sha256=21am3GP7XCG0nq6i3pArQ7mfGsrRn8rdcWT98fsdwlU,4397
34
34
  iatoolkit/repositories/tasks_repo.py,sha256=icVO_r2oPagGnnBhwVFzznnvEEU2EAx-2dlWuWvoDC4,1745
35
35
  iatoolkit/repositories/vs_repo.py,sha256=UkpmQQiocgM5IwRBmmWhw3HHzHP6zK1nN3J3TcQgjhc,5300
36
36
  iatoolkit/services/__init__.py,sha256=5JqK9sZ6jBuK83zDQokUhxQ0wuJJJ9DXB8pYCLkX7X4,102
37
- iatoolkit/services/auth_service.py,sha256=XHG2F0Vf2wDFjylc_OdDEIJL8I4cdhxeUIZIltgE4DU,2936
37
+ iatoolkit/services/auth_service.py,sha256=vBVKkFJWsaPhhR-LvfLXfIrR-mmR-P5Y8Ya_5p1pgC8,7126
38
38
  iatoolkit/services/benchmark_service.py,sha256=CdbFYyS3FHFhNzWQEa9ZNjUlmON10DT1nKNbZQ1EUi8,5880
39
39
  iatoolkit/services/branding_service.py,sha256=gXj9Lj6EIFNIHT6wAHia5lr4_2a2sD-ExMbewno5YD8,7505
40
40
  iatoolkit/services/dispatcher_service.py,sha256=Qdn2x4cozpgpKg2448sUxkhO6tuplzb8xPWUxdTTFBE,12772
@@ -86,10 +86,10 @@ iatoolkit/templates/onboarding_shell.html,sha256=r1ivSR2ci8GrDSm1uaD-cf78rfO1bKT
86
86
  iatoolkit/templates/signup.html,sha256=9ArDvcNQgHFR2dwxy-37AXzGUOeOsT7Nz5u0y6fAB3U,4385
87
87
  iatoolkit/templates/test.html,sha256=rwNtxC83tbCl5COZFXYvmRBxxmgFJtPNuVBd_nq9KWY,133
88
88
  iatoolkit/views/__init__.py,sha256=5JqK9sZ6jBuK83zDQokUhxQ0wuJJJ9DXB8pYCLkX7X4,102
89
- iatoolkit/views/base_login_view.py,sha256=6AANVhwhs5MyT3WVjJqKZLoS1TwyErsrKnb1xhpqP4Y,3970
89
+ iatoolkit/views/base_login_view.py,sha256=_JM-SMFlftxhpIRV7KyHsTAnAJN_ThY2j1WaYP-HneI,4111
90
90
  iatoolkit/views/change_password_view.py,sha256=tM0woZyKdhY4XYjS_YXg2sKq3RYkXGfcq_eVAKrNvNM,4498
91
91
  iatoolkit/views/chat_token_request_view.py,sha256=wf32_A2Sq8NHYWshCwL10Tovd1znLoD0jQjzutR3sVE,4408
92
- iatoolkit/views/external_login_view.py,sha256=nuWebrl2Opnhj6H-UW6dUOHwi8pj4s1_-dN3VEKNQdY,4483
92
+ iatoolkit/views/external_login_view.py,sha256=ZFd2Qm_B7ecLxFfu9vL4YjIfkRP6M18slGZszQzpB2g,2572
93
93
  iatoolkit/views/file_store_api_view.py,sha256=Uz9f6sey3_F5K8zuyQz6SwYRKAalCjD1ekf-Mkl_Kfo,2326
94
94
  iatoolkit/views/forgot_password_view.py,sha256=-qKJeeOBqJFdvDUk7rCNg1E1cDQnJQkozPpb0T0FgwA,3159
95
95
  iatoolkit/views/history_api_view.py,sha256=x-tZhB8UzqrD2n-WDIfmHK9iVhGZ9f0yncsGs9mxwt0,1953
@@ -98,14 +98,14 @@ iatoolkit/views/init_context_api_view.py,sha256=1j8NKfODfPrffbA5YO8TPMHh-ildlLNz
98
98
  iatoolkit/views/llmquery_api_view.py,sha256=Rh-y-VENwwtNsDrYAD_SWKwjK16fW-pFRWlEvI-OYwY,2120
99
99
  iatoolkit/views/llmquery_web_view.py,sha256=WhjlA1mfsoL8hL9tlKQfjCUcaTzT43odlp_uQKmT314,1500
100
100
  iatoolkit/views/login_simulation_view.py,sha256=0Qt-puRnltI2HZxlfdyJmOf26-hQp3xjknGV_jkwV7E,3484
101
- iatoolkit/views/login_view.py,sha256=sjM8ki_F7C9S0a9pukLd5jn5KnM2sbRabnAIrtIK8KQ,5373
101
+ iatoolkit/views/login_view.py,sha256=VmW5oDOBqbxLcHx7-LcLyGoI9WMwyg-UbMedV7vap8g,5448
102
102
  iatoolkit/views/prompt_api_view.py,sha256=MP0r-MiswwKcbNc_5KY7aVbHkrR218I8XCiCX1D0yTA,1244
103
103
  iatoolkit/views/signup_view.py,sha256=BCjhM2lMiDPwYrlW_eEwPl-ZLupblbFfsonWtq0E4vU,3922
104
104
  iatoolkit/views/tasks_review_view.py,sha256=keLsLCyOTTlcoIapnB_lbuSvLwrPVZVpBiFC_7ChbLg,3388
105
105
  iatoolkit/views/tasks_view.py,sha256=a3anTXrJTTvbQuc6PSpOzidLKQFL4hWa7PI2Cppcz8w,4110
106
106
  iatoolkit/views/user_feedback_api_view.py,sha256=59XB9uQLHI4Q6QA4_XhK787HzfXb-c6EY7k1Ccyr4hI,2424
107
107
  iatoolkit/views/verify_user_view.py,sha256=7XLSaxvs8LjBr3cYOUDa9B8DqW_50IGlq0IvmOQcD0Y,2340
108
- iatoolkit-0.56.0.dist-info/METADATA,sha256=Td4cobpNkkpPaIPZALITKGwwBHowzfG3klOYoGANjsk,9301
109
- iatoolkit-0.56.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
110
- iatoolkit-0.56.0.dist-info/top_level.txt,sha256=V_w4QvDx0b1RXiy8zTCrD1Bp7AZkFe3_O0-9fMiwogg,10
111
- iatoolkit-0.56.0.dist-info/RECORD,,
108
+ iatoolkit-0.57.0.dist-info/METADATA,sha256=zrwlhrbdyUpcYe4x6e3-1nexr7_p8-b2aEcDzuv4Vzs,9301
109
+ iatoolkit-0.57.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
110
+ iatoolkit-0.57.0.dist-info/top_level.txt,sha256=V_w4QvDx0b1RXiy8zTCrD1Bp7AZkFe3_O0-9fMiwogg,10
111
+ iatoolkit-0.57.0.dist-info/RECORD,,