Skip to content

[FIX] bedelia_api - Corrige CI, bugs del scraper y de la API - #1

Merged
moura-code merged 1 commit into
mainfrom
fix/ci-y-correcciones
Aug 3, 2026
Merged

[FIX] bedelia_api - Corrige CI, bugs del scraper y de la API#1
moura-code merged 1 commit into
mainfrom
fix/ci-y-correcciones

Conversation

@moura-code

@moura-code moura-code commented Aug 3, 2026

Copy link
Copy Markdown
Owner

Resumen

El workflow de GitHub Actions fallaba en todos los pushes desde diciembre (faltaba psycopg2 y no había configuración de base de datos para CI). Además se corrigen bugs reales encontrados en una revisión completa: crashes del scraper, un __str__ roto, un serializer inválido, el modo --dry-run del cargador de datos que crasheaba, y documentación que describía código que ya no existe.

Cambios realizados

  • CI (.github/workflows/deploy.yml): renombrado a "CI"; agrega USE_SQLITE=true y DJANGO_SECRET_KEY de CI; los fallos de tests y flake8 ya no se tragan con || echo / || true; corre también en pull requests.
  • Backend Django:
    • bedelia/requirements.txt: agrega psycopg2-binary (el Dockerfile ya instalaba libpq-dev pero faltaba el paquete).
    • settings.py: fallback de desarrollo para SECRET_KEY, registra global_exception_handler (existía pero nunca se usó), ENUM_NAME_OVERRIDES apuntaba a modelos renombrados (RequisitoNodoPreviaNodo).
    • models.py: PosPreviaItem.__str__ usaba materia_dependiente, campo que no existe (crasheaba admin/shell).
    • serializers/materias.py: PreviaNodoSerializer declaraba items_count sin definirlo (error de DRF al usarlo).
    • views/materias.py: elimina un @extend_schema_view huérfano que se apilaba sobre PreviasViewSet y pisaba su documentación.
    • load_bedelia_data.py: --dry-run crasheaba (get_or_create con instancias sin guardar en Django 5); guard de carrera/anio faltante en posprevias que envenenaba la transacción.
  • Scraper: vigentes_page sin inicializar en __init__; screenshot de error ahora se toma antes de cerrar el navegador (antes se intentaba con la sesión ya cerrada); exit code 1 si falla; tope de intentos al expandir el árbol de credits (antes loop infinito posible); guard de paginador sin ancla activa (AttributeError); filas vacías/incompletas en tablas; XPath de vigente acepta además de Si; import de common antes del sys.path.append en posprevias; doble slash en URL; except: desnudos reemplazados por logging del error real.
  • Raíz: requirements.txt tenía python-dotenv duplicado con versiones en conflicto — pip install -r requirements.txt fallaba directamente.
  • Docs: README reescrito (documentaba example_usage.py y una API de constructor que ya no existen; no mencionaba la API Django). QUICKSTART corregido (importaba modelos inexistentes Carrera/Curso/Previa, 5 links muertos, script verify_data.py inexistente).

Validación

  • python -m compileall sobre todo el repo: sin errores.
  • Import de todos los módulos del scraper: OK.
  • manage.py check: sin issues.
  • manage.py spectacular: el schema OpenAPI genera con 0 errores (antes fallaban los dos enum overrides).
  • flake8 con los selectores del CI: 0 errores.
  • manage.py migrate + load_bedelia_data --dry-run con los JSON reales del repo: completa sin crashear (antes moría con ValueError). Los avisos "materia no encontrada" son inherentes al dry-run (no persiste nada).
  • No se ejecutó el scraper contra el sitio real de Bedelías (requiere credenciales); esos cambios están validados por compilación, imports y revisión.

Notas

  • Hallazgos documentados pero NO corregidos por requerir decisiones de diseño/dominio: la lógica curso-vs-examen de eligible_courses.py trata ambas modalidades igual y no contempla modalidades de inscripción; el filtro unidad_tipo de posprevias no filtra las unidades emitidas ni deduplica; el cargador colapsa posprevias por (materia, plan) descartando fecha/descripcion por fila; go_to_page no navega a páginas fuera de la ventana visible del paginador si no parte de la página 1.
  • Repo personal sin sistema de tickets; el título no lleva referencia.

🤖 Generated with Claude Code

Summary by Sourcery

Update CI, documentation, scraper robustness, and Django API behavior to fix crashes and ensure the project can be set up and tested reliably.

New Features:

  • Expose interactive API documentation at /api/docs and provide a Postman collection reference in the README.
  • Add failure screenshots and proper non-zero exit codes to the scraper when scraping errors occur.

