csf-scraper-python 0.1.1__tar.gz → 0.2.0__tar.gz

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 (109) hide show
  1. csf_scraper_python-0.2.0/.github/workflows/publish.yml +103 -0
  2. {csf_scraper_python-0.1.1 → csf_scraper_python-0.2.0}/PKG-INFO +119 -8
  3. {csf_scraper_python-0.1.1 → csf_scraper_python-0.2.0}/README.md +114 -7
  4. {csf_scraper_python-0.1.1 → csf_scraper_python-0.2.0}/docs/migration-map.md +43 -40
  5. {csf_scraper_python-0.1.1 → csf_scraper_python-0.2.0}/pyproject.toml +6 -1
  6. {csf_scraper_python-0.1.1 → csf_scraper_python-0.2.0}/src/csf_scraper/__init__.py +7 -0
  7. csf_scraper_python-0.2.0/src/csf_scraper/image_readers/adapters/cpu/__init__.py +18 -0
  8. csf_scraper_python-0.2.0/src/csf_scraper/image_readers/adapters/cpu/open_cv/__init__.py +37 -0
  9. csf_scraper_python-0.2.0/src/csf_scraper/image_readers/adapters/cpu/open_cv/config.py +84 -0
  10. csf_scraper_python-0.2.0/src/csf_scraper/image_readers/adapters/cpu/open_cv/detection.py +269 -0
  11. csf_scraper_python-0.2.0/src/csf_scraper/image_readers/adapters/cpu/open_cv/enhance.py +103 -0
  12. csf_scraper_python-0.2.0/src/csf_scraper/image_readers/adapters/cpu/open_cv/errors.py +18 -0
  13. csf_scraper_python-0.2.0/src/csf_scraper/image_readers/adapters/cpu/open_cv/io.py +31 -0
  14. csf_scraper_python-0.2.0/src/csf_scraper/image_readers/adapters/cpu/open_cv/pipeline.py +89 -0
  15. csf_scraper_python-0.2.0/src/csf_scraper/image_readers/adapters/cpu/open_cv/qr.py +108 -0
  16. csf_scraper_python-0.2.0/src/csf_scraper/image_readers/adapters/cpu/open_cv/transform.py +97 -0
  17. csf_scraper_python-0.2.0/src/csf_scraper/image_readers/adapters/cpu/open_cv_image_reader.py +56 -0
  18. {csf_scraper_python-0.1.1 → csf_scraper_python-0.2.0}/src/csf_scraper/scraper.py +14 -3
  19. {csf_scraper_python-0.1.1 → csf_scraper_python-0.2.0}/.gitignore +0 -0
  20. {csf_scraper_python-0.1.1 → csf_scraper_python-0.2.0}/LICENSE +0 -0
  21. {csf_scraper_python-0.1.1 → csf_scraper_python-0.2.0}/NOTICE.md +0 -0
  22. {csf_scraper_python-0.1.1 → csf_scraper_python-0.2.0}/docs/differences.md +0 -0
  23. {csf_scraper_python-0.1.1 → csf_scraper_python-0.2.0}/src/csf_scraper/data_extractor.py +0 -0
  24. {csf_scraper_python-0.1.1 → csf_scraper_python-0.2.0}/src/csf_scraper/date_parser.py +0 -0
  25. {csf_scraper_python-0.1.1 → csf_scraper_python-0.2.0}/src/csf_scraper/exceptions/__init__.py +0 -0
  26. {csf_scraper_python-0.1.1 → csf_scraper_python-0.2.0}/src/csf_scraper/exceptions/image_reader.py +0 -0
  27. {csf_scraper_python-0.1.1 → csf_scraper_python-0.2.0}/src/csf_scraper/exceptions/pdf_reader.py +0 -0
  28. {csf_scraper_python-0.1.1 → csf_scraper_python-0.2.0}/src/csf_scraper/exceptions/value_objects.py +0 -0
  29. {csf_scraper_python-0.1.1 → csf_scraper_python-0.2.0}/src/csf_scraper/image_readers/__init__.py +0 -0
  30. {csf_scraper_python-0.1.1 → csf_scraper_python-0.2.0}/src/csf_scraper/image_readers/adapters/__init__.py +0 -0
  31. {csf_scraper_python-0.1.1 → csf_scraper_python-0.2.0}/src/csf_scraper/image_readers/adapters/llms/__init__.py +0 -0
  32. {csf_scraper_python-0.1.1 → csf_scraper_python-0.2.0}/src/csf_scraper/image_readers/adapters/llms/gemini_image_reader.py +0 -0
  33. {csf_scraper_python-0.1.1 → csf_scraper_python-0.2.0}/src/csf_scraper/image_readers/adapters/llms/prompts/__init__.py +0 -0
  34. {csf_scraper_python-0.1.1 → csf_scraper_python-0.2.0}/src/csf_scraper/image_readers/adapters/llms/prompts/llm_image_reader_prompt.py +0 -0
  35. {csf_scraper_python-0.1.1 → csf_scraper_python-0.2.0}/src/csf_scraper/image_readers/csf_image_extractor.py +0 -0
  36. {csf_scraper_python-0.1.1 → csf_scraper_python-0.2.0}/src/csf_scraper/image_readers/ports/__init__.py +0 -0
  37. {csf_scraper_python-0.1.1 → csf_scraper_python-0.2.0}/src/csf_scraper/image_readers/ports/image_reader_port.py +0 -0
  38. {csf_scraper_python-0.1.1 → csf_scraper_python-0.2.0}/src/csf_scraper/image_readers/schemas/__init__.py +0 -0
  39. {csf_scraper_python-0.1.1 → csf_scraper_python-0.2.0}/src/csf_scraper/image_readers/schemas/rfc_cif_schema.py +0 -0
  40. {csf_scraper_python-0.1.1 → csf_scraper_python-0.2.0}/src/csf_scraper/image_readers/services/__init__.py +0 -0
  41. {csf_scraper_python-0.1.1 → csf_scraper_python-0.2.0}/src/csf_scraper/image_readers/services/llm_image_reader_service.py +0 -0
  42. {csf_scraper_python-0.1.1 → csf_scraper_python-0.2.0}/src/csf_scraper/pdf_reader/__init__.py +0 -0
  43. {csf_scraper_python-0.1.1 → csf_scraper_python-0.2.0}/src/csf_scraper/pdf_reader/csf_pdf_extractor.py +0 -0
  44. {csf_scraper_python-0.1.1 → csf_scraper_python-0.2.0}/src/csf_scraper/pdf_reader/pdf_csf_text_parser.py +0 -0
  45. {csf_scraper_python-0.1.1 → csf_scraper_python-0.2.0}/src/csf_scraper/pdf_reader/pdf_to_text.py +0 -0
  46. {csf_scraper_python-0.1.1 → csf_scraper_python-0.2.0}/src/csf_scraper/persona.py +0 -0
  47. {csf_scraper_python-0.1.1 → csf_scraper_python-0.2.0}/src/csf_scraper/persona_fisica.py +0 -0
  48. {csf_scraper_python-0.1.1 → csf_scraper_python-0.2.0}/src/csf_scraper/persona_moral.py +0 -0
  49. {csf_scraper_python-0.1.1 → csf_scraper_python-0.2.0}/src/csf_scraper/ports/__init__.py +0 -0
  50. {csf_scraper_python-0.1.1 → csf_scraper_python-0.2.0}/src/csf_scraper/ports/rfc_cif_extractor_port.py +0 -0
  51. {csf_scraper_python-0.1.1 → csf_scraper_python-0.2.0}/src/csf_scraper/py.typed +0 -0
  52. {csf_scraper_python-0.1.1 → csf_scraper_python-0.2.0}/src/csf_scraper/regimen.py +0 -0
  53. {csf_scraper_python-0.1.1 → csf_scraper_python-0.2.0}/src/csf_scraper/rfc_cif_data.py +0 -0
  54. {csf_scraper_python-0.1.1 → csf_scraper_python-0.2.0}/src/csf_scraper/value_objects/__init__.py +0 -0
  55. {csf_scraper_python-0.1.1 → csf_scraper_python-0.2.0}/src/csf_scraper/value_objects/csf_image.py +0 -0
  56. {csf_scraper_python-0.1.1 → csf_scraper_python-0.2.0}/src/csf_scraper/value_objects/id_cif.py +0 -0
  57. {csf_scraper_python-0.1.1 → csf_scraper_python-0.2.0}/src/csf_scraper/value_objects/rfc.py +0 -0
  58. {csf_scraper_python-0.1.1 → csf_scraper_python-0.2.0}/tests/__init__.py +0 -0
  59. {csf_scraper_python-0.1.1 → csf_scraper_python-0.2.0}/tests/_files/csf-bad-content.txt +0 -0
  60. {csf_scraper_python-0.1.1 → csf_scraper_python-0.2.0}/tests/_files/csf-content.txt +0 -0
  61. {csf_scraper_python-0.1.1 → csf_scraper_python-0.2.0}/tests/_files/csf-correct-but-invalid.pdf +0 -0
  62. {csf_scraper_python-0.1.1 → csf_scraper_python-0.2.0}/tests/_files/csf-without-cif.pdf +0 -0
  63. {csf_scraper_python-0.1.1 → csf_scraper_python-0.2.0}/tests/_files/csf-without-rfc.pdf +0 -0
  64. {csf_scraper_python-0.1.1 → csf_scraper_python-0.2.0}/tests/_files/error.html +0 -0
  65. {csf_scraper_python-0.1.1 → csf_scraper_python-0.2.0}/tests/_files/scrap_fisica.html +0 -0
  66. {csf_scraper_python-0.1.1 → csf_scraper_python-0.2.0}/tests/_files/scrap_moral.html +0 -0
  67. {csf_scraper_python-0.1.1 → csf_scraper_python-0.2.0}/tests/_files/scrap_moral_without_colonia.html +0 -0
  68. {csf_scraper_python-0.1.1 → csf_scraper_python-0.2.0}/tests/_files/scrap_regimenes.html +0 -0
  69. {csf_scraper_python-0.1.1 → csf_scraper_python-0.2.0}/tests/helpers.py +0 -0
  70. {csf_scraper_python-0.1.1 → csf_scraper_python-0.2.0}/tests/integration/__init__.py +0 -0
  71. {csf_scraper_python-0.1.1 → csf_scraper_python-0.2.0}/tests/integration/helpers.py +0 -0
  72. {csf_scraper_python-0.1.1 → csf_scraper_python-0.2.0}/tests/integration/test_scrap_csf_from_rfc_and_cif.py +0 -0
  73. {csf_scraper_python-0.1.1 → csf_scraper_python-0.2.0}/tests/integration/test_scrap_from_csf_file.py +0 -0
  74. {csf_scraper_python-0.1.1 → csf_scraper_python-0.2.0}/tests/unit/__init__.py +0 -0
  75. {csf_scraper_python-0.1.1 → csf_scraper_python-0.2.0}/tests/unit/exceptions/__init__.py +0 -0
  76. {csf_scraper_python-0.1.1 → csf_scraper_python-0.2.0}/tests/unit/exceptions/pdf_reader/__init__.py +0 -0
  77. {csf_scraper_python-0.1.1 → csf_scraper_python-0.2.0}/tests/unit/exceptions/pdf_reader/test_cif_from_pdf_not_found_exception.py +0 -0
  78. {csf_scraper_python-0.1.1 → csf_scraper_python-0.2.0}/tests/unit/exceptions/pdf_reader/test_empty_pdf_content_exception.py +0 -0
  79. {csf_scraper_python-0.1.1 → csf_scraper_python-0.2.0}/tests/unit/exceptions/pdf_reader/test_pdf_to_text_convert_exception.py +0 -0
  80. {csf_scraper_python-0.1.1 → csf_scraper_python-0.2.0}/tests/unit/exceptions/pdf_reader/test_rfc_from_pdf_not_found_exception.py +0 -0
  81. {csf_scraper_python-0.1.1 → csf_scraper_python-0.2.0}/tests/unit/exceptions/test_cif_download_exception.py +0 -0
  82. {csf_scraper_python-0.1.1 → csf_scraper_python-0.2.0}/tests/unit/exceptions/test_cif_not_found_exception.py +0 -0
  83. {csf_scraper_python-0.1.1 → csf_scraper_python-0.2.0}/tests/unit/image_readers/__init__.py +0 -0
  84. {csf_scraper_python-0.1.1 → csf_scraper_python-0.2.0}/tests/unit/image_readers/test_csf_image_extractor.py +0 -0
  85. {csf_scraper_python-0.1.1 → csf_scraper_python-0.2.0}/tests/unit/image_readers/test_llm_image_reader.py +0 -0
  86. {csf_scraper_python-0.1.1 → csf_scraper_python-0.2.0}/tests/unit/pdf_reader/__init__.py +0 -0
  87. {csf_scraper_python-0.1.1 → csf_scraper_python-0.2.0}/tests/unit/pdf_reader/pdf_csf_text_extractor/__init__.py +0 -0
  88. {csf_scraper_python-0.1.1 → csf_scraper_python-0.2.0}/tests/unit/pdf_reader/pdf_csf_text_extractor/test_extract.py +0 -0
  89. {csf_scraper_python-0.1.1 → csf_scraper_python-0.2.0}/tests/unit/pdf_reader/pdf_csf_text_extractor/test_get_cif_id.py +0 -0
  90. {csf_scraper_python-0.1.1 → csf_scraper_python-0.2.0}/tests/unit/pdf_reader/pdf_csf_text_extractor/test_get_rfc.py +0 -0
  91. {csf_scraper_python-0.1.1 → csf_scraper_python-0.2.0}/tests/unit/pdf_reader/pdf_to_text/__init__.py +0 -0
  92. {csf_scraper_python-0.1.1 → csf_scraper_python-0.2.0}/tests/unit/pdf_reader/pdf_to_text/test_pdf_to_text_constructor.py +0 -0
  93. {csf_scraper_python-0.1.1 → csf_scraper_python-0.2.0}/tests/unit/persona/__init__.py +0 -0
  94. {csf_scraper_python-0.1.1 → csf_scraper_python-0.2.0}/tests/unit/persona/test_common_persona_properties.py +0 -0
  95. {csf_scraper_python-0.1.1 → csf_scraper_python-0.2.0}/tests/unit/persona/test_persona_fisica_properties.py +0 -0
  96. {csf_scraper_python-0.1.1 → csf_scraper_python-0.2.0}/tests/unit/persona/test_persona_json_serialize.py +0 -0
  97. {csf_scraper_python-0.1.1 → csf_scraper_python-0.2.0}/tests/unit/persona/test_persona_moral_properties.py +0 -0
  98. {csf_scraper_python-0.1.1 → csf_scraper_python-0.2.0}/tests/unit/persona/traits/__init__.py +0 -0
  99. {csf_scraper_python-0.1.1 → csf_scraper_python-0.2.0}/tests/unit/persona/traits/persona_trait.py +0 -0
  100. {csf_scraper_python-0.1.1 → csf_scraper_python-0.2.0}/tests/unit/regimen/__init__.py +0 -0
  101. {csf_scraper_python-0.1.1 → csf_scraper_python-0.2.0}/tests/unit/regimen/test_regimen_json_serialize.py +0 -0
  102. {csf_scraper_python-0.1.1 → csf_scraper_python-0.2.0}/tests/unit/regimen/test_regimenes_json_serialize.py +0 -0
  103. {csf_scraper_python-0.1.1 → csf_scraper_python-0.2.0}/tests/unit/regimen/test_set_regimen.py +0 -0
  104. {csf_scraper_python-0.1.1 → csf_scraper_python-0.2.0}/tests/unit/test_regimenes.py +0 -0
  105. {csf_scraper_python-0.1.1 → csf_scraper_python-0.2.0}/tests/unit/test_scraper.py +0 -0
  106. {csf_scraper_python-0.1.1 → csf_scraper_python-0.2.0}/tests/unit/value_objects/__init__.py +0 -0
  107. {csf_scraper_python-0.1.1 → csf_scraper_python-0.2.0}/tests/unit/value_objects/test_csf_image.py +0 -0
  108. {csf_scraper_python-0.1.1 → csf_scraper_python-0.2.0}/tests/unit/value_objects/test_id_cif.py +0 -0
  109. {csf_scraper_python-0.1.1 → csf_scraper_python-0.2.0}/tests/unit/value_objects/test_rfc.py +0 -0
