csf-scraper-python 0.1.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.
- csf_scraper/__init__.py +119 -0
- csf_scraper/data_extractor.py +74 -0
- csf_scraper/date_parser.py +26 -0
- csf_scraper/exceptions/__init__.py +17 -0
- csf_scraper/exceptions/image_reader.py +106 -0
- csf_scraper/exceptions/pdf_reader.py +33 -0
- csf_scraper/exceptions/value_objects.py +11 -0
- csf_scraper/image_readers/__init__.py +52 -0
- csf_scraper/image_readers/adapters/__init__.py +18 -0
- csf_scraper/image_readers/adapters/llms/__init__.py +33 -0
- csf_scraper/image_readers/adapters/llms/gemini_image_reader.py +157 -0
- csf_scraper/image_readers/adapters/llms/prompts/__init__.py +8 -0
- csf_scraper/image_readers/adapters/llms/prompts/llm_image_reader_prompt.py +17 -0
- csf_scraper/image_readers/csf_image_extractor.py +29 -0
- csf_scraper/image_readers/ports/__init__.py +5 -0
- csf_scraper/image_readers/ports/image_reader_port.py +37 -0
- csf_scraper/image_readers/schemas/__init__.py +5 -0
- csf_scraper/image_readers/schemas/rfc_cif_schema.py +42 -0
- csf_scraper/image_readers/services/__init__.py +18 -0
- csf_scraper/image_readers/services/llm_image_reader_service.py +328 -0
- csf_scraper/pdf_reader/__init__.py +7 -0
- csf_scraper/pdf_reader/csf_pdf_extractor.py +33 -0
- csf_scraper/pdf_reader/pdf_csf_text_parser.py +54 -0
- csf_scraper/pdf_reader/pdf_to_text.py +36 -0
- csf_scraper/persona.py +64 -0
- csf_scraper/persona_fisica.py +25 -0
- csf_scraper/persona_moral.py +21 -0
- csf_scraper/ports/__init__.py +5 -0
- csf_scraper/ports/rfc_cif_extractor_port.py +40 -0
- csf_scraper/py.typed +0 -0
- csf_scraper/regimen.py +62 -0
- csf_scraper/rfc_cif_data.py +15 -0
- csf_scraper/scraper.py +117 -0
- csf_scraper/value_objects/__init__.py +7 -0
- csf_scraper/value_objects/csf_image.py +68 -0
- csf_scraper/value_objects/id_cif.py +48 -0
- csf_scraper/value_objects/rfc.py +56 -0
- csf_scraper_python-0.1.1.dist-info/METADATA +390 -0
- csf_scraper_python-0.1.1.dist-info/RECORD +42 -0
- csf_scraper_python-0.1.1.dist-info/WHEEL +4 -0
- csf_scraper_python-0.1.1.dist-info/licenses/LICENSE +21 -0
- csf_scraper_python-0.1.1.dist-info/licenses/NOTICE.md +16 -0
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: csf-scraper-python
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Obtiene los datos fiscales actuales de una persona moral o física dado su RFC y su idCIF (port de phpcfdi/csf-scraper)
|
|
5
|
+
Project-URL: Homepage, https://github.com/PJMP4826/csf-scraper-python
|
|
6
|
+
Project-URL: Issues, https://github.com/PJMP4826/csf-scraper-python/issues
|
|
7
|
+
Project-URL: Source, https://github.com/PJMP4826/csf-scraper-python
|
|
8
|
+
Author-email: PJMP4826 <faustojaviermendozaperez.tareas@gmail.com>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
License-File: NOTICE.md
|
|
12
|
+
Keywords: csf,phpcfdi,rfc,sat,scraper
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Framework :: AsyncIO
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Typing :: Typed
|
|
23
|
+
Requires-Python: >=3.11
|
|
24
|
+
Requires-Dist: beautifulsoup4>=4.12
|
|
25
|
+
Requires-Dist: httpx>=0.27
|
|
26
|
+
Requires-Dist: llama-index-core>=0.14
|
|
27
|
+
Requires-Dist: lxml>=5.0
|
|
28
|
+
Requires-Dist: pydantic-core>=2.18
|
|
29
|
+
Requires-Dist: pydantic>=2.7
|
|
30
|
+
Provides-Extra: dev
|
|
31
|
+
Requires-Dist: mypy>=1.11; extra == 'dev'
|
|
32
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
33
|
+
Requires-Dist: respx>=0.22; extra == 'dev'
|
|
34
|
+
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
35
|
+
Provides-Extra: llm-gemini
|
|
36
|
+
Requires-Dist: google-genai>=1.0; extra == 'llm-gemini'
|
|
37
|
+
Requires-Dist: llama-index-llms-google-genai<0.12,>=0.11; extra == 'llm-gemini'
|
|
38
|
+
Description-Content-Type: text/markdown
|
|
39
|
+
|
|
40
|
+
# csf-scraper
|
|
41
|
+
|
|
42
|
+
Port a Python de [phpcfdi/csf-scraper](https://github.com/phpcfdi/csf-scraper):
|
|
43
|
+
obtiene los datos fiscales actuales de una persona moral o física dado su RFC y
|
|
44
|
+
su idCIF (SAT, Constancia de Situación Fiscal).
|
|
45
|
+
|
|
46
|
+
## Requisitos
|
|
47
|
+
|
|
48
|
+
- Python >= 3.11
|
|
49
|
+
- [`pdftotext`](https://poppler.freedesktop.org/) (paquete del sistema
|
|
50
|
+
`poppler-utils`), necesario **solo** para leer la CSF desde un archivo PDF
|
|
51
|
+
(`obtain_from_pdf_path`).
|
|
52
|
+
- Dependencias: `httpx`, `beautifulsoup4`, `lxml`, `pydantic`, `llama-index-core`.
|
|
53
|
+
Los clientes de cada proveedor LLM son extras opcionales (`llm-gemini`).
|
|
54
|
+
|
|
55
|
+
## Instalación
|
|
56
|
+
|
|
57
|
+
Desde PyPI (solo lectura de CSF por RFC+idCIF y por PDF):
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
pip install csf-scraper-python
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Para leer CSF escaneadas (JPEG/PNG) con Gemini, agrega el extra:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
pip install "csf-scraper-python[llm-gemini]"
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Desde el código fuente:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
pip install .
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Ejemplo de uso
|
|
76
|
+
|
|
77
|
+
Todo el flujo es asíncrono: `Scraper` envuelve un `httpx.AsyncClient` y todos
|
|
78
|
+
los métodos de lectura son corrutinas, así que se consumen con `await`.
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
import asyncio
|
|
82
|
+
|
|
83
|
+
from csf_scraper import Scraper
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
async def main() -> None:
|
|
87
|
+
# El scraper es un context manager asíncrono: cierra el AsyncClient al salir
|
|
88
|
+
async with Scraper.create() as scraper:
|
|
89
|
+
# RFC + idCIF directamente (se validan con los value objects Rfc e IdCif)
|
|
90
|
+
person = await scraper.obtain_from_rfc_and_cif("DIM8701081LA", "1904014102123")
|
|
91
|
+
|
|
92
|
+
# También puedes obtener los datos directamente de un archivo local PDF
|
|
93
|
+
# (requiere pdftotext / poppler-utils instalado)
|
|
94
|
+
person = await scraper.obtain_from_pdf_path("ruta/al/csf.pdf")
|
|
95
|
+
|
|
96
|
+
# Acceso a los datos de la persona (moral o física) por atributos
|
|
97
|
+
if person.is_fisica():
|
|
98
|
+
print(person.nombre)
|
|
99
|
+
elif person.is_moral():
|
|
100
|
+
print(person.razon_social)
|
|
101
|
+
|
|
102
|
+
# Regímenes: lista de Regimen, cada uno con su id ya calculado
|
|
103
|
+
for regimen in person.regimenes:
|
|
104
|
+
print(regimen.regimen, regimen.regimen_id, regimen.fecha_alta)
|
|
105
|
+
|
|
106
|
+
# Los títulos del HTML que no tienen un campo propio se guardan aquí
|
|
107
|
+
print(person.extra_data)
|
|
108
|
+
|
|
109
|
+
# Diccionario de datos (envoltura de model_dump) o JSON (ISO-8601 en fechas)
|
|
110
|
+
data = person.to_dict()
|
|
111
|
+
json_text = person.model_dump_json()
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
if __name__ == "__main__":
|
|
115
|
+
asyncio.run(main())
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Para leer una CSF escaneada (JPEG/PNG) se inyecta el motor de lectura de imágenes.
|
|
119
|
+
Requiere el extra `llm-gemini`:
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
import asyncio
|
|
123
|
+
|
|
124
|
+
from csf_scraper import CsfImageExtractor, GeminiImageReader, Scraper
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
async def main() -> None:
|
|
128
|
+
reader = GeminiImageReader(api_key="…", model_name="…")
|
|
129
|
+
async with Scraper.create(image_extractor=CsfImageExtractor(reader)) as scraper:
|
|
130
|
+
person = await scraper.obtain_from_image_path("ruta/al/csf.png")
|
|
131
|
+
print(person.to_dict())
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
if __name__ == "__main__":
|
|
135
|
+
asyncio.run(main())
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Salida de `person.to_dict()` para una persona moral (fechas como `datetime`):
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
{
|
|
142
|
+
"rfc": "DIM8701081LA",
|
|
143
|
+
"id_cif": "12345678",
|
|
144
|
+
"fecha_inicio_operaciones": datetime(2019, 2, 21),
|
|
145
|
+
# …
|
|
146
|
+
"regimenes": [
|
|
147
|
+
{
|
|
148
|
+
"regimen": "Régimen General de Ley Personas Morales",
|
|
149
|
+
"fecha_alta": datetime(2019, 2, 21),
|
|
150
|
+
"regimen_id": "601",
|
|
151
|
+
},
|
|
152
|
+
],
|
|
153
|
+
"extra_data": {"localidad": ""},
|
|
154
|
+
"razon_social": "Mi razón social",
|
|
155
|
+
"regimen_de_capital": "SA DE CV",
|
|
156
|
+
"fecha_constitucion": datetime(2019, 2, 21),
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## Modelos
|
|
161
|
+
|
|
162
|
+
- `Persona` es la base de `PersonaFisica` y `PersonaMoral`; sus campos se
|
|
163
|
+
exponen como atributos en `snake_case` (`rfc`, `id_cif`, `colonia`, …).
|
|
164
|
+
- `rfc: Rfc | None` e `id_cif: IdCif | None` usan los value objects que validan
|
|
165
|
+
el formato tanto al construir como al asignar (pydantic normaliza las cadenas
|
|
166
|
+
automáticamente).
|
|
167
|
+
- `is_fisica()` e `is_moral()` permiten distinguir el tipo de persona sin
|
|
168
|
+
inspeccionar el RFC por fuera de la librería.
|
|
169
|
+
- `Regimen` se construye con el texto del régimen y, opcionalmente, la fecha de
|
|
170
|
+
alta: `Regimen(regimen="Régimen Simplificado de Confianza", fecha_alta="01-02-2001")`.
|
|
171
|
+
El atributo `regimen_id` se calcula a partir del texto y `fecha_alta` acepta
|
|
172
|
+
cadenas `dd-mm-yyyy` o `datetime`.
|
|
173
|
+
- `extra_data` reúne los títulos del HTML que no corresponden a un campo del
|
|
174
|
+
modelo. La correspondencia título → campo está en `Persona.field_titles`.
|
|
175
|
+
- `to_dict()` es una envoltura de `model_dump()`; `model_dump_json()` serializa
|
|
176
|
+
las fechas en ISO-8601.
|
|
177
|
+
|
|
178
|
+
## Value objects
|
|
179
|
+
|
|
180
|
+
| Value object | Regla |
|
|
181
|
+
| ------------ | ----------------------------------------------------------------------------------------------------------------------------------------- |
|
|
182
|
+
| `Rfc` | `3–4 letras` (incluye `Ñ` y `&`) + `6 dígitos` + `3 alfanuméricos`; normaliza espacios/mayúsculas. `is_fisica()` (13) / `is_moral()` (12) |
|
|
183
|
+
| `IdCif` | solo dígitos, longitud 8–13 |
|
|
184
|
+
| `CsfImage` | imagen JPEG o PNG validada por _magic bytes_; expone `as_bytes()`, `as_base64()` y `get_mime_type()` |
|
|
185
|
+
|
|
186
|
+
Los value objects son subclases de `str`, por lo que se comportan como cadenas,
|
|
187
|
+
se serializan como texto plano y lanzan la excepción personalizada
|
|
188
|
+
correspondiente si el formato no es válido:
|
|
189
|
+
|
|
190
|
+
```python
|
|
191
|
+
from csf_scraper import InvalidIdCifFormatException, InvalidRfcFormatException, Rfc
|
|
192
|
+
|
|
193
|
+
rfc = Rfc(" dim8701081la ") # → "DIM8701081LA"
|
|
194
|
+
rfc.is_moral() # → True
|
|
195
|
+
|
|
196
|
+
try:
|
|
197
|
+
Rfc("no-es-un-rfc")
|
|
198
|
+
except InvalidRfcFormatException as exception:
|
|
199
|
+
print(exception)
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
## Lectura de CSF
|
|
203
|
+
|
|
204
|
+
La librería separa dos responsabilidades que es fácil confundir:
|
|
205
|
+
|
|
206
|
+
- **Origen**: de dónde salen los datos (una ruta de archivo). Contrato público
|
|
207
|
+
`RfcCifExtractorPort.extract(path) -> RfcCifData`.
|
|
208
|
+
- **Motor**: cómo se lee una imagen ya cargada en memoria. Contrato
|
|
209
|
+
`ImageReaderPort.read(image) -> RfcCifData`.
|
|
210
|
+
|
|
211
|
+
`Scraper` solo habla con `RfcCifExtractorPort`, de modo que no distingue un PDF
|
|
212
|
+
(`CsfPdfExtractor`) de una imagen: cualquier extractor que cumpla el contrato
|
|
213
|
+
sirve para ambas rutas.
|
|
214
|
+
|
|
215
|
+
```mermaid
|
|
216
|
+
flowchart LR
|
|
217
|
+
S[Scraper] -->|obtain_from_pdf_path| P[CsfPdfExtractor]
|
|
218
|
+
S -->|obtain_from_image_path| B[CsfImageExtractor]
|
|
219
|
+
B -->|ImageReaderPort| G[GeminiImageReader]
|
|
220
|
+
G -->|hereda| L[LlmImageReaderService]
|
|
221
|
+
B -.-> T[OCR: OpenCV + Tesseract]
|
|
222
|
+
P --> R[RfcCifData]
|
|
223
|
+
B --> R
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
### Contrato de los extractores (`RfcCifExtractorPort`)
|
|
227
|
+
|
|
228
|
+
```python
|
|
229
|
+
class RfcCifExtractorPort(ABC):
|
|
230
|
+
@abstractmethod
|
|
231
|
+
async def extract(self, path: str) -> RfcCifData: ...
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
- El contrato es **asíncrono**: `extract` es una corrutina para que cada
|
|
235
|
+
implementación pueda delegar E/S (procesos externos, disco) sin bloquear el
|
|
236
|
+
bucle de eventos; `Scraper` la consume con `await`.
|
|
237
|
+
- La ruta viaja **en el método**, no en el constructor: los extractores son
|
|
238
|
+
sin estado y reutilizables entre archivos.
|
|
239
|
+
- El constructor queda reservado para configuración del motor (API key, modelo,
|
|
240
|
+
binario alternativo…).
|
|
241
|
+
- Devuelve siempre `RfcCifData` con `rfc: Rfc` e `id_cif: IdCif` ya validados.
|
|
242
|
+
- Los adaptadores de la librería reportan sus fallos con `CsfScraperException`;
|
|
243
|
+
las dependencias inyectadas pueden propagar sus propias excepciones.
|
|
244
|
+
|
|
245
|
+
Implementaciones: `CsfPdfExtractor` (texto de `pdftotext` + `PdfCsfTextParser`)
|
|
246
|
+
y `CsfImageExtractor` (puente entre una ruta y cualquier `ImageReaderPort`).
|
|
247
|
+
|
|
248
|
+
### Contrato de los motores de imagen (`ImageReaderPort`)
|
|
249
|
+
|
|
250
|
+
```python
|
|
251
|
+
class ImageReaderPort(ABC):
|
|
252
|
+
@abstractmethod
|
|
253
|
+
async def read(self, image: CsfImage) -> RfcCifData: ...
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
- Recibe una imagen **en memoria**; nunca abre archivos ni conoce rutas.
|
|
257
|
+
- La validación de formato (JPEG/PNG) ocurre al construir `CsfImage`, así que el
|
|
258
|
+
motor puede asumir que la imagen es válida.
|
|
259
|
+
|
|
260
|
+
### Sub-contrato de los adaptadores basados en LLM
|
|
261
|
+
|
|
262
|
+
Los adaptadores de Gemini, DeepSeek, etc. heredan de `LlmImageReaderService` e
|
|
263
|
+
implementan `ImageReaderPort`. La base aporta el comportamiento común; el
|
|
264
|
+
adaptador crea su cliente de LlamaIndex y expone `read()` delegando en el método
|
|
265
|
+
plantilla.
|
|
266
|
+
|
|
267
|
+
```python
|
|
268
|
+
class LlmImageReaderService(ABC):
|
|
269
|
+
prompt: ClassVar[str] = CSF_PROMPT
|
|
270
|
+
provider: str # propiedad abstracta
|
|
271
|
+
model: str
|
|
272
|
+
|
|
273
|
+
async def _run_program(
|
|
274
|
+
self, image: CsfImage, llm_provider: LLM
|
|
275
|
+
) -> RfcCifData: # plantilla
|
|
276
|
+
...
|
|
277
|
+
|
|
278
|
+
async def _image_to_schema(
|
|
279
|
+
self,
|
|
280
|
+
image_documents: Sequence[ImageBlock],
|
|
281
|
+
retry_prompt: str,
|
|
282
|
+
llm_provider: LLM,
|
|
283
|
+
) -> RfcCifSchema:
|
|
284
|
+
...
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
`LlmImageReaderService` solo tiene cuatro responsabilidades:
|
|
288
|
+
|
|
289
|
+
1. **Armado del prompt**: `CSF_PROMPT` más la realimentación del intento previo.
|
|
290
|
+
2. **Armado del mensaje con la imagen**: un único `ImageBlock` con los bytes y
|
|
291
|
+
el MIME type de la `CsfImage`.
|
|
292
|
+
3. **Reintentos**: cuando `Rfc` o `IdCif` rechazan el formato de negocio, se
|
|
293
|
+
vuelve a interrogar al modelo pasándole el error como contexto (hasta
|
|
294
|
+
`max_retries`, por omisión `MAX_RETRIES = 3`). Los fallos de transporte _no_
|
|
295
|
+
se reintentan: se envuelven en `ImageReadingException`.
|
|
296
|
+
4. **Parseo al DTO**: un `RfcCifSchema` ya tipado se mapea a `RfcCifData`.
|
|
297
|
+
|
|
298
|
+
| Responsabilidad | Dónde vive |
|
|
299
|
+
| ----------------------- | ----------------------------------------------------------- |
|
|
300
|
+
| Prompt canónico | constante `CSF_PROMPT` (+ `RETRY_PROMPT`) |
|
|
301
|
+
| Lectura del archivo | `CsfImage.new_from_path`, invocado por `CsfImageExtractor` |
|
|
302
|
+
| Forma de la respuesta | `RfcCifSchema` (`Field` con `description` para el modelo) |
|
|
303
|
+
| Transporte al proveedor | el adaptador crea `llm_provider` y lo pasa a `_run_program` |
|
|
304
|
+
| Validación de RFC/idCIF | value objects `Rfc` / `IdCif` |
|
|
305
|
+
| Mapeo de errores | `ImageReadingException` y derivadas |
|
|
306
|
+
|
|
307
|
+
Nota: la imagen se lee con `asyncio.to_thread` dentro de `CsfImageExtractor` y
|
|
308
|
+
`pdftotext` se ejecuta con `asyncio.to_thread` dentro de `PdfToText`, porque
|
|
309
|
+
ambas operaciones son bloqueantes; el resto del flujo (HTTP, LLM) usa APIs
|
|
310
|
+
asíncronas.
|
|
311
|
+
|
|
312
|
+
`RfcCifSchema` es un `BaseModel` cuyos `Field` llevan `description`; esos textos
|
|
313
|
+
son los que el modelo multimodal usa para saber qué aspecto tienen el RFC y el
|
|
314
|
+
idCIF impresos en la Constancia de Situación Fiscal. Pydantic valida la _forma_
|
|
315
|
+
(campos presentes y de tipo `str`) y los value objects validan el _formato de
|
|
316
|
+
negocio_, que es justo lo que dispara los reintentos.
|
|
317
|
+
|
|
318
|
+
El adaptador crea su cliente de LlamaIndex (`GoogleGenAI`) y lo pasa al método
|
|
319
|
+
plantilla; `_image_to_schema` construye y ejecuta el
|
|
320
|
+
`MultiModalLLMCompletionProgram`:
|
|
321
|
+
|
|
322
|
+
```python
|
|
323
|
+
class GeminiImageReader(LlmImageReaderService, ImageReaderPort):
|
|
324
|
+
def __init__(
|
|
325
|
+
self, api_key: str, model_name: str, *, max_retries: int = MAX_RETRIES
|
|
326
|
+
) -> None:
|
|
327
|
+
super().__init__(model=model_name, max_retries=max_retries)
|
|
328
|
+
self._llm_provider = GoogleGenAI(api_key=api_key, model=model_name)
|
|
329
|
+
|
|
330
|
+
@property
|
|
331
|
+
def provider(self) -> str:
|
|
332
|
+
return "gemini"
|
|
333
|
+
|
|
334
|
+
async def read(self, image: CsfImage) -> RfcCifSchema:
|
|
335
|
+
return await self._run_program(
|
|
336
|
+
image=image, llm_provider=self._llm_provider
|
|
337
|
+
)
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
El extra `llm-gemini` instala `llama-index-llms-google-genai`; el resto de la
|
|
341
|
+
librería no depende de ningún proveedor.
|
|
342
|
+
|
|
343
|
+
Por omisión `Scraper` solo sabe leer PDFs; el extractor de imágenes es
|
|
344
|
+
**opcional** y se inyecta con `Scraper.create(image_extractor=…)`. Si se invoca
|
|
345
|
+
`obtain_from_image_path` sin extractor configurado se lanza
|
|
346
|
+
`ImageExtractorNotConfiguredException`.
|
|
347
|
+
|
|
348
|
+
## Excepciones
|
|
349
|
+
|
|
350
|
+
Todas heredan de `CsfScraperException`:
|
|
351
|
+
|
|
352
|
+
- `CifDownloadException` (la petición HTTP falló; conserva `url`; la excepción
|
|
353
|
+
original queda encadenada en `__cause__`)
|
|
354
|
+
- `CifNotFoundException` (el SAT no devolvió información del CIF)
|
|
355
|
+
- `RfcFromPdfNotFoundException` / `CifFromPdfNotFoundException` (el PDF no
|
|
356
|
+
contiene RFC / idCIF)
|
|
357
|
+
- `EmptyPdfContentException` / `PdfToTextConvertException` (problemas con
|
|
358
|
+
`pdftotext`)
|
|
359
|
+
- `RfcFromImageNotFoundException` / `CifFromImageNotFoundException` (la imagen
|
|
360
|
+
no contiene RFC / idCIF)
|
|
361
|
+
- `ImageExtractorNotConfiguredException` (se pidió leer una imagen sin inyectar
|
|
362
|
+
un extractor)
|
|
363
|
+
- `InvalidImageException` / `InvalidImageFormatException` (imagen vacía, ilegible
|
|
364
|
+
o con formato distinto de JPEG/PNG)
|
|
365
|
+
- `ImageReadingException` (fallo del motor de lectura de imágenes; conserva
|
|
366
|
+
`provider` y `model`)
|
|
367
|
+
- `InvalidRfcFormatException` / `InvalidIdCifFormatException` (formato inválido
|
|
368
|
+
en los value objects `Rfc` / `IdCif`)
|
|
369
|
+
|
|
370
|
+
## Desarrollo
|
|
371
|
+
|
|
372
|
+
```bash
|
|
373
|
+
python -m venv .venv
|
|
374
|
+
.venv/bin/pip install -e ".[dev]"
|
|
375
|
+
.venv/bin/ruff check .
|
|
376
|
+
.venv/bin/mypy
|
|
377
|
+
.venv/bin/pytest
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
Los tests usan fixtures de `tests/_files/` y mockean HTTP con `respx`:
|
|
381
|
+
**ningún test contacta al SAT**.
|
|
382
|
+
|
|
383
|
+
## Documentación de la migración
|
|
384
|
+
|
|
385
|
+
- `docs/migration-map.md`: correspondencia archivo a archivo PHP → Python.
|
|
386
|
+
- `docs/differences.md`: diferencias de comportamiento deliberadas.
|
|
387
|
+
|
|
388
|
+
## Licencia
|
|
389
|
+
|
|
390
|
+
MIT — ver `LICENSE` (heredado del original) y `NOTICE.md`.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
csf_scraper/__init__.py,sha256=PaumGzitsC5f_ZzITtAcc7rGqEE9Leo3Z1C5RnvVg2s,3623
|
|
2
|
+
csf_scraper/data_extractor.py,sha256=c2d48ISxK_-yFViRCdZ6Vfqrv7PChrdr3MNpoxrZ2oA,2815
|
|
3
|
+
csf_scraper/date_parser.py,sha256=bQpBXhA-Git3dOVdij98pqj1ScDU0DmQdB1rNgO9tUk,797
|
|
4
|
+
csf_scraper/persona.py,sha256=ZOiO4WEbufA4teRm265GuRlW2oCCcyUhmKJRBA-wJwo,2445
|
|
5
|
+
csf_scraper/persona_fisica.py,sha256=_ar4BhzZsCiY24unYibpAq3sDXd54M-mqGooOmmUC48,659
|
|
6
|
+
csf_scraper/persona_moral.py,sha256=4CHSmVqvxVf2xD6XMZ5Ij0NuOX4DOwBKTpgw9b7fyp4,582
|
|
7
|
+
csf_scraper/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
+
csf_scraper/regimen.py,sha256=r0_wblU4dMNvkc0KoAaQ6P10oGTDBuSIm_RhDMsnxeo,2180
|
|
9
|
+
csf_scraper/rfc_cif_data.py,sha256=7UHNocdf-y8Cpb0BHtOt5JAazZ-aCOg019aWUPJTa9Q,404
|
|
10
|
+
csf_scraper/scraper.py,sha256=FqAxYFaXgCM7D1DhkTS2H3CSJ8BnKSs2DWAoK-HS9fI,4418
|
|
11
|
+
csf_scraper/exceptions/__init__.py,sha256=Rio4OEagZmAmxge1TQ4CsjhyUY1QEvLca3Uor9SFGo4,506
|
|
12
|
+
csf_scraper/exceptions/image_reader.py,sha256=Acu0iCF11Z6XKpeSNn-s8HRfWzM-4RESdOYNmX-Wdq0,3529
|
|
13
|
+
csf_scraper/exceptions/pdf_reader.py,sha256=zkPv7gbsvWO-Vs7vc_stt8YbG_PTLDoq_Nibeh9vvZk,845
|
|
14
|
+
csf_scraper/exceptions/value_objects.py,sha256=Kc2FUWud_Tc_LBhgGUMebO67ScI1CbNUQYvo0sQBp4Y,378
|
|
15
|
+
csf_scraper/image_readers/__init__.py,sha256=0RRLSc0nOurpV8DwE4h5d6hUlYjvuGxrjKsoXAVOr_Q,1591
|
|
16
|
+
csf_scraper/image_readers/csf_image_extractor.py,sha256=wnQFUI1BADAlbni0qK98bQhP6saGw6tq1fL-amqPWP8,1271
|
|
17
|
+
csf_scraper/image_readers/adapters/__init__.py,sha256=tO-N2ZCUo1E751TeHVkvLO1wWxUbEkG8PT8KryFxGGU,534
|
|
18
|
+
csf_scraper/image_readers/adapters/llms/__init__.py,sha256=FR0OzKO5lxMriOGS_MYaSd9RoNJ30-HaEsN7YvIGIug,1139
|
|
19
|
+
csf_scraper/image_readers/adapters/llms/gemini_image_reader.py,sha256=NCTlBBVsNUNhmSHX7ZixZaBqlkTSz1ds40RZFXpGVbw,6003
|
|
20
|
+
csf_scraper/image_readers/adapters/llms/prompts/__init__.py,sha256=tGau_NVzj8qxgfkq4IW8qNZC4XJ267NGZKbuHdXoizA,240
|
|
21
|
+
csf_scraper/image_readers/adapters/llms/prompts/llm_image_reader_prompt.py,sha256=ZZJ62ZdJQfft6elX7YxWZLiM7ehogvzd3pqBjjs-N9g,825
|
|
22
|
+
csf_scraper/image_readers/ports/__init__.py,sha256=l83jE3ANVV7-0m6yzjueGLMhBmjHVn7Yw9TcDISILGw,151
|
|
23
|
+
csf_scraper/image_readers/ports/image_reader_port.py,sha256=2beSoqtlb8jUQJHbyH1it64yw-WANPS7zJR0qLQ1OA0,1550
|
|
24
|
+
csf_scraper/image_readers/schemas/__init__.py,sha256=p57k00upoHpZLQLtLy6nCkij1tJQlXA2q9l5COJN72w,174
|
|
25
|
+
csf_scraper/image_readers/schemas/rfc_cif_schema.py,sha256=ehAa062HN77Ibrwt_hY-0uIlqXcD8G0Ms-WNTSGCLe0,2054
|
|
26
|
+
csf_scraper/image_readers/services/__init__.py,sha256=ckTPY41nXPIzZBgaPJ4XAYxZU4gsNdC-NFdrrMqkF1k,465
|
|
27
|
+
csf_scraper/image_readers/services/llm_image_reader_service.py,sha256=n4H7Md4iKmaT1NMzxQM3hmLL02KzCk473MDATjvfeZk,13416
|
|
28
|
+
csf_scraper/pdf_reader/__init__.py,sha256=ji-BFrQQ7iwL3dgwEDluBBDjo4_jvnuuqYi7l0yC4vE,359
|
|
29
|
+
csf_scraper/pdf_reader/csf_pdf_extractor.py,sha256=DLH_-VR3-MfMpqefQ-w2WuUVE-n_eV0vuBpCTnNRI4s,1324
|
|
30
|
+
csf_scraper/pdf_reader/pdf_csf_text_parser.py,sha256=ZPIB4uulcFx4ewaUChhpQZ7b0fdi37H7NNxm-ie0pjA,2094
|
|
31
|
+
csf_scraper/pdf_reader/pdf_to_text.py,sha256=pw-9t2ZYqbMpCsmEk0KUSlpJL3wWTy1XzDmg4AG0eFE,1530
|
|
32
|
+
csf_scraper/ports/__init__.py,sha256=RPvaYHzzpcX40M6E1iUQIFCeK_hVjetWYoS1YaXtUu4,153
|
|
33
|
+
csf_scraper/ports/rfc_cif_extractor_port.py,sha256=V51_vAatCvjx6C3V46LWDm41_DvYm7HP0wPVtsLuXmU,1830
|
|
34
|
+
csf_scraper/value_objects/__init__.py,sha256=fksrywY6MHNYgSKpy6VLWXMvdfXi8Tjx3xRqI7B-9s4,238
|
|
35
|
+
csf_scraper/value_objects/csf_image.py,sha256=RzU0ZO8JomxYVPTaqARXaI_NV76ZO2zM5N_PforNE8A,2332
|
|
36
|
+
csf_scraper/value_objects/id_cif.py,sha256=8ZVVPgNEM_AeEVe-dk8x0b-x1NObW9bNhMGNVD3R6LU,1564
|
|
37
|
+
csf_scraper/value_objects/rfc.py,sha256=jLkrF0_DKC6ETV66GQFQNU3BiBxRh9ffaHdPciDoCn0,1987
|
|
38
|
+
csf_scraper_python-0.1.1.dist-info/METADATA,sha256=YNQ51ihu850s_QccnXaVP54Y2gzdSZDB38cp6b66V74,14955
|
|
39
|
+
csf_scraper_python-0.1.1.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
40
|
+
csf_scraper_python-0.1.1.dist-info/licenses/LICENSE,sha256=c9W1BLxpJQUZ21EogN1_nyVGgB0SdXqmpc5c0lvpnQI,1106
|
|
41
|
+
csf_scraper_python-0.1.1.dist-info/licenses/NOTICE.md,sha256=-0Mi9rUvfhYtXNRnDlX3SZAv8K3zU2okd5e4vg4q_sA,901
|
|
42
|
+
csf_scraper_python-0.1.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2022 - 2025 PhpCfdi https://www.phpcfdi.com/
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# NOTICE
|
|
2
|
+
|
|
3
|
+
Este proyecto es un **port a Python** de la librería PHP
|
|
4
|
+
[`phpcfdi/csf-scraper`](https://github.com/phpcfdi/csf-scraper).
|
|
5
|
+
|
|
6
|
+
- **Proyecto original**: phpcfdi/csf-scraper
|
|
7
|
+
- **Repositorio**: <https://github.com/phpcfdi/csf-scraper>
|
|
8
|
+
- **Tag de origen migrado**: `v0.2.0`
|
|
9
|
+
- **Commit de origen migrado**: `c66aab5fc1997da5de0ace9cea38274b86bfeaaf` (`v0.2.0-1-gc66aab5`)
|
|
10
|
+
- **Licencia original**: MIT ( véase [LICENSE](LICENSE), copyright conservado de los autores originales)
|
|
11
|
+
- **Autoría original**: Cesar Aguilera, Misael Limón, Fernando Isidro y colaboradores de [PhpCfdi](https://www.phpcfdi.com/)
|
|
12
|
+
|
|
13
|
+
Todo el código fuente de este port se deriva del proyecto original; la lógica de
|
|
14
|
+
negocio, los nombres de dominio, los mensajes de las excepciones y los fixtures
|
|
15
|
+
de pruebas provienen de `phpcfdi/csf-scraper` y fueron migrados a Python
|
|
16
|
+
(httpx, BeautifulSoup4 + lxml, pydantic, pytest, respx).
|