Bug Fixes:

  • Fix scraper crashes by initializing missing page attributes, handling empty or malformed table rows, limiting expansion loops, tolerating varying text for "vigente" and no-instance messages, and adjusting navigation and pagination edge cases.
  • Prevent dry-run mode in the Django data loader from calling get_or_create on unsaved instances, and skip malformed posprevias entries without carrera/anio to avoid transaction errors.
  • Correct the PosPreviaItem string representation and PreviaNodo serializer configuration to avoid runtime errors in admin and API usage.
  • Ensure scraper helper functions log real errors instead of silently swallowing exceptions.
  • Remove a misapplied schema extension decorator that was overriding the previas endpoint documentation.
  • Avoid crashes in analysis scripts by safely defaulting missing exam code and name fields.
  • Prevent paginator utilities from failing when no active anchor is visible by assuming a single page.

Enhancements:

  • Simplify QUICKSTART and README to focus on the Django REST API, updated data model, and modern usage with USE_SQLITE for local setup.
  • Wire up a global exception handler in DRF settings and fix enum overrides to match the current model names.
  • Improve scraper resilience in credits and vigentes pages by better handling dynamic content and absence of data.
  • Normalize posprevias URLs and module imports for more reliable execution.

Build:

  • Add psycopg2-binary to the Django app requirements and remove a conflicting duplicate python-dotenv from the root requirements.

CI:

  • Rename the GitHub Actions workflow to a CI job, run it on pushes and pull requests, configure SQLite-based Django settings for CI, and make test and flake8 failures fail the workflow instead of being ignored.

Documentation:

  • Rewrite README and QUICKSTART to describe the current Django API, data loader, environment configuration, and usage patterns, removing references to obsolete scripts and models.

- CI: agrega psycopg2-binary faltante, corre con USE_SQLITE y secret de CI,
  deja de tragar fallos de tests y flake8 (fallaba en todos los pushes)
- settings: fallback de SECRET_KEY para desarrollo, registra el
  exception handler global, actualiza ENUM_NAME_OVERRIDES a PreviaNodo/PreviaItem
- models: PosPreviaItem.__str__ referenciaba un campo inexistente
- serializers: agrega items_count faltante en PreviaNodoSerializer
- views: elimina decorador extend_schema_view huérfano que se apilaba
  sobre PreviasViewSet
- load_bedelia_data: el modo --dry-run crasheaba con instancias sin guardar;
  agrega guard de carrera/anio en posprevias
- scraper: inicializa vigentes_page, screenshot de error antes de cerrar el
  navegador, exit code 1 al fallar, tope al loop de expansión de credits,
  guards contra None/IndexError en paginación y filas, XPath acepta Si/Sí,
  corrige import de common en posprevias y doble slash en URL
- requirements raíz: elimina python-dotenv duplicado que rompía pip install
- README y QUICKSTART reescritos para reflejar el código real

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@moura-code
moura-code merged commit 2cb71ff into main Aug 3, 2026
1 check passed
@moura-code
moura-code deleted the fix/ci-y-correcciones branch August 3, 2026 23:46
@sourcery-ai

sourcery-ai Bot commented Aug 3, 2026

Copy link
Copy Markdown

Reviewer's Guide

Fixes broken CI by configuring a SQLite-backed Django environment, tightens test/lint behavior, and resolves several runtime bugs in the Django API, management command, and scraper, while updating docs to reflect the current data model and usage.

Sequence diagram for scraper failure handling with screenshot and exit code

sequenceDiagram
    participant Main as main
    participant Config as ScraperConfig
    participant B as Bedelias
    participant D as SeleniumDriver

    Main->>Config: ScraperConfig()
    Main->>B: Bedelias(config)
    Main->>B: run()
    B->>D: start_driver()
    note over B,D: Scraping operations
    B-->>B: run() raises Exception
    alt driver exists
        B->>D: get_screenshot_as_file("screenshot.png")
    end
    B->>D: quit()
    B-->>Main: Exception propagates
    Main->>Main: traceback.print_exc()
    Main->>Main: sys.exit(1)
Loading

File-Level Changes