@@ -0,0 +1,103 @@
1
+ name: Publish
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*"
7
+
8
+ permissions:
9
+ contents: read
10
+
11
+ concurrency:
12
+ group: publish-${{ github.ref }}
13
+ cancel-in-progress: false
14
+
15
+ jobs:
16
+ test:
17
+ runs-on: ubuntu-latest
18
+ steps:
19
+ - uses: actions/checkout@v7
20
+ - uses: actions/setup-python@v7
21
+ with:
22
+ python-version: "3.14.8"
23
+ cache: pip
24
+ cache-dependency-path: pyproject.toml
25
+ - name: Install poppler-utils
26
+ run: sudo apt-get update && sudo apt-get install -y poppler-utils
27
+ - name: Install development dependencies
28
+ run: |
29
+ python -m pip install --upgrade pip
30
+ python -m pip install -e ".[dev,opencv,llm-gemini]"
31
+ - name: Ruff
32
+ run: ruff check src tests
33
+ - name: Mypy
34
+ run: mypy
35
+ - name: Pytest
36
+ run: pytest
37
+ - name: Verify OpenCV extra
38
+ run: |
39
+ python -c "import cv2, numpy; print('OpenCV', cv2.__version__, 'NumPy', numpy.__version__)"
40
+ python -c "from csf_scraper import OpenCvImageReader; print(OpenCvImageReader.__name__)"
41
+
42
+ build:
43
+ needs: test
44
+ runs-on: ubuntu-latest
45
+ steps:
46
+ - uses: actions/checkout@v7
47
+ - uses: actions/setup-python@v7
48
+ with:
49
+ python-version: "3.14.8"
50
+ - name: Install build tools
51
+ run: python -m pip install --upgrade build twine
52
+ - name: Verify version matches tag
53
+ run: |
54
+ set -euo pipefail
55
+ VERSION=$(python -c 'import tomllib; print(tomllib.load(open("pyproject.toml", "rb"))["project"]["version"])')
56
+ echo "pyproject.toml version: v${VERSION}"
57
+ echo "Received tag: ${GITHUB_REF_NAME}"
58
+ test "v${VERSION}" = "${GITHUB_REF_NAME}"
59
+ - name: Build distributions
60
+ run: python -m build
61
+ - name: Check distributions
62
+ run: python -m twine check dist/*
63
+ - name: Upload distributions
64
+ uses: actions/upload-artifact@v7
65
+ with:
66
+ name: dist
67
+ path: dist/
68
+
69
+ publish:
70
+ needs: build
71
+ runs-on: ubuntu-latest
72
+ environment: pypi
73
+ permissions:
74
+ contents: read
75
+ id-token: write
76
+ steps:
77
+ - name: Download distributions
78
+ uses: actions/download-artifact@v8
79
+ with:
80
+ name: dist
81
+ path: dist/
82
+ - name: Publish to PyPI
83
+ uses: pypa/gh-action-pypi-publish@release/v1
84
+
85
+ release:
86
+ needs: publish
87
+ runs-on: ubuntu-latest
88
+ permissions:
89
+ contents: write
90
+ steps:
91
+ - name: Download distributions
92
+ uses: actions/download-artifact@v8
93
+ with:
94
+ name: dist
95
+ path: dist/
96
+ - name: Create GitHub Release
97
+ env:
98
+ GH_TOKEN: ${{ github.token }}
99
+ run: |
100
+ gh release view "$GITHUB_REF_NAME" >/dev/null 2>&1 || \
101
+ gh release create "$GITHUB_REF_NAME" dist/* \
102
+ --title "$GITHUB_REF_NAME" \
103
+ --generate-notes
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: csf-scraper-python
3
- Version: 0.1.1
3
+ Version: 0.2.0
4
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
5
  Project-URL: Homepage, https://github.com/PJMP4826/csf-scraper-python
6
6
  Project-URL: Issues, https://github.com/PJMP4826/csf-scraper-python/issues
@@ -19,6 +19,7 @@ Classifier: Programming Language :: Python :: 3 :: Only
19
19
  Classifier: Programming Language :: Python :: 3.11
20
20
  Classifier: Programming Language :: Python :: 3.12
21
21
  Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Programming Language :: Python :: 3.14
22
23
  Classifier: Typing :: Typed
23
24
  Requires-Python: >=3.11
24
25
  Requires-Dist: beautifulsoup4>=4.12
@@ -35,9 +36,12 @@ Requires-Dist: ruff>=0.6; extra == 'dev'
35
36
  Provides-Extra: llm-gemini
36
37
  Requires-Dist: google-genai>=1.0; extra == 'llm-gemini'
37
38
  Requires-Dist: llama-index-llms-google-genai<0.12,>=0.11; extra == 'llm-gemini'
39
+ Provides-Extra: opencv
40
+ Requires-Dist: numpy>=2; extra == 'opencv'
41
+ Requires-Dist: opencv-python-headless>=5.0; extra == 'opencv'
38
42
  Description-Content-Type: text/markdown
39
43
 
40
- # csf-scraper
44
+ # csf-scraper-python
41
45
 
42
46
  Port a Python de [phpcfdi/csf-scraper](https://github.com/phpcfdi/csf-scraper):
43
47
  obtiene los datos fiscales actuales de una persona moral o física dado su RFC y
@@ -50,7 +54,8 @@ su idCIF (SAT, Constancia de Situación Fiscal).
50
54
  `poppler-utils`), necesario **solo** para leer la CSF desde un archivo PDF
51
55
  (`obtain_from_pdf_path`).
52
56
  - Dependencias: `httpx`, `beautifulsoup4`, `lxml`, `pydantic`, `llama-index-core`.
53
- Los clientes de cada proveedor LLM son extras opcionales (`llm-gemini`).
57
+ Los clientes de cada proveedor LLM son extras opcionales (`llm-gemini`). El
58
+ lector de imágenes basado en OpenCV requiere el extra `opencv`.
54
59
 
55
60
  ## Instalación
56
61
 
@@ -66,6 +71,12 @@ Para leer CSF escaneadas (JPEG/PNG) con Gemini, agrega el extra:
66
71
  pip install "csf-scraper-python[llm-gemini]"
67
72
  ```
68
73
 
74
+ Para leerlas usando el lector basado en OpenCV:
75
+
76
+ ```bash
77
+ pip install "csf-scraper-python[opencv]"
78
+ ```
79
+
69
80
  Desde el código fuente:
70
81
 
71
82
  ```bash
@@ -77,6 +88,10 @@ pip install .
77
88
  Todo el flujo es asíncrono: `Scraper` envuelve un `httpx.AsyncClient` y todos
78
89
  los métodos de lectura son corrutinas, así que se consumen con `await`.
79
90
 
91
+ `Scraper.create()` crea y devuelve la instancia directamente; **no** es
92
+ obligatorio usarla con `async with`. Basta con cerrar el cliente al terminar,
93
+ ya sea llamando a `aclose()` o usando el scraper como _async context manager_:
94
+
80
95
  ```python
81
96
  import asyncio
82
97
 
@@ -84,14 +99,17 @@ from csf_scraper import Scraper
84
99
 
85
100
 
86
101
  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:
102
+ scraper = Scraper.create()
103
+
104
+ try:
89
105
  # RFC + idCIF directamente (se validan con los value objects Rfc e IdCif)
90
106
  person = await scraper.obtain_from_rfc_and_cif("DIM8701081LA", "1904014102123")
91
107
 
92
108
  # También puedes obtener los datos directamente de un archivo local PDF
93
109
  # (requiere pdftotext / poppler-utils instalado)
94
110
  person = await scraper.obtain_from_pdf_path("ruta/al/csf.pdf")
111
+ finally:
112
+ await scraper.aclose()
95
113
 
96
114
  # Acceso a los datos de la persona (moral o física) por atributos
97
115
  if person.is_fisica():
@@ -115,6 +133,16 @@ if __name__ == "__main__":
115
133
  asyncio.run(main())
116
134
  ```
117
135
 
136
+ Si prefieres no gestionar el cierre manualmente, el scraper también funciona
137
+ como _async context manager_ (cierra el `AsyncClient` al salir del bloque):
138
+
139
+ ```python
140
+ async def main() -> None:
141
+ async with Scraper.create() as scraper:
142
+ person = await scraper.obtain_from_rfc_and_cif("DIM8701081LA", "1904014102123")
143
+ print(person.to_dict())
144
+ ```
145
+
118
146
  Para leer una CSF escaneada (JPEG/PNG) se inyecta el motor de lectura de imágenes.
119
147
  Requiere el extra `llm-gemini`:
120
148
 
@@ -126,8 +154,33 @@ from csf_scraper import CsfImageExtractor, GeminiImageReader, Scraper
126
154
 
127
155
  async def main() -> None:
128
156
  reader = GeminiImageReader(api_key="…", model_name="…")
129
- async with Scraper.create(image_extractor=CsfImageExtractor(reader)) as scraper:
157
+ scraper = Scraper.create(image_extractor=CsfImageExtractor(reader))
158
+ try:
159
+ person = await scraper.obtain_from_image_path("ruta/al/csf.png")
160
+ finally:
161
+ await scraper.aclose()
162
+ print(person.to_dict())
163
+
164
+
165
+ if __name__ == "__main__":
166
+ asyncio.run(main())
167
+ ```
168
+
169
+ Para usar el lector basado en OpenCV no se necesita un proveedor LLM. Requiere
170
+ el extra `opencv`:
171
+
172
+ ```python
173
+ import asyncio
174
+
175
+ from csf_scraper import CsfImageExtractor, OpenCvImageReader, Scraper
176
+
177
+
178
+ async def main() -> None:
179
+ scraper = Scraper.create(image_extractor=CsfImageExtractor(OpenCvImageReader()))
180
+ try:
130
181
  person = await scraper.obtain_from_image_path("ruta/al/csf.png")
182
+ finally:
183
+ await scraper.aclose()
131
184
  print(person.to_dict())
132
185
 
133
186
 
@@ -135,14 +188,31 @@ if __name__ == "__main__":
135
188
  asyncio.run(main())
136
189
  ```
137
190
 
138
- Salida de `person.to_dict()` para una persona moral (fechas como `datetime`):
191
+ `to_dict()` devuelve el conjunto completo de propiedades del modelo que se haya
192
+ obtenido: los campos comunes de `Persona` más los propios de `PersonaMoral` o de
193
+ `PersonaFisica`. Las fechas se devuelven como `datetime` (o `None`) y los value
194
+ objects `Rfc`/`IdCif` como cadenas.
195
+
196
+ Salida de `person.to_dict()` para una **persona moral**:
139
197
 
140
198
  ```python
141
199
  {
200
+ # Compartidos con PersonaFisica
142
201
  "rfc": "DIM8701081LA",
143
202
  "id_cif": "12345678",
144
203
  "fecha_inicio_operaciones": datetime(2019, 2, 21),
145
- # …
204
+ "situacion_contribuyente": "ACTIVO",
205
+ "fecha_ultimo_cambio_situacion": datetime(2019, 2, 21),
206
+ "entidad_federativa": "CIUDAD DE MEXICO",
207
+ "municipio_delegacion": "CUAUHTEMOC",
208
+ "colonia": "CENTRO",
209
+ "tipo_vialidad": "CALLE",
210
+ "nombre_vialidad": "AVENIDA REFORMA",
211
+ "numero_exterior": "123",
212
+ "numero_interior": "A",
213
+ "codigo_postal": "06000",
214
+ "correo_electronico": "contacto@ejemplo.mx",
215
+ "al": "CIUDAD DE MEXICO",
146
216
  "regimenes": [
147
217
  {
148
218
  "regimen": "Régimen General de Ley Personas Morales",
@@ -151,12 +221,53 @@ Salida de `person.to_dict()` para una persona moral (fechas como `datetime`):
151
221
  },
152
222
  ],
153
223
  "extra_data": {"localidad": ""},
224
+ # Propios de PersonaMoral
154
225
  "razon_social": "Mi razón social",
155
226
  "regimen_de_capital": "SA DE CV",
156
227
  "fecha_constitucion": datetime(2019, 2, 21),
157
228
  }
158
229
  ```
159
230
 
231
+ Salida de `person.to_dict()` para una **persona física**:
232
+
233
+ ```python
234
+ {
235
+ # Compartidos con PersonaMoral
236
+ "rfc": "GODE561231GR8",
237
+ "id_cif": "12345678",
238
+ "fecha_inicio_operaciones": datetime(2019, 2, 21),
239
+ "situacion_contribuyente": "ACTIVO",
240
+ "fecha_ultimo_cambio_situacion": datetime(2019, 2, 21),
241
+ "entidad_federativa": "CIUDAD DE MEXICO",
242
+ "municipio_delegacion": "CUAUHTEMOC",
243
+ "colonia": "CENTRO",
244
+ "tipo_vialidad": "CALLE",
245
+ "nombre_vialidad": "AVENIDA REFORMA",
246
+ "numero_exterior": "123",
247
+ "numero_interior": "A",
248
+ "codigo_postal": "06000",
249
+ "correo_electronico": "contacto@ejemplo.mx",
250
+ "al": "CIUDAD DE MEXICO",
251
+ "regimenes": [
252
+ {
253
+ "regimen": "Régimen de Sueldos y Salarios e Ingresos Asimilados a Salarios",
254
+ "fecha_alta": datetime(2019, 2, 21),
255
+ "regimen_id": "605",
256
+ },
257
+ ],
258
+ "extra_data": {"localidad": ""},
259
+ # Propios de PersonaFisica
260
+ "curp": "GODE561231HDFRRN09",
261
+ "nombre": "MI NOMBRE",
262
+ "apellido_paterno": "MI APELLIDO PATERNO",
263
+ "apellido_materno": "MI APELLIDO MATERNO",
264
+ "fecha_nacimiento": datetime(1956, 12, 31),
265
+ }
266
+ ```
267
+
268
+ Si necesitas las fechas en ISO-8601, usa `person.model_dump_json()`, que
269
+ serializa exactamente las mismas propiedades.
270
+
160
271
  ## Modelos
161
272
 
162
273
  - `Persona` es la base de `PersonaFisica` y `PersonaMoral`; sus campos se
@@ -1,4 +1,4 @@
1
- # csf-scraper
1
+ # csf-scraper-python
2
2
 
3
3
  Port a Python de [phpcfdi/csf-scraper](https://github.com/phpcfdi/csf-scraper):
4
4
  obtiene los datos fiscales actuales de una persona moral o física dado su RFC y
@@ -11,7 +11,8 @@ su idCIF (SAT, Constancia de Situación Fiscal).
11
11
  `poppler-utils`), necesario **solo** para leer la CSF desde un archivo PDF
12
12
  (`obtain_from_pdf_path`).
13
13
  - Dependencias: `httpx`, `beautifulsoup4`, `lxml`, `pydantic`, `llama-index-core`.
14
- Los clientes de cada proveedor LLM son extras opcionales (`llm-gemini`).
14
+ Los clientes de cada proveedor LLM son extras opcionales (`llm-gemini`). El
15
+ lector de imágenes basado en OpenCV requiere el extra `opencv`.
15
16
 
16
17
  ## Instalación
17
18
 
@@ -27,6 +28,12 @@ Para leer CSF escaneadas (JPEG/PNG) con Gemini, agrega el extra:
27
28
  pip install "csf-scraper-python[llm-gemini]"
28
29
  ```
29
30
 
31
+ Para leerlas usando el lector basado en OpenCV:
32
+
33
+ ```bash
34
+ pip install "csf-scraper-python[opencv]"
35
+ ```
36
+
30
37
  Desde el código fuente:
31
38
 
32
39
  ```bash
@@ -38,6 +45,10 @@ pip install .
38
45
  Todo el flujo es asíncrono: `Scraper` envuelve un `httpx.AsyncClient` y todos
39
46
  los métodos de lectura son corrutinas, así que se consumen con `await`.
40
47
 
48
+ `Scraper.create()` crea y devuelve la instancia directamente; **no** es
49
+ obligatorio usarla con `async with`. Basta con cerrar el cliente al terminar,
50
+ ya sea llamando a `aclose()` o usando el scraper como _async context manager_:
51
+
41
52
  ```python
42
53
  import asyncio
43
54
 
@@ -45,14 +56,17 @@ from csf_scraper import Scraper
45
56
 
46
57
 
47
58
  async def main() -> None:
48
- # El scraper es un context manager asíncrono: cierra el AsyncClient al salir
49
- async with Scraper.create() as scraper:
59
+ scraper = Scraper.create()
60
+
61
+ try:
50
62
  # RFC + idCIF directamente (se validan con los value objects Rfc e IdCif)
51
63
  person = await scraper.obtain_from_rfc_and_cif("DIM8701081LA", "1904014102123")
52
64
 
53
65
  # También puedes obtener los datos directamente de un archivo local PDF
54
66
  # (requiere pdftotext / poppler-utils instalado)
55
67
  person = await scraper.obtain_from_pdf_path("ruta/al/csf.pdf")
68
+ finally:
69
+ await scraper.aclose()
56
70
 
57
71
  # Acceso a los datos de la persona (moral o física) por atributos
58
72
  if person.is_fisica():
@@ -76,6 +90,16 @@ if __name__ == "__main__":
76
90
  asyncio.run(main())
77
91
  ```
78
92
 
93
+ Si prefieres no gestionar el cierre manualmente, el scraper también funciona
94
+ como _async context manager_ (cierra el `AsyncClient` al salir del bloque):
95
+
96
+ ```python
97
+ async def main() -> None:
98
+ async with Scraper.create() as scraper:
99
+ person = await scraper.obtain_from_rfc_and_cif("DIM8701081LA", "1904014102123")
100
+ print(person.to_dict())
101
+ ```
102
+
79
103
  Para leer una CSF escaneada (JPEG/PNG) se inyecta el motor de lectura de imágenes.
80
104
  Requiere el extra `llm-gemini`:
81
105
 
@@ -87,8 +111,33 @@ from csf_scraper import CsfImageExtractor, GeminiImageReader, Scraper
87
111
 
88
112
  async def main() -> None:
89
113
  reader = GeminiImageReader(api_key="…", model_name="…")
90
- async with Scraper.create(image_extractor=CsfImageExtractor(reader)) as scraper:
114
+ scraper = Scraper.create(image_extractor=CsfImageExtractor(reader))
115
+ try:
116
+ person = await scraper.obtain_from_image_path("ruta/al/csf.png")
117
+ finally:
118
+ await scraper.aclose()
119
+ print(person.to_dict())
120
+
121
+
122
+ if __name__ == "__main__":
123
+ asyncio.run(main())
124
+ ```
125
+
126
+ Para usar el lector basado en OpenCV no se necesita un proveedor LLM. Requiere
127
+ el extra `opencv`:
128
+
129
+ ```python
130
+ import asyncio
131
+
132
+ from csf_scraper import CsfImageExtractor, OpenCvImageReader, Scraper
133
+
134
+
135
+ async def main() -> None:
136
+ scraper = Scraper.create(image_extractor=CsfImageExtractor(OpenCvImageReader()))
137
+ try:
91
138
  person = await scraper.obtain_from_image_path("ruta/al/csf.png")
139
+ finally:
140
+ await scraper.aclose()
92
141
  print(person.to_dict())
93
142
 
94
143
 
@@ -96,14 +145,31 @@ if __name__ == "__main__":
96
145
  asyncio.run(main())
97
146
  ```
98
147
 
99
- Salida de `person.to_dict()` para una persona moral (fechas como `datetime`):
148
+ `to_dict()` devuelve el conjunto completo de propiedades del modelo que se haya
149
+ obtenido: los campos comunes de `Persona` más los propios de `PersonaMoral` o de
150
+ `PersonaFisica`. Las fechas se devuelven como `datetime` (o `None`) y los value
151
+ objects `Rfc`/`IdCif` como cadenas.
152
+
153
+ Salida de `person.to_dict()` para una **persona moral**:
100
154
 
101
155
  ```python
102
156
  {
157
+ # Compartidos con PersonaFisica
103
158
  "rfc": "DIM8701081LA",
104
159
  "id_cif": "12345678",
105
160
  "fecha_inicio_operaciones": datetime(2019, 2, 21),
106
- # …
161
+ "situacion_contribuyente": "ACTIVO",
162
+ "fecha_ultimo_cambio_situacion": datetime(2019, 2, 21),
163
+ "entidad_federativa": "CIUDAD DE MEXICO",
164
+ "municipio_delegacion": "CUAUHTEMOC",
165
+ "colonia": "CENTRO",
166
+ "tipo_vialidad": "CALLE",
167
+ "nombre_vialidad": "AVENIDA REFORMA",
168
+ "numero_exterior": "123",
169
+ "numero_interior": "A",
170
+ "codigo_postal": "06000",
171
+ "correo_electronico": "contacto@ejemplo.mx",
172
+ "al": "CIUDAD DE MEXICO",
107
173
  "regimenes": [
108
174
  {
109
175
  "regimen": "Régimen General de Ley Personas Morales",
@@ -112,12 +178,53 @@ Salida de `person.to_dict()` para una persona moral (fechas como `datetime`):
112
178
  },
113
179
  ],
114
180
  "extra_data": {"localidad": ""},
181
+ # Propios de PersonaMoral
115
182
  "razon_social": "Mi razón social",
116
183
  "regimen_de_capital": "SA DE CV",
117
184
  "fecha_constitucion": datetime(2019, 2, 21),
118
185
  }
119
186
  ```
120
187
 
188
+ Salida de `person.to_dict()` para una **persona física**:
189
+
190
+ ```python
191
+ {
192
+ # Compartidos con PersonaMoral
193
+ "rfc": "GODE561231GR8",
194
+ "id_cif": "12345678",
195
+ "fecha_inicio_operaciones": datetime(2019, 2, 21),
196
+ "situacion_contribuyente": "ACTIVO",
197
+ "fecha_ultimo_cambio_situacion": datetime(2019, 2, 21),
198
+ "entidad_federativa": "CIUDAD DE MEXICO",
199
+ "municipio_delegacion": "CUAUHTEMOC",
200
+ "colonia": "CENTRO",
201
+ "tipo_vialidad": "CALLE",
202
+ "nombre_vialidad": "AVENIDA REFORMA",
203
+ "numero_exterior": "123",
204
+ "numero_interior": "A",
205
+ "codigo_postal": "06000",
206
+ "correo_electronico": "contacto@ejemplo.mx",
207
+ "al": "CIUDAD DE MEXICO",
208
+ "regimenes": [
209
+ {
210
+ "regimen": "Régimen de Sueldos y Salarios e Ingresos Asimilados a Salarios",
211
+ "fecha_alta": datetime(2019, 2, 21),
212
+ "regimen_id": "605",
213
+ },
214
+ ],
215
+ "extra_data": {"localidad": ""},
216
+ # Propios de PersonaFisica
217
+ "curp": "GODE561231HDFRRN09",
218
+ "nombre": "MI NOMBRE",
219
+ "apellido_paterno": "MI APELLIDO PATERNO",
220
+ "apellido_materno": "MI APELLIDO MATERNO",
221
+ "fecha_nacimiento": datetime(1956, 12, 31),
222
+ }
223
+ ```
224
+
225
+ Si necesitas las fechas en ISO-8601, usa `person.model_dump_json()`, que
226
+ serializa exactamente las mismas propiedades.
227
+
121
228
  ## Modelos
122
229
 
123
230
  - `Persona` es la base de `PersonaFisica` y `PersonaMoral`; sus campos se