Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .devcontainer/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,11 @@ ENV PYENV_ROOT="${HOME}/.pyenv"
ENV PATH="${PYENV_ROOT}/shims:${PYENV_ROOT}/bin:${HOME}/.local/bin:$PATH"

RUN echo "done 0" \
&& curl https://pyenv.run | bash \
&& curl -ksS https://pyenv.run | bash \
&& echo "done 1" \
&& pyenv install ${PYTHON_VERSION} \
&& echo "done 2" \
&& pyenv global ${PYTHON_VERSION} \
&& echo "done 3" \
&& curl -sSL https://install.python-poetry.org | python3 - \
&& curl -ksSL https://install.python-poetry.org | python3 - \
&& poetry config virtualenvs.in-project true
2 changes: 1 addition & 1 deletion .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
"ms-python.black-formatter",
"ms-python.isort",
"donjayamanne.python-extension-pack",
"Codeium.codeium"
"aaron-bond.better-comments"
]
}
}
Expand Down
47 changes: 47 additions & 0 deletions .github/workflows/python-unittest.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# This workflow will install Python dependencies, run tests and lint with a variety of Python versions
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions

name: Python unit test

on:
push:
branches:
- 'main'
pull_request:
branches:
- '**'

jobs:
unit-tests:
name: Unit Tests
runs-on: ubuntu-latest

strategy:
fail-fast: false
matrix:
python-version: ['3.10', '3.11', '3.12', '3.13', '3.14']

steps:
- name: Checkout
uses: actions/checkout@v5

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}

- name: Set up Poetry
uses: Gr1N/setup-poetry@v9

- name: Cache dependencies
uses: actions/cache@v4
with:
path: ~/.cache/pypoetry/virtualenvs
key: ${{ runner.os }}-poetry-${{ hashFiles('poetry.lock') }}
restore-keys: ${{ runner.os }}-poetry-${{ hashFiles('poetry.lock') }}

- name: Install dependencies
run: poetry install --without dev --no-root --no-interaction --no-ansi

- name: Launch unit test
run: poetry run python -m unittest -v
11 changes: 11 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"python.testing.unittestArgs": [
"-v",
"-s",
"./tests",
"-p",
"test_*.py"
],
"python.testing.pytestEnabled": false,
"python.testing.unittestEnabled": true
}
3 changes: 1 addition & 2 deletions src/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@