Change Details Files
CI workflow now runs Django checks/tests and flake8 against a SQLite-backed environment on pushes and pull requests, failing on errors instead of swallowing them.
  • Rename GitHub Actions workflow to generic CI and trigger on pushes to main, pull requests, and manual dispatch.
  • Set USE_SQLITE and a CI-only DJANGO_SECRET_KEY in the CI job environment.
  • Run manage.py check and manage.py test normally (no more
Django backend configuration and models/serializers are aligned with the current schema and OpenAPI settings, and global exception handling is wired in.
  • Add psycopg2-binary to backend requirements so PostgreSQL connections work outside Docker.
  • Provide a development-only SECRET_KEY fallback when DJANGO_SECRET_KEY is missing.
  • Register a global DRF exception handler in REST_FRAMEWORK settings.
  • Fix ENUM_NAME_OVERRIDES to reference the renamed PreviaNodo/PreviaItem enums instead of obsolete Requisito* models.
  • Update PosPreviaItem.str to use plan_estudio instead of a non-existent materia_dependiente field.
  • Define items_count as a SerializerMethodField in PreviaNodoSerializer and implement its getter.
bedelia/requirements.txt
bedelia/config/settings.py
bedelia/api/models.py
bedelia/api/serializers/materias.py
The load_bedelia_data management command’s dry-run and posprevias handling are made safe under Django 5 and resilient to malformed plan data.
  • Avoid calling get_or_create on unsaved PlanMateria instances when running in dry-run mode; instantiate them without saving instead.
  • Skip posprevias records where carrera/anio cannot be parsed from carrera_plan before attempting to look up a plan.
  • Apply the same dry-run behavior to PlanMateria source creation in the posprevias processor to prevent transactional errors.
bedelia/api/management/commands/load_bedelia_data.py
Previas-related API view documentation is cleaned up so schema generation reflects the intended endpoints without duplicate/overlapping schema decorators.
  • Remove an orphan extend_schema_view decorator that was stacked on PreviasViewSet and overwrote or conflicted with its documentation configuration.
bedelia/api/views/materias.py
Scraper robustness is improved across navigation, credits, previas, posprevias, vigentes and helper modules, preventing crashes, infinite loops and unhelpful error handling.
  • Initialize vigentes_page attribute in the Bedelias orchestrator to avoid attribute access errors.
  • Capture a screenshot on scraping failures before closing the browser and exit with code 1 in the CLI entrypoint instead of relying on driver access in the except block.
  • Bound the loop that expands credit tree nodes with a max retry counter and raise if nodes cannot be fully expanded.
  • Generalize the XPath used to locate course title spans in credits pages to handle both title variants.
  • Ensure plan-section table parsing skips empty or malformed rows when determining total plan sections.
  • Make the vigente XPath accept both “Si”/“Sí” (and lowercase variants) when selecting active plans.
  • Refresh previas table row references safely when re-rendering, skipping rows that disappear or have too few cells.
  • Treat missing “active paginator” anchors in usetable as a single-page scenario and log a warning.
  • Relax vigentes page checks for “no instances” messages and only process tables when data is present.
  • Limit the number of attempts to expand requirements in previas before refresh and failure, and avoid over-refreshing.
  • Move the import of common.navigation before sys.path modifications in posprevias and fix home_url concatenation to avoid double slashes.
  • Replace bare except blocks in scroll_to_element_and_click with logged, specific Exception handling.
  • Handle missing exam code/name gracefully in get_libres by defaulting to empty strings.
scraper/main.py
scraper/scraper.py
scraper/common/navigation.py
scraper/common/usetable.py
scraper/pages/credits.py
scraper/pages/previas.py
scraper/pages/posprevias.py
scraper/pages/vigentes.py
get_libres.py
Top-level and Quickstart documentation now describe the current Django REST API, models and usage, including SQLite-based setup and API docs location, instead of obsolete scraper-only examples.
  • Rewrite README to describe both the scraper and Django REST API, including project structure, configuration, and how to run the scraper and API (with SQLite or Docker).
  • Update QUICKSTART to assume commands are run from the bedelia directory, document USE_SQLITE usage, reflect the current model names and example queries, and fix references to JSON locations and helper scripts.
  • Remove references to non-existent example_usage.py, verify_data.py, old models (Carrera/Curso/Previa), dead links and outdated TODO guidance; add mention of interactive docs at /api/docs/ and the Postman collection.
README.md
QUICKSTART.md
Root Python dependencies are deduplicated to avoid installation conflicts.
  • Remove a duplicate, conflicting python-dotenv pin from the root requirements file so pip install -r requirements.txt succeeds.
requirements.txt

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 2 issues, and left some high level feedback:

  • In load_bedelia_data, the dry-run branches now instantiate PlanMateria without saving in multiple places; consider centralizing this behavior (e.g., via a helper) and double-check that these unsaved instances are never reused in contexts that expect a primary key to avoid subtle bugs if the loader is extended.
  • Several scraper paths (e.g., credit tree expansion and requirements expansion) still raise generic Exception on control-flow limits; using more specific/custom exception types and including plan/year context in the message would make failures easier to diagnose and handle upstream.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `load_bedelia_data`, the dry-run branches now instantiate `PlanMateria` without saving in multiple places; consider centralizing this behavior (e.g., via a helper) and double-check that these unsaved instances are never reused in contexts that expect a primary key to avoid subtle bugs if the loader is extended.
- Several scraper paths (e.g., credit tree expansion and requirements expansion) still raise generic `Exception` on control-flow limits; using more specific/custom exception types and including plan/year context in the message would make failures easier to diagnose and handle upstream.

## Individual Comments

### Comment 1
<location path="scraper/common/navigation.py" line_range="109-110" />
<code_context>

-        # Click on the info icon for the row that has both the matching year AND "Si" in vigente
-        xpath = f'//tr[td[1][text()="{plan_year}"]][td[3][text()="Si"]]//i[contains(@class, "pi-info-circle") or contains(@class, "pi-calendar")]'
+        # Click on the info icon for the row that has both the matching year AND "Si"/"Sí" in vigente
+        xpath = (
+            f'//tr[td[1][text()="{plan_year}"]]'
+            f'[td[3][text()="Si" or text()="Sí" or text()="si" or text()="sí"]]'
</code_context>
<issue_to_address>
**nitpick:** Log message is now slightly misleading given the expanded vigente match conditions.

The XPath now accepts `Si`, ``, `si`, and ``, but the log still hardcodes `vigente=Si`. Please update the log to reflect the full set of accepted values, or log the actual matched text, so it accurately describes what the scraper selected.
</issue_to_address>

### Comment 2
<location path="scraper/scraper.py" line_range="76" />
<code_context>
-        traceback.print_exc()
+            except Exception as e:
+                last_error = e
+        self.logger.warning(f"Failed to click element after 3 attempts: {last_error}")
         return False

</code_context>
<issue_to_address>
**suggestion:** Swallowing the exception completely may make diagnosing click failures harder.

Only `last_error` is logged, without a traceback, before returning `False`. For intermittent Selenium issues, the full stack trace is often needed to debug root causes. Please log the traceback (e.g., via `traceback.format_exc()` or `logger.exception` on the final attempt) so we keep full context while still limiting noise from transient failures.

Suggested implementation:

```python
        last_error = None
        trys_number += 1
        try:
            self.scroll_to_element(element)
            self.wait_for_element_to_be_clickable(element).click()
            return True
        except Exception as e:
            last_error = e
        if last_error:
            # Log with full traceback on the final failure to aid debugging
            self.logger.exception(
                "Failed to click element after 3 attempts", exc_info=last_error
            )
        else:
            self.logger.warning("Failed to click element after 3 attempts")
        return False



```

1. The snippet suggests this is inside a loop (e.g., `while trys_number < 3:` or `for _ in range(3):`). Ensure `last_error = None` is declared before that loop, not reinitialized on every iteration. If the loop is above this snippet, move `last_error = None` to the beginning of the method or immediately before the retry loop.
2. If your logger is configured to include tracebacks automatically on `logger.exception()` without `exc_info`, you can simplify the call to `self.logger.exception("Failed to click element after 3 attempts")`. Adjust to match any existing logging conventions in `scraper.py`.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +109 to +110
# Click on the info icon for the row that has both the matching year AND "Si"/"Sí" in vigente
xpath = (

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: Log message is now slightly misleading given the expanded vigente match conditions.

The XPath now accepts Si, , si, and , but the log still hardcodes vigente=Si. Please update the log to reflect the full set of accepted values, or log the actual matched text, so it accurately describes what the scraper selected.

Comment thread scraper/scraper.py
traceback.print_exc()
except Exception as e:
last_error = e
self.logger.warning(f"Failed to click element after 3 attempts: {last_error}")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Swallowing the exception completely may make diagnosing click failures harder.

Only last_error is logged, without a traceback, before returning False. For intermittent Selenium issues, the full stack trace is often needed to debug root causes. Please log the traceback (e.g., via traceback.format_exc() or logger.exception on the final attempt) so we keep full context while still limiting noise from transient failures.

Suggested implementation:

        last_error = None
        trys_number += 1
        try:
            self.scroll_to_element(element)
            self.wait_for_element_to_be_clickable(element).click()
            return True
        except Exception as e:
            last_error = e
        if last_error:
            # Log with full traceback on the final failure to aid debugging
            self.logger.exception(
                "Failed to click element after 3 attempts", exc_info=last_error
            )
        else:
            self.logger.warning("Failed to click element after 3 attempts")
        return False

  1. The snippet suggests this is inside a loop (e.g., while trys_number < 3: or for _ in range(3):). Ensure last_error = None is declared before that loop, not reinitialized on every iteration. If the loop is above this snippet, move last_error = None to the beginning of the method or immediately before the retry loop.
  2. If your logger is configured to include tracebacks automatically on logger.exception() without exc_info, you can simplify the call to self.logger.exception("Failed to click element after 3 attempts"). Adjust to match any existing logging conventions in scraper.py.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant