1+ """Compatibilidad temporal: AccessAdapter basado en pyodbc.
2+
3+ Este módulo provee un shim para mantener compatibilidad con tests y código
4+ legacy que esperaban `AccessAdapter` mientras la nueva API es `AccessDatabase`.
5+
6+ La implementación es mínima y centrada en:
7+ - Conexión inmediata en __init__ usando pyodbc.
8+ - Métodos execute_query / execute_non_query / get_tables.
9+ - Manejo de cierre y uso como context manager.
10+ """
11+ from __future__ import annotations
12+
13+ from pathlib import Path
14+ from typing import Any , Dict , List , Optional , Tuple
15+
16+ try : # Detectar disponibilidad de pyodbc
17+ import pyodbc # type: ignore
18+ PYODBC_AVAILABLE = True
19+ except Exception : # pragma: no cover - ruta de error probada vía patch en tests
20+ pyodbc = None # type: ignore
21+ PYODBC_AVAILABLE = False
22+
23+
24+ class AccessAdapter :
25+ """Adaptador simple para bases Access usando pyodbc.
26+
27+ Notas:
28+ - El constructor valida existencia del archivo y disponibilidad de pyodbc.
29+ - Abre la conexión inmediatamente (simplifica su uso en tests legacy).
30+ - Acepta contraseña opcional (agregada tal cual al connection string como PWD=...).
31+ """
32+
33+ def __init__ (self , db_path : Path | str , password : Optional [str ] = None ) -> None :
34+ path = Path (db_path )
35+ if not path .exists ():
36+ raise FileNotFoundError ("Base de datos Access no encontrada" )
37+ if not PYODBC_AVAILABLE :
38+ raise ImportError ("pyodbc no está disponible" )
39+
40+ self .db_path : Path = path
41+ self .password : Optional [str ] = password
42+ self .connection = self ._connect ()
43+
44+ # Construcción simple del connection string para Access (*.accdb)
45+ def _build_connection_string (self ) -> str :
46+ # Driver genérico de Access en Windows
47+ conn = (
48+ f"DRIVER={{Microsoft Access Driver (*.mdb, *.accdb)}};"
49+ f"DBQ={ self .db_path } ;"
50+ )
51+ if self .password :
52+ conn += f"PWD={ self .password } ;"
53+ return conn
54+
55+ def _connect (self ):
56+ conn_str = self ._build_connection_string ()
57+ # pyodbc.connect puede lanzar excepciones que se propagan (como esperan los tests)
58+ return pyodbc .connect (conn_str ) # type: ignore[attr-defined]
59+
60+ # API de consultas
61+ def execute_query (self , query : str , params : Optional [Tuple [Any , ...]] = None ) -> List [Dict [str , Any ]]:
62+ cursor = self .connection .cursor ()
63+ if params :
64+ cursor .execute (query , params )
65+ else :
66+ cursor .execute (query )
67+ # description es una secuencia de tuplas; el nombre está en el índice 0
68+ columns = [col [0 ] for col in cursor .description ]
69+ rows = cursor .fetchall ()
70+ return [dict (zip (columns , row )) for row in rows ]
71+
72+ def execute_non_query (self , query : str , params : Optional [Tuple [Any , ...]] = None ) -> int :
73+ cursor = self .connection .cursor ()
74+ if params :
75+ cursor .execute (query , params )
76+ else :
77+ cursor .execute (query )
78+ affected = getattr (cursor , "rowcount" , - 1 )
79+ # Commit explícito tras operaciones de escritura
80+ self .connection .commit ()
81+ return affected
82+
83+ def get_tables (self ) -> List [str ]:
84+ try :
85+ cursor = self .connection .cursor ()
86+ tables = cursor .tables (tableType = 'TABLE' )
87+ # pyodbc devuelve filas con atributos; en tests se mockean con .table_name
88+ return [t .table_name for t in tables ]
89+ except Exception :
90+ # El contrato de tests espera lista vacía ante errores
91+ return []
92+
93+ # Gestión de recursos
94+ def close (self ) -> None :
95+ try :
96+ if self .connection :
97+ self .connection .close ()
98+ except Exception :
99+ # Silenciar errores de cierre (tests no exigen propagación)
100+ pass
101+
102+ # Context manager
103+ def __enter__ (self ) -> "AccessAdapter" :
104+ return self
105+
106+ def __exit__ (self , exc_type , exc , tb ) -> None :
107+ self .close ()
0 commit comments