Source code for pluxee.base_pluxee_client

import os
from datetime import date, datetime
from enum import Enum
from typing import List, Optional, Type, Union

import requests
from bs4 import BeautifulSoup

from .exceptions import PluxeeAPIError, PluxeeLoginError

try:
    import aiohttp

    Session_Type = Type[Union[aiohttp.ClientSession, requests.Session]]
except ImportError:
    Session_Type = Type[requests.Session]


_TRANSACTION_PATHS = {
    'fr': 'mon-solde-sodexo-card',
    'nl': 'mijn-sodexo-card-saldo',
}


[docs] class PassType(str, Enum): """The different types of pass that are provided.""" LUNCH = "LUNCH" ECO = "ECO" CONSO = "CONSO" GIFT = "GIFT"
[docs] class PluxeeBalance: """The balance of each pass.""" def __init__(self, lunch_pass: float, eco_pass: float, gift_pass: float, conso_pass: float): self.lunch_pass = lunch_pass self.eco_pass = eco_pass self.gift_pass = gift_pass self.conso_pass = conso_pass def __str__(self): return f"lunch_pass: {self.lunch_pass}\neco_pass: {self.eco_pass}\ngift_pass: {self.gift_pass}\nconso_pass: {self.conso_pass}" def __repr__(self): return self.__str__()
[docs] class PluxeeTransaction: """A payment or the reception of your pass.""" def __init__(self, date: date, amount: float, detail: str, merchant: str): self.date = date self.amount = amount self.detail = detail self.merchant = merchant def __str__(self): return f"date: {self.date}\namount: {self.amount}\ndetail: {self.detail}\nmerchant: {self.merchant}" def __repr__(self): return self.__str__()
class _ResponseWrapper: def __init__(self, content: str, status_code: int): self.content = content self.status_code = status_code class _PluxeeClient: """ The business logic, how to parse and what information to extract. Args: username: The pluxee username. password: The pluxee password. language: The pluxee website language (either 'fr' or 'nl', defaults to 'fr'). timeout: Request timeout in seconds (defaults to 30). Attrs: username: The pluxee username. password: The pluxee password. language: The pluxee website language (either 'fr' or 'nl', defaults to 'fr'). """ DOMAIN = "users.pluxee.be" LUNCH_PASS_SELECTOR = ( 'body > div > header > div.header-fixed > div.balance-block > div > ul > li > a[href*="LUNCH"] > span.balance--price' ) ECO_PASS_SELECTOR = ( 'body > div > header > div.header-fixed > div.balance-block > div > ul > li > a[href*="ECO"] > span.balance--price' ) GIFT_PASS_SELECTOR = ( 'body > div > header > div.header-fixed > div.balance-block > div > ul > li > a[href*="GIFT"] > span.balance--price' ) CONSO_PASS_SELECTOR = ( 'body > div > header > div.header-fixed > div.balance-block > div > ul > li > a[href*="CONSO"] > span.balance--price' ) TRANSACTION_SELECTOR = "body > div.dialog-off-canvas-main-canvas > div > div > div.transaction--section > div.transaction-list--section > div.transactions-list--table > div > table > tbody > tr" TRANSACTION_TABLE_SELECTOR = "body > div.dialog-off-canvas-main-canvas > div > div > div.transaction--section > div.transaction-list--section > div.transactions-list--table > div > table" def __init__( self, username: str, password: str, language: str = 'fr', session: Optional[Session_Type] = None, timeout: int = 30 ): if language not in _TRANSACTION_PATHS: raise ValueError(f"Invalid language '{language}'. Must be one of: {list(_TRANSACTION_PATHS.keys())}") self._username = username or os.environ.get("PLUXEE_USERNAME") self._password = password or os.environ.get("PLUXEE_PASSWORD") self._language = language self._timeout = timeout self._base_url_localized = f"https://{_PluxeeClient.DOMAIN}/{self._language}" self._base_url_login = f"{self._base_url_localized}/user/login" self._base_url_balance = f"{self._base_url_localized}" self._base_url_transactions = f"{self._base_url_localized}/{_TRANSACTION_PATHS[self._language]}" self._session = session @staticmethod def _price_to_float(price) -> float: return float(price.replace("€", "").replace(",", ".").replace("EUR", "").strip().replace(" ", "")) def _parse_balance_from_response(self, response: _ResponseWrapper) -> PluxeeBalance: soup = BeautifulSoup(response.content, features="html.parser") lunch_tag = soup.select_one(self.LUNCH_PASS_SELECTOR) lunch = self._price_to_float(lunch_tag.text) if lunch_tag is not None else 0 eco_tag = soup.select_one(self.ECO_PASS_SELECTOR) eco = self._price_to_float(eco_tag.text) if eco_tag is not None else 0 gift_tag = soup.select_one(self.GIFT_PASS_SELECTOR) gift = self._price_to_float(gift_tag.text) if gift_tag is not None else 0 conso_tag = soup.select_one(self.CONSO_PASS_SELECTOR) conso = self._price_to_float(conso_tag.text) if conso_tag is not None else 0 if (lunch_tag, eco_tag, gift_tag, conso_tag) == (None, None, None, None): raise PluxeeAPIError("Could not find the balance in the response") return PluxeeBalance(lunch, eco, gift, conso) def _parse_transactions_from_response( self, response: _ResponseWrapper, transactions: List[PluxeeTransaction], since: Optional[date] = None, until: Optional[date] = None, ) -> bool: dom = BeautifulSoup(response.content, features="html.parser") table = dom.select_one(self.TRANSACTION_TABLE_SELECTOR) if not table: if not transactions: # If there is no table, it means something unexpected happened. raise PluxeeAPIError("No transaction table found and no prior transactions collected") else: # In the case where we already have some transactions in the list, it means we have reached an empty page. return True entries = dom.select(self.TRANSACTION_SELECTOR) complete = len(entries) < 10 for entry in entries: date_dom = entry.select_one("td.views-field-date") merchant_dom = entry.select_one("td.views-field-description") description_dom = entry.select_one("td.views-field-detail") amount_dom = entry.select_one("td.views-field-amount > span") if date_dom is None or merchant_dom is None or description_dom is None or amount_dom is None: raise PluxeeAPIError("Could not find the transactions in the response") date = datetime.strptime(date_dom.text.strip(), "%d.%m.%Y").date() merchant = merchant_dom.text.strip() description = description_dom.text.strip() amount = self._price_to_float(amount_dom.text) if since and date < since: complete = True break if not until or date < until: transactions.append(PluxeeTransaction(date, amount, description, merchant)) return complete def gen_login_post_args(self): return { "url": self._base_url_login, "params": { "destination": f"/{self._language}/frontpage", }, "allow_redirects": False, "data": { "name": self._username, "pass": self._password, "form_build_id": "form_build_id", "form_id": "user_login_form", "op": "Se connecter", }, } def get_language(self): return self._language @staticmethod def handle_login_status(status): if status != 303: if status == 302: raise PluxeeLoginError(f"Bad username/password. {status}") raise PluxeeAPIError(f"Pluxee webpage did not respond with the expected status. {status}")