def parse_date(date: str) -> str:
"""Transform a string "Lundi 16 janvier 2025 into a datetime object"""
"""Transform a string "Lundi 16 janvier 2025" into a datetime object"""
date_match = re.search(
DATE_REGX,
date,
Expand All @@ -29,7 +29,6 @@ def parse_date(date: str) -> str:

def parse_name(name: str) -> Tuple[str, str]:
splited_name = name.split(" ") # ? we split to remove the gender prefix
# ? we remove the gender prefix
splited_name = splited_name[1:]
# ? some depute have a composed name like Marc de Fleurian
# ? so we take the first entry as first name
Expand Down
Empty file added tests/__init__.py
Empty file.
3,689 changes: 3,689 additions & 0 deletions tests/data/scrutin_analyse_data.html

Large diffs are not rendered by default.

2,474 changes: 2,474 additions & 0 deletions tests/data/scrutins_data.html

Large diffs are not rendered by default.

70 changes: 70 additions & 0 deletions tests/test_scrutin_analyse.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import unittest
from unittest.mock import patch

from bs4 import BeautifulSoup

from src.models import ScrutinAnalyse
from src.parsers import ScrutinAnalyseParser


class TestScrutinAnalyse(unittest.TestCase):
@classmethod
def setUpClass(cls) -> None:
with open("tests/data/scrutin_analyse_data.html", "r", encoding="utf-8") as f:
cls.sample_html = f.read()

def setUp(self) -> None:
# ? link is required since id is extracted from the url
self.parser = ScrutinAnalyseParser(am_url="https://www.assemblee-nationale.fr/dyn/17/scrutins/3186")

patch_fetch_page = patch.object(ScrutinAnalyseParser, "_fetch_page", side_effect=self.mock_fetch_page)
patch_fetch_vizu = patch.object(
ScrutinAnalyseParser, "_extract_visualiser", side_effect=self.mock_fetch_vizualizer
)

self.mocked_fetch_page = patch_fetch_page.start()
self.mocked_fetch_vizu = patch_fetch_vizu.start()

self.addCleanup(patch_fetch_page.stop)
self.addCleanup(patch_fetch_vizu.stop)

def mock_fetch_page(self, url: str):
return BeautifulSoup(self.sample_html, "html.parser")

def mock_fetch_vizualizer(self, visualiser_url: str) -> str:
# ? extract the url from the sample html to simulate the process
# ? real vizualizer requires selenium so we just return the url here
page = BeautifulSoup(self.sample_html, "html.parser")
visualizer_url = page.find("div", attrs={"data-targetembedid": "embedHemicycle"})
url = visualizer_url.get("data-content")

return url

def test_fetch_scrutin_analyse_data(self):
self.parser.fetch_scrutin_analyse_data()
self.assertIsInstance(self.parser.scrutin_analyse, ScrutinAnalyse)
self.assertEqual(self.mocked_fetch_page.call_count, 1)

self.assertEqual(self.parser.scrutin_analyse.id, 3186)
self.assertEqual(self.parser.scrutin_analyse.date, "2025-10-28")
self.assertEqual(
self.parser.scrutin_analyse.title,
"Scrutin public n°3186 sur l'amendement n° 2493 de M. Prud'homme après l'article 12 (examen prioritaire) du projet de loi de finances pour 2026 (première lecture).",
)

self.assertIsNotNone(self.parser.scrutin_analyse.visualizer)
self.assertIn("hemicycle", self.parser.scrutin_analyse.visualizer)

self.assertIsInstance(self.parser.scrutin_analyse.vote_for, list)
self.assertEqual(len(self.parser.scrutin_analyse.vote_for), 42)

self.assertIsInstance(self.parser.scrutin_analyse.vote_against, list)
self.assertEqual(len(self.parser.scrutin_analyse.vote_against), 178)

self.assertIsInstance(self.parser.scrutin_analyse.vote_abstention, list)
self.assertEqual(len(self.parser.scrutin_analyse.vote_abstention), 6)

self.assertIsInstance(self.parser.scrutin_analyse.vote_absent, list)
self.assertEqual(len(self.parser.scrutin_analyse.vote_absent), 15)

self.assertFalse(self.parser.scrutin_analyse.adopted)
64 changes: 64 additions & 0 deletions tests/test_scrutin_parser.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import unittest
from unittest.mock import patch

from bs4 import BeautifulSoup

from src.models import Scrutin
from src.parsers import ScrutinParser


class TestScrutinParser(unittest.TestCase):
@classmethod
def setUpClass(cls) -> None:
with open("tests/data/scrutins_data.html", "r", encoding="utf-8") as f:
cls.sample_html = f.read()

def setUp(self) -> None:
self.parser = ScrutinParser(legislature=17, max_page=1)

patch_fetch_page = patch.object(ScrutinParser, "_fetch_page", side_effect=self.mock_fetch_page)

self.mocked_fetch_page = patch_fetch_page.start()

self.addCleanup(patch_fetch_page.stop)

def mock_fetch_page(self, an_url: str):
return BeautifulSoup(self.sample_html, "html.parser")

def test_parse_scrutins(self):
self.parser.parse_scrutins()
self.assertEqual(self.mocked_fetch_page.call_count, 2)

self.assertIsInstance(self.parser.scrutins, list)
self.assertEqual(len(self.parser.scrutins), 10)

expected_ids = [3159, 3160, 3124, 3125, 3166, 3165, 3167, 3137, 3168, 3150]

for scrutin in self.parser.scrutins:
self.assertIsInstance(scrutin, Scrutin)
self.assertIsNotNone(scrutin.id)
self.assertIsInstance(scrutin.id, int)
self.assertIn(scrutin.id, expected_ids)

self.assertIsNotNone(scrutin.name)
self.assertIsInstance(scrutin.name, str)

self.assertIsNotNone(scrutin.url)
self.assertIsInstance(scrutin.url, str)

self.assertIsNotNone(scrutin.text_url)
self.assertIsInstance(scrutin.text_url, str)

self.assertIsNotNone(scrutin.date)
self.assertIsInstance(scrutin.date, str)

self.assertIsInstance(scrutin.adopted, bool)

self.assertIsNotNone(scrutin.vote_for)
self.assertIsInstance(scrutin.vote_for, int)

self.assertIsNotNone(scrutin.vote_against)
self.assertIsInstance(scrutin.vote_against, int)

self.assertIsNotNone(scrutin.vote_abstention)
self.assertIsInstance(scrutin.vote_abstention, int)