mirror of
https://github.com/Kizuren/LidlConnect.py.git
synced 2025-12-24 00:04:06 +01:00
Initial Commit
This commit is contained in:
commit
a16b90e181
15 changed files with 1711 additions and 0 deletions
184
.gitignore
vendored
Normal file
184
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,184 @@
|
||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# C extensions
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
share/python-wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
MANIFEST
|
||||||
|
|
||||||
|
# PyInstaller
|
||||||
|
# Usually these files are written by a python script from a template
|
||||||
|
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||||
|
*.manifest
|
||||||
|
*.spec
|
||||||
|
|
||||||
|
# Installer logs
|
||||||
|
pip-log.txt
|
||||||
|
pip-delete-this-directory.txt
|
||||||
|
|
||||||
|
# Unit test / coverage reports
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.nox/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
.cache
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
*.cover
|
||||||
|
*.py,cover
|
||||||
|
.hypothesis/
|
||||||
|
.pytest_cache/
|
||||||
|
cover/
|
||||||
|
|
||||||
|
# Translations
|
||||||
|
*.mo
|
||||||
|
*.pot
|
||||||
|
|
||||||
|
# Django stuff:
|
||||||
|
*.log
|
||||||
|
local_settings.py
|
||||||
|
db.sqlite3
|
||||||
|
db.sqlite3-journal
|
||||||
|
|
||||||
|
# Flask stuff:
|
||||||
|
instance/
|
||||||
|
.webassets-cache
|
||||||
|
|
||||||
|
# Scrapy stuff:
|
||||||
|
.scrapy
|
||||||
|
|
||||||
|
# Sphinx documentation
|
||||||
|
docs/_build/
|
||||||
|
|
||||||
|
# PyBuilder
|
||||||
|
.pybuilder/
|
||||||
|
target/
|
||||||
|
|
||||||
|
# Jupyter Notebook
|
||||||
|
.ipynb_checkpoints
|
||||||
|
|
||||||
|
# IPython
|
||||||
|
profile_default/
|
||||||
|
ipython_config.py
|
||||||
|
|
||||||
|
# pyenv
|
||||||
|
# For a library or package, you might want to ignore these files since the code is
|
||||||
|
# intended to run in multiple environments; otherwise, check them in:
|
||||||
|
# .python-version
|
||||||
|
|
||||||
|
# pipenv
|
||||||
|
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||||
|
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||||
|
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||||
|
# install all needed dependencies.
|
||||||
|
#Pipfile.lock
|
||||||
|
|
||||||
|
# UV
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
||||||
|
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||||
|
# commonly ignored for libraries.
|
||||||
|
#uv.lock
|
||||||
|
|
||||||
|
# poetry
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||||
|
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||||
|
# commonly ignored for libraries.
|
||||||
|
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||||
|
#poetry.lock
|
||||||
|
|
||||||
|
# pdm
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||||
|
#pdm.lock
|
||||||
|
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
||||||
|
# in version control.
|
||||||
|
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
|
||||||
|
.pdm.toml
|
||||||
|
.pdm-python
|
||||||
|
.pdm-build/
|
||||||
|
|
||||||
|
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||||
|
__pypackages__/
|
||||||
|
|
||||||
|
# Celery stuff
|
||||||
|
celerybeat-schedule
|
||||||
|
celerybeat.pid
|
||||||
|
|
||||||
|
# SageMath parsed files
|
||||||
|
*.sage.py
|
||||||
|
|
||||||
|
# Environments
|
||||||
|
.env
|
||||||
|
.venv
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env.bak/
|
||||||
|
venv.bak/
|
||||||
|
|
||||||
|
# Spyder project settings
|
||||||
|
.spyderproject
|
||||||
|
.spyproject
|
||||||
|
|
||||||
|
# Rope project settings
|
||||||
|
.ropeproject
|
||||||
|
|
||||||
|
# mkdocs documentation
|
||||||
|
/site
|
||||||
|
|
||||||
|
# mypy
|
||||||
|
.mypy_cache/
|
||||||
|
.dmypy.json
|
||||||
|
dmypy.json
|
||||||
|
|
||||||
|
# Pyre type checker
|
||||||
|
.pyre/
|
||||||
|
|
||||||
|
# pytype static type analyzer
|
||||||
|
.pytype/
|
||||||
|
|
||||||
|
# Cython debug symbols
|
||||||
|
cython_debug/
|
||||||
|
|
||||||
|
# PyCharm
|
||||||
|
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||||
|
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||||
|
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||||
|
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||||
|
#.idea/
|
||||||
|
|
||||||
|
# Visual Studio Code
|
||||||
|
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
|
||||||
|
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
|
||||||
|
# and can be added to the global gitignore or merged into this file. However, if you prefer,
|
||||||
|
# you could uncomment the following to ignore the enitre vscode folder
|
||||||
|
# .vscode/
|
||||||
|
|
||||||
|
# Ruff stuff:
|
||||||
|
.ruff_cache/
|
||||||
|
|
||||||
|
# PyPI configuration file
|
||||||
|
.pypirc
|
||||||
|
|
||||||
|
# Library testing stuff
|
||||||
|
test-lib.py
|
||||||
5
LidlConnect/__init__.py
Normal file
5
LidlConnect/__init__.py
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
"""Lidl Connect API client library."""
|
||||||
|
|
||||||
|
from .client import LidlConnect
|
||||||
|
|
||||||
|
__all__ = ["LidlConnect"]
|
||||||
1
LidlConnect/api/__init__.py
Normal file
1
LidlConnect/api/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
"""API modules for Lidl Connect."""
|
||||||
173
LidlConnect/api/invoices.py
Normal file
173
LidlConnect/api/invoices.py
Normal file
|
|
@ -0,0 +1,173 @@
|
||||||
|
"""Invoices-related API functionality for Lidl Connect."""
|
||||||
|
|
||||||
|
from typing import Dict, Any, List, Optional
|
||||||
|
from datetime import datetime
|
||||||
|
from ..helpers import ttl_cache
|
||||||
|
|
||||||
|
class InvoicesMixin:
|
||||||
|
"""Invoices-related API methods for Lidl Connect API."""
|
||||||
|
|
||||||
|
INVOICES_URL = "https://selfcare.lidl-connect.at/customer/invoices/invoice-list"
|
||||||
|
VOUCHER_URL = "https://selfcare.lidl-connect.at/customer/invoices/consumed-vouchers"
|
||||||
|
|
||||||
|
@ttl_cache(30)
|
||||||
|
def get_invoices(self) -> List[Dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
Get list of invoices for the current account.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List[Dict]: List of invoice objects with transaction details
|
||||||
|
"""
|
||||||
|
if not self.logged_in or not self.csrf_token:
|
||||||
|
raise ValueError("Not logged in or missing CSRF token")
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
"Accept": "application/json",
|
||||||
|
"Content-Type": "application/json;charset=UTF-8",
|
||||||
|
"X-Requested-With": "XMLHttpRequest",
|
||||||
|
"Origin": "https://selfcare.lidl-connect.at",
|
||||||
|
"Referer": self.DASHBOARD_URL,
|
||||||
|
"X-CSRF-TOKEN": self.csrf_token,
|
||||||
|
"X-SELF-CARE": "1",
|
||||||
|
"ocrAvailable": "0",
|
||||||
|
}
|
||||||
|
|
||||||
|
payload = {"accountId": self.endpoint_id, "userId": self.user_id, "endpointId": self.endpoint_id}
|
||||||
|
r = self.session.post(self.INVOICES_URL, headers=headers, json=payload)
|
||||||
|
|
||||||
|
if r.status_code != 200:
|
||||||
|
if r.status_code == 422 and r.text == '[]':
|
||||||
|
return []
|
||||||
|
raise ValueError(f"Invoices request failed: {r.status_code} {r.text!r}")
|
||||||
|
|
||||||
|
return r.json()
|
||||||
|
|
||||||
|
@ttl_cache(30)
|
||||||
|
def get_vouchers(self) -> List[Dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
Get list of consumed vouchers for the current account.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List[Dict]: List of voucher objects with transaction details
|
||||||
|
"""
|
||||||
|
if not self.logged_in or not self.csrf_token:
|
||||||
|
raise ValueError("Not logged in or missing CSRF token")
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
"Accept": "application/json",
|
||||||
|
"Content-Type": "application/json;charset=UTF-8",
|
||||||
|
"X-Requested-With": "XMLHttpRequest",
|
||||||
|
"Origin": "https://selfcare.lidl-connect.at",
|
||||||
|
"Referer": self.DASHBOARD_URL,
|
||||||
|
"X-CSRF-TOKEN": self.csrf_token,
|
||||||
|
"X-SELF-CARE": "1",
|
||||||
|
"ocrAvailable": "0",
|
||||||
|
}
|
||||||
|
|
||||||
|
payload = {"userId": self.user_id, "endpointId": self.endpoint_id}
|
||||||
|
r = self.session.post(self.VOUCHER_URL, headers=headers, json=payload)
|
||||||
|
|
||||||
|
if r.status_code != 200:
|
||||||
|
if r.status_code == 422 and r.text == '[]':
|
||||||
|
return []
|
||||||
|
raise ValueError(f"Vouchers request failed: {r.status_code} {r.text!r}")
|
||||||
|
|
||||||
|
return r.json()
|
||||||
|
|
||||||
|
def print_invoices(self) -> None:
|
||||||
|
"""
|
||||||
|
Pretty-print invoice and voucher history.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
invoices = self.get_invoices()
|
||||||
|
vouchers = self.get_vouchers()
|
||||||
|
|
||||||
|
if not invoices and not vouchers:
|
||||||
|
print("\nNo payment history available")
|
||||||
|
return
|
||||||
|
|
||||||
|
if invoices:
|
||||||
|
print(f"\n{'=' * 20} INVOICE HISTORY {'=' * 20}")
|
||||||
|
|
||||||
|
for invoice in invoices:
|
||||||
|
try:
|
||||||
|
posting_date = datetime.fromisoformat(invoice.get("postingDate").replace("Z", "+00:00"))
|
||||||
|
date_str = posting_date.strftime("%d %b %Y, %H:%M")
|
||||||
|
except (ValueError, AttributeError):
|
||||||
|
date_str = invoice.get("postingDate", "Unknown date")
|
||||||
|
|
||||||
|
payment_type = invoice.get("type", "").capitalize()
|
||||||
|
provider = invoice.get("provider", "")
|
||||||
|
channel = invoice.get("channel", "").replace("_", " ").title()
|
||||||
|
payment_method = f"{payment_type} via {provider} ({channel})"
|
||||||
|
|
||||||
|
print(f"\n{'-' * 60}")
|
||||||
|
print(f"Transaction ID: {invoice.get('id')}")
|
||||||
|
print(f"Date: {date_str}")
|
||||||
|
print(f"Amount: €{invoice.get('amount', 0):.2f}")
|
||||||
|
print(f"Payment Method: {payment_method}")
|
||||||
|
|
||||||
|
if vouchers:
|
||||||
|
print(f"\n{'=' * 20} VOUCHER HISTORY {'=' * 20}")
|
||||||
|
|
||||||
|
for voucher in vouchers:
|
||||||
|
try:
|
||||||
|
posting_date = datetime.fromisoformat(voucher.get("consumedDate").replace("Z", "+00:00"))
|
||||||
|
date_str = posting_date.strftime("%d %b %Y, %H:%M")
|
||||||
|
except (ValueError, AttributeError):
|
||||||
|
date_str = voucher.get("consumedDate", "Unknown date")
|
||||||
|
|
||||||
|
print(f"\n{'-' * 60}")
|
||||||
|
print(f"Voucher ID: {voucher.get('id')}")
|
||||||
|
print(f"Serial: {voucher.get('serial')}")
|
||||||
|
print(f"Value: €{voucher.get('balanceAdvice', 0):.2f}")
|
||||||
|
print(f"Consumed Date: {date_str}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"\nError fetching payment history: {str(e)}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
|
def get_total_spent(self) -> float:
|
||||||
|
"""
|
||||||
|
Calculate the total amount spent across all invoices and vouchers.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
float: Total amount in euros
|
||||||
|
"""
|
||||||
|
total = 0.0
|
||||||
|
try:
|
||||||
|
invoices = self.get_invoices()
|
||||||
|
vouchers = self.get_vouchers()
|
||||||
|
total += sum(invoice.get("amount", 0) for invoice in invoices)
|
||||||
|
total += sum(voucher.get("balanceAdvice", 0) for voucher in vouchers)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return total
|
||||||
|
|
||||||
|
@property
|
||||||
|
def last_payment_date(self) -> Optional[str]:
|
||||||
|
"""
|
||||||
|
Get the date of the most recent payment from either invoices or vouchers.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: ISO formatted date string or None if no payments
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
all_dates = []
|
||||||
|
|
||||||
|
invoices = self.get_invoices()
|
||||||
|
invoice_dates = [(invoice.get("postingDate"), invoice.get("amount"))
|
||||||
|
for invoice in invoices if invoice.get("postingDate")]
|
||||||
|
|
||||||
|
vouchers = self.get_vouchers()
|
||||||
|
voucher_dates = [(voucher.get("consumedDate"), voucher.get("balanceAdvice"))
|
||||||
|
for voucher in vouchers if voucher.get("consumedDate")]
|
||||||
|
|
||||||
|
all_dates = invoice_dates + voucher_dates
|
||||||
|
if not all_dates:
|
||||||
|
return None
|
||||||
|
|
||||||
|
all_dates.sort(key=lambda x: x[0], reverse=True)
|
||||||
|
return all_dates[0][0]
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
138
LidlConnect/api/tariffs.py
Normal file
138
LidlConnect/api/tariffs.py
Normal file
|
|
@ -0,0 +1,138 @@
|
||||||
|
"""Tariffs-related API functionality for Lidl Connect."""
|
||||||
|
|
||||||
|
from typing import Dict, Any, List
|
||||||
|
from ..helpers import ttl_cache
|
||||||
|
|
||||||
|
class TariffsMixin:
|
||||||
|
"""Tariffs-related API methods for Lidl Connect API."""
|
||||||
|
|
||||||
|
TARIFFS_URL = "https://selfcare.lidl-connect.at/customer/tariffs/all-packages"
|
||||||
|
|
||||||
|
@ttl_cache(60)
|
||||||
|
def get_tariffs(self) -> List[Dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
Get all available tariffs for the current account.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List[Dict]: List of tariff objects with relevant information
|
||||||
|
"""
|
||||||
|
if not self.logged_in or not self.csrf_token:
|
||||||
|
raise ValueError("Not logged in or missing CSRF token")
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
"Accept": "application/json",
|
||||||
|
"Content-Type": "application/json;charset=UTF-8",
|
||||||
|
"X-Requested-With": "XMLHttpRequest",
|
||||||
|
"Origin": "https://selfcare.lidl-connect.at",
|
||||||
|
"Referer": self.DASHBOARD_URL,
|
||||||
|
"X-CSRF-TOKEN": self.csrf_token,
|
||||||
|
"X-SELF-CARE": "1",
|
||||||
|
}
|
||||||
|
|
||||||
|
payload = {"userId": self.user_id, "endpointId": self.endpoint_id}
|
||||||
|
r = self.session.post(self.TARIFFS_URL, headers=headers, json=payload)
|
||||||
|
|
||||||
|
if r.status_code != 200:
|
||||||
|
raise ValueError(f"Tariffs request failed: {r.status_code} {r.text!r}")
|
||||||
|
|
||||||
|
data = r.json()
|
||||||
|
tariffs = [item for item in data if item.get("category") == "TARIFF"]
|
||||||
|
|
||||||
|
result = []
|
||||||
|
for tariff in tariffs:
|
||||||
|
processed_tariff = {
|
||||||
|
"id": tariff.get("id"),
|
||||||
|
"itemId": tariff.get("itemId"),
|
||||||
|
"name": tariff.get("name"),
|
||||||
|
"description": tariff.get("description"),
|
||||||
|
"sort": tariff.get("sort", 0),
|
||||||
|
"featured": tariff.get("featured", False),
|
||||||
|
"visible": tariff.get("visible", True),
|
||||||
|
}
|
||||||
|
|
||||||
|
translations = tariff.get("translations")
|
||||||
|
if translations and isinstance(translations, dict):
|
||||||
|
german_translation = translations.get("de", {})
|
||||||
|
if german_translation and isinstance(german_translation, dict):
|
||||||
|
processed_tariff["german_name"] = german_translation.get("name")
|
||||||
|
processed_tariff["german_content"] = german_translation.get("content")
|
||||||
|
processed_tariff["german_teaser"] = german_translation.get("teaser")
|
||||||
|
|
||||||
|
result.append(processed_tariff)
|
||||||
|
|
||||||
|
result.sort(key=lambda x: x.get("sort", 0))
|
||||||
|
return result
|
||||||
|
|
||||||
|
def print_tariffs(self) -> None:
|
||||||
|
"""
|
||||||
|
Pretty-print available tariffs.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
tariffs = self.get_tariffs()
|
||||||
|
|
||||||
|
if not tariffs:
|
||||||
|
print("\nNo tariff information available")
|
||||||
|
return
|
||||||
|
|
||||||
|
print(f"\n{'=' * 20} AVAILABLE TARIFFS {'=' * 20}")
|
||||||
|
for tariff in tariffs:
|
||||||
|
print(f"\n{'-' * 60}")
|
||||||
|
print(f"Name: {tariff.get('german_name') or tariff.get('name')}")
|
||||||
|
print(f"ID: {tariff.get('itemId')}")
|
||||||
|
|
||||||
|
if tariff.get('german_teaser'):
|
||||||
|
import html
|
||||||
|
import re
|
||||||
|
|
||||||
|
# HTML entities
|
||||||
|
teaser = html.unescape(tariff.get('german_teaser'))
|
||||||
|
|
||||||
|
# style attributes
|
||||||
|
teaser = re.sub(r'style="[^"]*"', '', teaser)
|
||||||
|
teaser = re.sub(r'class="[^"]*"', '', teaser)
|
||||||
|
|
||||||
|
# table structure
|
||||||
|
teaser = re.sub(r'<tr[^>]*>', '\n', teaser)
|
||||||
|
teaser = re.sub(r'<td[^>]*>', ' | ', teaser)
|
||||||
|
teaser = re.sub(r'</tr>', '', teaser)
|
||||||
|
teaser = re.sub(r'</td>', '', teaser)
|
||||||
|
|
||||||
|
# line breaks
|
||||||
|
teaser = teaser.replace('<br>', '\n').replace('<br/>', '\n')
|
||||||
|
teaser = teaser.replace('<p>', '').replace('</p>', '\n')
|
||||||
|
|
||||||
|
# divs and spans
|
||||||
|
teaser = re.sub(r'<div[^>]*>', '', teaser)
|
||||||
|
teaser = re.sub(r'</div>', '', teaser)
|
||||||
|
teaser = re.sub(r'<span[^>]*>', '', teaser)
|
||||||
|
teaser = re.sub(r'</span>', '', teaser)
|
||||||
|
|
||||||
|
# other HTML tags
|
||||||
|
teaser = re.sub(r'<[^>]+>', '', teaser)
|
||||||
|
|
||||||
|
# whitespace and cleanup
|
||||||
|
teaser = re.sub(r'\s+', ' ', teaser)
|
||||||
|
teaser = re.sub(r' \| ', ' | ', teaser)
|
||||||
|
teaser = re.sub(r'\n\s+', '\n', teaser)
|
||||||
|
|
||||||
|
# newlines
|
||||||
|
teaser = re.sub(r'\n+', '\n', teaser)
|
||||||
|
|
||||||
|
# special chars
|
||||||
|
teaser = teaser.replace('\\u00a0', ' ')
|
||||||
|
|
||||||
|
# keep only printable ASCII chars
|
||||||
|
teaser = ''.join([i if ord(i) < 128 else ' ' for i in teaser])
|
||||||
|
|
||||||
|
# other table tags
|
||||||
|
for tag in ['<table>', '</table>', '<tbody>', '</tbody>']:
|
||||||
|
teaser = teaser.replace(tag, '')
|
||||||
|
|
||||||
|
print(f"\nDetails:\n{teaser.strip()}")
|
||||||
|
|
||||||
|
print(f"Featured: {'✓' if tariff.get('featured') else '✗'}")
|
||||||
|
print(f"Visible: {'✓' if tariff.get('visible') else '✗'}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"\nError fetching tariffs: {str(e)}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
197
LidlConnect/api/usage.py
Normal file
197
LidlConnect/api/usage.py
Normal file
|
|
@ -0,0 +1,197 @@
|
||||||
|
"""Usage-related API functionality for Lidl Connect."""
|
||||||
|
|
||||||
|
from typing import Dict, Any, Optional
|
||||||
|
from ..helpers import ttl_cache
|
||||||
|
|
||||||
|
class UsageMixin:
|
||||||
|
"""Usage-related API methods for Lidl Connect."""
|
||||||
|
|
||||||
|
USAGE_URL = "https://selfcare.lidl-connect.at/customer/usage/"
|
||||||
|
|
||||||
|
@ttl_cache(5)
|
||||||
|
def get_usage_data(self) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Get usage data for the current account.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict: Usage data including instanceGroups with counters
|
||||||
|
"""
|
||||||
|
if not self.logged_in or not self.csrf_token:
|
||||||
|
raise ValueError("Not logged in or missing CSRF token")
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
"Accept": "application/json",
|
||||||
|
"Content-Type": "application/json;charset=UTF-8",
|
||||||
|
"X-Requested-With": "XMLHttpRequest",
|
||||||
|
"Origin": "https://selfcare.lidl-connect.at",
|
||||||
|
"Referer": self.DASHBOARD_URL,
|
||||||
|
"X-CSRF-TOKEN": self.csrf_token,
|
||||||
|
"X-SELF-CARE": "1",
|
||||||
|
}
|
||||||
|
payload = {"userId": self.user_id, "endpointId": self.endpoint_id}
|
||||||
|
|
||||||
|
r = self.session.post(self.USAGE_URL, headers=headers, json=payload)
|
||||||
|
if r.status_code != 200:
|
||||||
|
raise ValueError(f"Usage request failed: {r.status_code} {r.text!r}")
|
||||||
|
|
||||||
|
return r.json()
|
||||||
|
|
||||||
|
def print_usage_summary(self, data: Optional[Dict[str, Any]] = None) -> None:
|
||||||
|
"""
|
||||||
|
Pretty-print usage summary data.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
data: Optional usage data. If None, will fetch new data
|
||||||
|
"""
|
||||||
|
if data is None:
|
||||||
|
data = self.get_usage_data()
|
||||||
|
|
||||||
|
for group in data.get("instanceGroups", []):
|
||||||
|
print(f"{group['itemName']} ({group['itemCategory']})")
|
||||||
|
for elem in group.get("instanceElements", []):
|
||||||
|
print(f" • Valid: {elem['validFrom']} → {elem['validTo']}")
|
||||||
|
for counter in elem.get("counters", []):
|
||||||
|
nv = counter["niceValue"]
|
||||||
|
unit = nv.get("type") or counter.get("baseValue", {}).get("type", "")
|
||||||
|
print(f" - {counter['counterId']}: {nv['value']} / {nv['initialValue']} {unit}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
def get_remaining_data(self) -> Dict[str, float]:
|
||||||
|
"""
|
||||||
|
Get remaining data balance (in GiB).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict with remaining, total, and used data in GiB
|
||||||
|
"""
|
||||||
|
data = self.get_usage_data()
|
||||||
|
result = {"remaining": 0, "total": 0, "used": 0}
|
||||||
|
|
||||||
|
for group in data.get("instanceGroups", []):
|
||||||
|
for elem in group.get("instanceElements", []):
|
||||||
|
for counter in elem.get("counters", []):
|
||||||
|
if counter["counterId"] == "DATA":
|
||||||
|
nv = counter["niceValue"]
|
||||||
|
if nv.get("type") == "GiB":
|
||||||
|
result["remaining"] = nv["value"]
|
||||||
|
result["total"] = nv["initialValue"]
|
||||||
|
result["used"] = nv["initialValue"] - nv["value"]
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
def get_remaining_eu_data(self) -> Dict[str, float]:
|
||||||
|
"""
|
||||||
|
Get remaining EU data balance (in GiB).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict with remaining, total, and used EU data in GiB
|
||||||
|
"""
|
||||||
|
data = self.get_usage_data()
|
||||||
|
result = {"remaining": 0, "total": 0, "used": 0}
|
||||||
|
|
||||||
|
for group in data.get("instanceGroups", []):
|
||||||
|
for elem in group.get("instanceElements", []):
|
||||||
|
for counter in elem.get("counters", []):
|
||||||
|
if counter["counterId"] == "DATA_EU":
|
||||||
|
nv = counter["niceValue"]
|
||||||
|
if nv.get("type") == "GiB":
|
||||||
|
result["remaining"] = nv["value"]
|
||||||
|
result["total"] = nv["initialValue"]
|
||||||
|
result["used"] = nv["initialValue"] - nv["value"]
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
def get_remaining_minutes(self) -> Dict[str, float]:
|
||||||
|
"""
|
||||||
|
Get remaining voice minutes.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict with remaining, total, and used minutes
|
||||||
|
"""
|
||||||
|
data = self.get_usage_data()
|
||||||
|
result = {"remaining": 0, "total": 0, "used": 0}
|
||||||
|
|
||||||
|
for group in data.get("instanceGroups", []):
|
||||||
|
for elem in group.get("instanceElements", []):
|
||||||
|
for counter in elem.get("counters", []):
|
||||||
|
if counter["counterId"] == "VOICE_SMS":
|
||||||
|
nv = counter["niceValue"]
|
||||||
|
if nv.get("type") == "MIN":
|
||||||
|
result["remaining"] = nv["value"]
|
||||||
|
result["total"] = nv["initialValue"]
|
||||||
|
result["used"] = nv["initialValue"] - nv["value"]
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
@property
|
||||||
|
def tariff_package_valid_from(self) -> Optional[str]:
|
||||||
|
"""
|
||||||
|
Get the start date of the current tariff package.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
ISO formatted date string or None if not available
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
data = self.get_usage_data()
|
||||||
|
for group in data.get("instanceGroups", []):
|
||||||
|
if group.get("itemCategory") == "TARIFF_PACKAGE":
|
||||||
|
for elem in group.get("instanceElements", []):
|
||||||
|
return elem.get("validFrom")
|
||||||
|
return None
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def tariff_package_valid_to(self) -> Optional[str]:
|
||||||
|
"""
|
||||||
|
Get the end date of the current tariff package.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
ISO formatted date string or None if not available
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
data = self.get_usage_data()
|
||||||
|
for group in data.get("instanceGroups", []):
|
||||||
|
if group.get("itemCategory") == "TARIFF_PACKAGE":
|
||||||
|
for elem in group.get("instanceElements", []):
|
||||||
|
return elem.get("validTo")
|
||||||
|
return None
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def tariff_package_details(self) -> Optional[Dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
Get detailed information about the current tariff package.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict containing name, category, validFrom, validTo and other details
|
||||||
|
or None if not available
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
data = self.get_usage_data()
|
||||||
|
for group in data.get("instanceGroups", []):
|
||||||
|
if group.get("itemCategory") == "TARIFF_PACKAGE":
|
||||||
|
result = {
|
||||||
|
"name": group.get("itemName"),
|
||||||
|
"category": group.get("itemCategory"),
|
||||||
|
}
|
||||||
|
|
||||||
|
if group.get("instanceElements") and len(group.get("instanceElements")) > 0:
|
||||||
|
elem = group.get("instanceElements")[0]
|
||||||
|
result.update({
|
||||||
|
"validFrom": elem.get("validFrom"),
|
||||||
|
"validTo": elem.get("validTo"),
|
||||||
|
"counters": [
|
||||||
|
{
|
||||||
|
"id": counter.get("counterId"),
|
||||||
|
"value": counter.get("niceValue", {}).get("value"),
|
||||||
|
"initialValue": counter.get("niceValue", {}).get("initialValue"),
|
||||||
|
"unit": counter.get("niceValue", {}).get("type")
|
||||||
|
}
|
||||||
|
for counter in elem.get("counters", [])
|
||||||
|
]
|
||||||
|
})
|
||||||
|
return result
|
||||||
|
return None
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
138
LidlConnect/api/user.py
Normal file
138
LidlConnect/api/user.py
Normal file
|
|
@ -0,0 +1,138 @@
|
||||||
|
"""User-related API functionality for Lidl Connect."""
|
||||||
|
|
||||||
|
from typing import Dict, Any, Optional
|
||||||
|
from ..helpers import ttl_cache
|
||||||
|
|
||||||
|
class UserDataMixin:
|
||||||
|
"""User data methods for Lidl Connect API."""
|
||||||
|
|
||||||
|
USER_DATA_URL = "https://selfcare.lidl-connect.at/customer/dashboard/login-check"
|
||||||
|
|
||||||
|
@ttl_cache(30)
|
||||||
|
def _get_user_data(self) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Get user data from the server.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict: User data including name, type, accounts, etc.
|
||||||
|
"""
|
||||||
|
if not self.logged_in or not self.csrf_token:
|
||||||
|
raise ValueError("Not logged in or missing CSRF token")
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
"Accept": "application/json",
|
||||||
|
"Content-Type": "application/json;charset=UTF-8",
|
||||||
|
"X-Requested-With": "XMLHttpRequest",
|
||||||
|
"Origin": "https://selfcare.lidl-connect.at",
|
||||||
|
"Referer": self.DASHBOARD_URL,
|
||||||
|
"X-CSRF-TOKEN": self.csrf_token,
|
||||||
|
"X-SELF-CARE": "1",
|
||||||
|
}
|
||||||
|
|
||||||
|
payload = {"userId": self.user_id, "endpointId": self.endpoint_id}
|
||||||
|
r = self.session.post(self.USER_DATA_URL, headers=headers, json=payload)
|
||||||
|
|
||||||
|
if r.status_code != 200:
|
||||||
|
raise ValueError(f"User data request failed: {r.status_code} {r.text!r}")
|
||||||
|
|
||||||
|
return r.json()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def user_name(self) -> Optional[str]:
|
||||||
|
"""Get user's name."""
|
||||||
|
try:
|
||||||
|
return self._get_user_data().get("name")
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def user_type(self) -> Optional[str]:
|
||||||
|
"""Get user's type (e.g., 'CUSTOMER')."""
|
||||||
|
try:
|
||||||
|
return self._get_user_data().get("userType")
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def has_password(self) -> bool:
|
||||||
|
"""Check if user has set a password."""
|
||||||
|
try:
|
||||||
|
return self._get_user_data().get("hasPassword", False)
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def birth_date(self) -> Optional[str]:
|
||||||
|
"""Get user's birth date."""
|
||||||
|
try:
|
||||||
|
return self._get_user_data().get("birthDate")
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def status(self) -> Optional[str]:
|
||||||
|
"""Get endpoint status (e.g., 'ACTIVE')."""
|
||||||
|
try:
|
||||||
|
data = self._get_user_data()
|
||||||
|
for account in data.get("accounts", []):
|
||||||
|
for endpoint in account.get("endpoints", []):
|
||||||
|
if endpoint.get("id") == self.endpoint_id:
|
||||||
|
return endpoint.get("status")
|
||||||
|
return None
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def customer_type(self) -> Optional[str]:
|
||||||
|
"""Get customer type (e.g., 'ANONYM')."""
|
||||||
|
try:
|
||||||
|
return self._get_user_data().get("customerType")
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def customer_language(self) -> Optional[str]:
|
||||||
|
"""Get customer language preference."""
|
||||||
|
try:
|
||||||
|
return self._get_user_data().get("customerLanguage")
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def balance(self) -> Optional[float]:
|
||||||
|
"""Get account balance."""
|
||||||
|
try:
|
||||||
|
data = self._get_user_data()
|
||||||
|
for account in data.get("accounts", []):
|
||||||
|
for endpoint in account.get("endpoints", []):
|
||||||
|
if endpoint.get("id") == self.endpoint_id:
|
||||||
|
return endpoint.get("ocsBalance")
|
||||||
|
return None
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def activation_date(self) -> Optional[str]:
|
||||||
|
"""Get activation date."""
|
||||||
|
try:
|
||||||
|
data = self._get_user_data()
|
||||||
|
for account in data.get("accounts", []):
|
||||||
|
for endpoint in account.get("endpoints", []):
|
||||||
|
if endpoint.get("id") == self.endpoint_id:
|
||||||
|
return endpoint.get("activationDate")
|
||||||
|
return None
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def deactivation_date(self) -> Optional[str]:
|
||||||
|
"""Get deactivation date."""
|
||||||
|
try:
|
||||||
|
data = self._get_user_data()
|
||||||
|
for account in data.get("accounts", []):
|
||||||
|
for endpoint in account.get("endpoints", []):
|
||||||
|
if endpoint.get("id") == self.endpoint_id:
|
||||||
|
return endpoint.get("deactivationDate")
|
||||||
|
return None
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
41
LidlConnect/api/utils.py
Normal file
41
LidlConnect/api/utils.py
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
"""Utility functionality for Lidl Connect API."""
|
||||||
|
|
||||||
|
from typing import Dict, Any, Union
|
||||||
|
|
||||||
|
class ApiMixin:
|
||||||
|
"""General API utilities for Lidl Connect."""
|
||||||
|
|
||||||
|
def make_api_request(self, url: str, data: Dict = None, method: str = "POST") -> Union[Dict[str, Any], str]:
|
||||||
|
"""
|
||||||
|
Make a generic API request to Lidl Connect.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
url: API endpoint to call
|
||||||
|
data: Payload to send (optional)
|
||||||
|
method: HTTP method (default: POST)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict or str: API response (JSON parsed if Content-Type is application/json)
|
||||||
|
"""
|
||||||
|
if not self.logged_in or not self.csrf_token:
|
||||||
|
raise ValueError("Not logged in or missing CSRF token")
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
"Accept": "application/json",
|
||||||
|
"Content-Type": "application/json;charset=UTF-8",
|
||||||
|
"X-Requested-With": "XMLHttpRequest",
|
||||||
|
"Origin": "https://selfcare.lidl-connect.at",
|
||||||
|
"Referer": self.DASHBOARD_URL,
|
||||||
|
"X-CSRF-TOKEN": self.csrf_token,
|
||||||
|
"X-SELF-CARE": "1",
|
||||||
|
}
|
||||||
|
|
||||||
|
if method.upper() == "POST":
|
||||||
|
r = self.session.post(url, headers=headers, json=data or {})
|
||||||
|
else:
|
||||||
|
r = self.session.get(url, headers=headers)
|
||||||
|
|
||||||
|
if r.status_code >= 400:
|
||||||
|
raise ValueError(f"API request failed: {r.status_code} {r.text!r}")
|
||||||
|
|
||||||
|
return r.json() if r.headers.get('Content-Type') == 'application/json' else r.text
|
||||||
56
LidlConnect/auth.py
Normal file
56
LidlConnect/auth.py
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
"""Authentication-related functionality for Lidl Connect."""
|
||||||
|
|
||||||
|
from typing import Dict
|
||||||
|
|
||||||
|
class AuthMixin:
|
||||||
|
"""Authentication methods for Lidl Connect API."""
|
||||||
|
|
||||||
|
LOGIN_PUK_URL = "https://selfcare.lidl-connect.at/de/customer/login/puk"
|
||||||
|
LOGIN_PASSWORD_URL = "https://selfcare.lidl-connect.at/de/customer/login/account"
|
||||||
|
LOGOUT_URL = "https://selfcare.lidl-connect.at/de/customer/logout"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def login_url(self):
|
||||||
|
"""Get the appropriate login URL based on credentials."""
|
||||||
|
return self.LOGIN_PUK_URL if self.puk else self.LOGIN_PASSWORD_URL
|
||||||
|
|
||||||
|
def login(self) -> bool:
|
||||||
|
"""
|
||||||
|
Log in to Lidl Connect.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if login successful
|
||||||
|
"""
|
||||||
|
login_headers = {
|
||||||
|
"Accept": "application/json",
|
||||||
|
"Content-Type": "application/json;charset=utf-8",
|
||||||
|
"X-Requested-With": "XMLHttpRequest",
|
||||||
|
"Origin": "https://selfcare.lidl-connect.at",
|
||||||
|
"Referer": "https://selfcare.lidl-connect.at/en/customer/login",
|
||||||
|
"X-AUTH-SELF-CARE": "1",
|
||||||
|
"locale": "en",
|
||||||
|
}
|
||||||
|
login_payload = {"identifier": self.identifier, "token": self.token}
|
||||||
|
|
||||||
|
r = self.session.post(self.login_url, headers=login_headers, json=login_payload)
|
||||||
|
if r.status_code != 200:
|
||||||
|
return False
|
||||||
|
|
||||||
|
self.logged_in = True
|
||||||
|
return True
|
||||||
|
|
||||||
|
def logout(self) -> bool:
|
||||||
|
"""
|
||||||
|
Log out from Lidl Connect.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if logout successful, False otherwise
|
||||||
|
"""
|
||||||
|
if not self.logged_in:
|
||||||
|
return True
|
||||||
|
|
||||||
|
r = self.session.get(self.LOGOUT_URL)
|
||||||
|
if r.status_code == 200 or r.status_code == 302:
|
||||||
|
self.logged_in = False
|
||||||
|
return True
|
||||||
|
return False
|
||||||
84
LidlConnect/client.py
Normal file
84
LidlConnect/client.py
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
"""Main client class for Lidl Connect API."""
|
||||||
|
|
||||||
|
import requests
|
||||||
|
import signal
|
||||||
|
import atexit
|
||||||
|
|
||||||
|
from .auth import AuthMixin
|
||||||
|
from .extractors import ExtractorMixin
|
||||||
|
from .api.usage import UsageMixin
|
||||||
|
from .api.utils import ApiMixin
|
||||||
|
from .api.user import UserDataMixin
|
||||||
|
from .api.tariffs import TariffsMixin
|
||||||
|
from .api.invoices import InvoicesMixin
|
||||||
|
|
||||||
|
class LidlConnect(AuthMixin, ExtractorMixin, UsageMixin, ApiMixin, UserDataMixin, TariffsMixin, InvoicesMixin):
|
||||||
|
"""Client for interacting with Lidl Connect Self-Care portal."""
|
||||||
|
|
||||||
|
DASHBOARD_URL = "https://selfcare.lidl-connect.at/customer/dashboard/"
|
||||||
|
|
||||||
|
def __init__(self, identifier: str, puk: str = None, password: str = None):
|
||||||
|
"""
|
||||||
|
Initialize Lidl Connect client.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
identifier: Your phone number or customer ID
|
||||||
|
puk: Your PUK code (optional if password provided)
|
||||||
|
password: Your password (optional if PUK provided)
|
||||||
|
"""
|
||||||
|
# Base components
|
||||||
|
self.identifier = identifier
|
||||||
|
self.puk = puk
|
||||||
|
self.password = password
|
||||||
|
self.token = puk if puk else password
|
||||||
|
|
||||||
|
# Session and tokens
|
||||||
|
self.session = requests.Session()
|
||||||
|
self.csrf_token = None
|
||||||
|
self.endpoint_id = None
|
||||||
|
self.logged_in = False
|
||||||
|
|
||||||
|
# User data
|
||||||
|
self.user_id = None
|
||||||
|
|
||||||
|
atexit.register(self._cleanup)
|
||||||
|
signal.signal(signal.SIGINT, self._signal_handler)
|
||||||
|
signal.signal(signal.SIGTERM, self._signal_handler)
|
||||||
|
|
||||||
|
def _cleanup(self):
|
||||||
|
"""Clean up resources and log out when program exits."""
|
||||||
|
if self.logged_in:
|
||||||
|
try:
|
||||||
|
self.logout()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error during logout at program shutdown: {e}")
|
||||||
|
|
||||||
|
def _signal_handler(self, signum, frame):
|
||||||
|
"""Handle termination signals to ensure clean logout."""
|
||||||
|
print("\nCaught termination signal. Logging out...")
|
||||||
|
self._cleanup()
|
||||||
|
signal.signal(signum, signal.SIG_DFL)
|
||||||
|
signal.raise_signal(signum)
|
||||||
|
|
||||||
|
def initialize(self) -> bool:
|
||||||
|
"""
|
||||||
|
Initialize the client: login, fetch dashboard, and extract necessary tokens and IDs.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if initialization successful
|
||||||
|
"""
|
||||||
|
if not self.login():
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
soup = self._fetch_dashboard()
|
||||||
|
self.csrf_token = self._extract_csrf(soup)
|
||||||
|
self.user_id, self.endpoint_id = self._extract_user_and_endpoint(soup)
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error during initialization: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _fetch_dashboard(self):
|
||||||
|
"""Fetch dashboard HTML and parse it."""
|
||||||
|
return self._get_soup(self.DASHBOARD_URL)
|
||||||
39
LidlConnect/extractors.py
Normal file
39
LidlConnect/extractors.py
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
"""HTML and data extraction for Lidl Connect API."""
|
||||||
|
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
import re
|
||||||
|
from typing import Tuple
|
||||||
|
|
||||||
|
class ExtractorMixin:
|
||||||
|
"""HTML extraction methods for Lidl Connect API."""
|
||||||
|
|
||||||
|
def _get_soup(self, url: str) -> BeautifulSoup:
|
||||||
|
"""Get BeautifulSoup object from URL."""
|
||||||
|
r = self.session.get(url)
|
||||||
|
return BeautifulSoup(r.text, "html.parser")
|
||||||
|
|
||||||
|
def _extract_csrf(self, soup: BeautifulSoup) -> str:
|
||||||
|
"""Extract CSRF token from dashboard HTML."""
|
||||||
|
meta = soup.find("meta", {"name": "csrf-token"})
|
||||||
|
if not meta or not meta.get("content"):
|
||||||
|
raise ValueError("CSRF token not found in dashboard HTML")
|
||||||
|
return meta["content"]
|
||||||
|
|
||||||
|
def _extract_user_and_endpoint(self, soup: BeautifulSoup) -> Tuple[int, int]:
|
||||||
|
"""Extract user ID and endpoint ID from dashboard HTML."""
|
||||||
|
all_scripts = ""
|
||||||
|
for script in soup.find_all("script"):
|
||||||
|
if script.string:
|
||||||
|
all_scripts += script.string
|
||||||
|
|
||||||
|
user_match = re.search(r"window\.user\s*=\s*\{.*?'user':\s*\{\s*\"id\":\s*(\d+).*?\"userType\":\s*\"CUSTOMER\"", all_scripts, re.DOTALL)
|
||||||
|
endpoint_match = re.search(r'"endpoints":\s*\[\{\s*"id":\s*(\d+)', all_scripts, re.DOTALL)
|
||||||
|
|
||||||
|
if not user_match or not endpoint_match:
|
||||||
|
user_match = re.search(r'"id":\s*(\d+).*?"userType":\s*"CUSTOMER"', all_scripts, re.DOTALL)
|
||||||
|
endpoint_match = re.search(r'"endpoints":\s*\[\{\s*"id":\s*(\d+)', all_scripts, re.DOTALL)
|
||||||
|
|
||||||
|
if not user_match or not endpoint_match:
|
||||||
|
raise ValueError("Could not extract userId or endpointId from scripts")
|
||||||
|
|
||||||
|
return int(user_match.group(1)), int(endpoint_match.group(1))
|
||||||
34
LidlConnect/helpers.py
Normal file
34
LidlConnect/helpers.py
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
"""Helper functions for Lidl Connect library."""
|
||||||
|
|
||||||
|
from functools import wraps
|
||||||
|
import time
|
||||||
|
|
||||||
|
def ttl_cache(ttl_seconds=30):
|
||||||
|
"""
|
||||||
|
Time-based cache decorator.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
ttl_seconds: Number of seconds to cache the result
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Decorated function with caching
|
||||||
|
"""
|
||||||
|
def decorator(func):
|
||||||
|
cache = {}
|
||||||
|
|
||||||
|
@wraps(func)
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
now = time.time()
|
||||||
|
key = str(args) + str(kwargs)
|
||||||
|
|
||||||
|
if key in cache:
|
||||||
|
result, timestamp = cache[key]
|
||||||
|
if now - timestamp < ttl_seconds:
|
||||||
|
return result
|
||||||
|
|
||||||
|
result = func(*args, **kwargs)
|
||||||
|
cache[key] = (result, now)
|
||||||
|
return result
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
return decorator
|
||||||
2
LidlConnect/requirements.txt
Normal file
2
LidlConnect/requirements.txt
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
requests
|
||||||
|
bs4
|
||||||
84
README.md
Normal file
84
README.md
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
# LidlConnect.py
|
||||||
|
|
||||||
|
[](https://badge.fury.io/py/LidlConnect.py)
|
||||||
|
[](https://opensource.org/licenses/MIT)
|
||||||
|
|
||||||
|
A Python library for accessing your Lidl Connect account through the Self-Care API.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- 📱 View your current data, minutes, and SMS usage
|
||||||
|
- 💰 Check your account balance and payment history
|
||||||
|
- 📅 View tariff details and package validity dates
|
||||||
|
- 🧾 Access invoice and voucher history
|
||||||
|
- 🔄 Automatic session management with proper login/logout
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
Install directly from PyPI:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install LidlConnect.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```python
|
||||||
|
from LidlConnect import LidlConnect
|
||||||
|
|
||||||
|
# Initialize with PUK (preferred method)
|
||||||
|
client = LidlConnect(identifier="069012345678", puk="12345678")
|
||||||
|
|
||||||
|
# Or initialize with password
|
||||||
|
# client = LidlConnect(identifier="069012345678", password="yourPassword")
|
||||||
|
|
||||||
|
# Login and initialize connection
|
||||||
|
if not client.initialize():
|
||||||
|
print("Failed to initialize client")
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
# Get remaining data
|
||||||
|
data_info = client.get_remaining_data()
|
||||||
|
print(f"Data: {data_info['remaining']}/{data_info['total']} GiB")
|
||||||
|
|
||||||
|
# Check minutes
|
||||||
|
minutes_info = client.get_remaining_minutes()
|
||||||
|
print(f"Minutes: {minutes_info['remaining']}/{minutes_info['total']} minutes")
|
||||||
|
|
||||||
|
# Get EU roaming data
|
||||||
|
eu_data_info = client.get_remaining_eu_data()
|
||||||
|
print(f"EU Data: {eu_data_info['remaining']}/{eu_data_info['total']} GiB")
|
||||||
|
|
||||||
|
# Access account information
|
||||||
|
print(f"User: {client.user_name}")
|
||||||
|
print(f"Balance: €{client.balance if client.balance is not None else 'N/A'}")
|
||||||
|
print(f"Package valid until: {client.tariff_package_valid_to}")
|
||||||
|
|
||||||
|
# View payment history
|
||||||
|
client.print_invoices()
|
||||||
|
|
||||||
|
# Logout when done (automatic on program exit, but explicit is better)
|
||||||
|
client.logout()
|
||||||
|
```
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
For complete documentation of all features and methods, please refer to the [API documentation](apiDoc.md).
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Python 3.6+
|
||||||
|
- `requests` library
|
||||||
|
- `beautifulsoup4` library
|
||||||
|
|
||||||
|
## Supported Services
|
||||||
|
|
||||||
|
This library currently supports the Austrian Lidl Connect service (https://selfcare.lidl-connect.at). It's designed to work with accounts that have at least one active tariff package.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
## Disclaimer
|
||||||
|
|
||||||
|
This is an unofficial library and is not affiliated with, maintained, authorized, endorsed, or sponsored by Lidl or any of its affiliates (like Drei).
|
||||||
535
apiDoc.md
Normal file
535
apiDoc.md
Normal file
|
|
@ -0,0 +1,535 @@
|
||||||
|
# LidlConnect API Documentation
|
||||||
|
|
||||||
|
A Python client library for interacting with the Lidl Connect Self-Care portal API.
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
- [Installation](#installation)
|
||||||
|
- [Quick Start](#quick-start)
|
||||||
|
- [Authentication](#authentication)
|
||||||
|
- [Core Client](#core-client)
|
||||||
|
- [Usage Data](#usage-data)
|
||||||
|
- [User Information](#user-information)
|
||||||
|
- [Tariffs](#tariffs)
|
||||||
|
- [Invoices and Payments](#invoices-and-payments)
|
||||||
|
- [Utility Methods](#utility-methods)
|
||||||
|
- [Caching](#caching)
|
||||||
|
- [Error Handling](#error-handling)
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Clone the repository
|
||||||
|
git clone https://github.com/MarcUs7i/LidlConnect.git
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
Required dependencies:
|
||||||
|
- `requests`
|
||||||
|
- `beautifulsoup4`
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```python
|
||||||
|
from LidlConnect import LidlConnect
|
||||||
|
|
||||||
|
# Initialize with PUK
|
||||||
|
client = LidlConnect(identifier="069012345678", puk="12345678")
|
||||||
|
|
||||||
|
# Or initialize with password
|
||||||
|
# client = LidlConnect(identifier="069012345678", password="yourPassword")
|
||||||
|
|
||||||
|
# Login and initialize connection
|
||||||
|
if not client.initialize():
|
||||||
|
print("Failed to initialize client")
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
# Get usage information
|
||||||
|
data_info = client.get_remaining_data()
|
||||||
|
print(f"Data: {data_info['remaining']}/{data_info['total']} GiB")
|
||||||
|
|
||||||
|
# Logout when done
|
||||||
|
client.logout()
|
||||||
|
```
|
||||||
|
|
||||||
|
## Authentication
|
||||||
|
|
||||||
|
### LidlConnect(identifier, puk=None, password=None)
|
||||||
|
|
||||||
|
Initialize the Lidl Connect client.
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
- `identifier` (str): Your phone number or customer ID
|
||||||
|
- `puk` (str, optional): Your PUK code (required if password not provided)
|
||||||
|
- `password` (str, optional): Your password (required if PUK not provided)
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```python
|
||||||
|
# Using PUK
|
||||||
|
client = LidlConnect(identifier="069012345678", puk="12345678")
|
||||||
|
|
||||||
|
# Using password
|
||||||
|
client = LidlConnect(identifier="069012345678", password="yourPassword")
|
||||||
|
```
|
||||||
|
|
||||||
|
### initialize() → bool
|
||||||
|
|
||||||
|
Initialize the client by logging in, fetching the dashboard, and extracting necessary tokens and IDs.
|
||||||
|
|
||||||
|
**Returns:**
|
||||||
|
- `bool`: True if initialization successful, False otherwise
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```python
|
||||||
|
if not client.initialize():
|
||||||
|
print("Failed to initialize client")
|
||||||
|
exit(1)
|
||||||
|
```
|
||||||
|
|
||||||
|
### login() → bool
|
||||||
|
|
||||||
|
Log in to Lidl Connect using the provided credentials.
|
||||||
|
|
||||||
|
**Returns:**
|
||||||
|
- `bool`: True if login successful, False otherwise
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```python
|
||||||
|
success = client.login()
|
||||||
|
```
|
||||||
|
|
||||||
|
### logout() → bool
|
||||||
|
|
||||||
|
Log out from Lidl Connect, clearing the session.
|
||||||
|
|
||||||
|
**Returns:**
|
||||||
|
- `bool`: True if logout successful, False otherwise
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```python
|
||||||
|
client.logout()
|
||||||
|
```
|
||||||
|
|
||||||
|
## Core Client
|
||||||
|
|
||||||
|
The client automatically handles session management and token extraction, it tries to log out when the program is about to exit or to get killed.
|
||||||
|
|
||||||
|
**Properties:**
|
||||||
|
- `identifier` (str): The phone number or customer ID
|
||||||
|
- `user_id` (int): The user ID extracted from the dashboard
|
||||||
|
- `endpoint_id` (int): The endpoint ID extracted from the dashboard
|
||||||
|
- `logged_in` (bool): Whether the client is currently logged in
|
||||||
|
- `csrf_token` (str): The CSRF token for API requests
|
||||||
|
|
||||||
|
## Usage Data
|
||||||
|
|
||||||
|
Methods for getting and displaying usage data.
|
||||||
|
|
||||||
|
### get_usage_data() → Dict[str, Any]
|
||||||
|
|
||||||
|
Get raw usage data for the current account.
|
||||||
|
|
||||||
|
**Returns:**
|
||||||
|
- `Dict[str, Any]`: Usage data including instanceGroups with counters
|
||||||
|
|
||||||
|
**Cache TTL:** 5 seconds
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```python
|
||||||
|
usage_data = client.get_usage_data()
|
||||||
|
```
|
||||||
|
|
||||||
|
### print_usage_summary(data=None) → None
|
||||||
|
|
||||||
|
Pretty-print usage summary data.
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
- `data` (Dict[str, Any], optional): Optional usage data. If None, will fetch new data
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```python
|
||||||
|
client.print_usage_summary()
|
||||||
|
```
|
||||||
|
|
||||||
|
### get_remaining_data() → Dict[str, float]
|
||||||
|
|
||||||
|
Get remaining data balance (in GiB).
|
||||||
|
|
||||||
|
**Returns:**
|
||||||
|
- `Dict[str, float]`: Dictionary with keys "remaining", "total", and "used" in GiB
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```python
|
||||||
|
data_info = client.get_remaining_data()
|
||||||
|
print(f"Data: {data_info['remaining']}/{data_info['total']} GiB")
|
||||||
|
```
|
||||||
|
|
||||||
|
### get_remaining_eu_data() → Dict[str, float]
|
||||||
|
|
||||||
|
Get remaining EU data balance (in GiB).
|
||||||
|
|
||||||
|
**Returns:**
|
||||||
|
- `Dict[str, float]`: Dictionary with keys "remaining", "total", and "used" in GiB
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```python
|
||||||
|
eu_data_info = client.get_remaining_eu_data()
|
||||||
|
print(f"EU Data: {eu_data_info['remaining']}/{eu_data_info['total']} GiB")
|
||||||
|
```
|
||||||
|
|
||||||
|
### get_remaining_minutes() → Dict[str, float]
|
||||||
|
|
||||||
|
Get remaining voice minutes.
|
||||||
|
|
||||||
|
**Returns:**
|
||||||
|
- `Dict[str, float]`: Dictionary with keys "remaining", "total", and "used" in minutes
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```python
|
||||||
|
minutes_info = client.get_remaining_minutes()
|
||||||
|
print(f"Minutes: {minutes_info['remaining']}/{minutes_info['total']} minutes")
|
||||||
|
```
|
||||||
|
|
||||||
|
### tariff_package_valid_from → Optional[str]
|
||||||
|
|
||||||
|
Get the start date of the current tariff package.
|
||||||
|
|
||||||
|
**Returns:**
|
||||||
|
- `str`: ISO formatted date string or None if not available
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```python
|
||||||
|
print(f"Package valid from: {client.tariff_package_valid_from}")
|
||||||
|
```
|
||||||
|
|
||||||
|
### tariff_package_valid_to → Optional[str]
|
||||||
|
|
||||||
|
Get the end date of the current tariff package.
|
||||||
|
|
||||||
|
**Returns:**
|
||||||
|
- `str`: ISO formatted date string or None if not available
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```python
|
||||||
|
print(f"Package valid to: {client.tariff_package_valid_to}")
|
||||||
|
```
|
||||||
|
|
||||||
|
### tariff_package_details → Optional[Dict[str, Any]]
|
||||||
|
|
||||||
|
Get detailed information about the current tariff package.
|
||||||
|
|
||||||
|
**Returns:**
|
||||||
|
- `Dict[str, Any]`: Dictionary containing name, category, validFrom, validTo and counter details or None if not available
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```python
|
||||||
|
details = client.tariff_package_details
|
||||||
|
if details:
|
||||||
|
print(f"Package name: {details['name']}")
|
||||||
|
print(f"Valid from: {details['validFrom']} to {details['validTo']}")
|
||||||
|
```
|
||||||
|
|
||||||
|
## User Information
|
||||||
|
|
||||||
|
Methods for retrieving user account information.
|
||||||
|
|
||||||
|
### _get_user_data() → Dict[str, Any]
|
||||||
|
|
||||||
|
Get user data from the server. This is an internal method used by the properties below.
|
||||||
|
|
||||||
|
**Returns:**
|
||||||
|
- `Dict[str, Any]`: User data including name, type, accounts, etc.
|
||||||
|
|
||||||
|
**Cache TTL:** 30 seconds
|
||||||
|
|
||||||
|
### user_name → Optional[str]
|
||||||
|
|
||||||
|
Get user's name.
|
||||||
|
|
||||||
|
**Returns:**
|
||||||
|
- `str`: User's name or None if not available
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```python
|
||||||
|
print(f"User name: {client.user_name}")
|
||||||
|
```
|
||||||
|
|
||||||
|
### user_type → Optional[str]
|
||||||
|
|
||||||
|
Get user's type (e.g., 'CUSTOMER').
|
||||||
|
|
||||||
|
**Returns:**
|
||||||
|
- `str`: User type or None if not available
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```python
|
||||||
|
print(f"User type: {client.user_type}")
|
||||||
|
```
|
||||||
|
|
||||||
|
### has_password → bool
|
||||||
|
|
||||||
|
Check if user has set a password.
|
||||||
|
|
||||||
|
**Returns:**
|
||||||
|
- `bool`: True if the user has a password, False otherwise
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```python
|
||||||
|
if client.has_password:
|
||||||
|
print("User has a password set")
|
||||||
|
```
|
||||||
|
|
||||||
|
### birth_date → Optional[str]
|
||||||
|
|
||||||
|
Get user's birth date.
|
||||||
|
|
||||||
|
**Returns:**
|
||||||
|
- `str`: Birth date or None if not available
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```python
|
||||||
|
print(f"Birth date: {client.birth_date}")
|
||||||
|
```
|
||||||
|
|
||||||
|
### status → Optional[str]
|
||||||
|
|
||||||
|
Get endpoint status (e.g., 'ACTIVE').
|
||||||
|
|
||||||
|
**Returns:**
|
||||||
|
- `str`: Endpoint status or None if not available
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```python
|
||||||
|
print(f"Status: {client.status}")
|
||||||
|
```
|
||||||
|
|
||||||
|
### customer_type → Optional[str]
|
||||||
|
|
||||||
|
Get customer type (e.g., 'ANONYM').
|
||||||
|
|
||||||
|
**Returns:**
|
||||||
|
- `str`: Customer type or None if not available
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```python
|
||||||
|
print(f"Customer type: {client.customer_type}")
|
||||||
|
```
|
||||||
|
|
||||||
|
### customer_language → Optional[str]
|
||||||
|
|
||||||
|
Get customer language preference.
|
||||||
|
|
||||||
|
**Returns:**
|
||||||
|
- `str`: Customer language or None if not available
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```python
|
||||||
|
print(f"Language: {client.customer_language}")
|
||||||
|
```
|
||||||
|
|
||||||
|
### balance → Optional[float]
|
||||||
|
|
||||||
|
Get account balance.
|
||||||
|
|
||||||
|
**Returns:**
|
||||||
|
- `float`: Account balance or None if not available
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```python
|
||||||
|
print(f"Balance: €{client.balance if client.balance is not None else 'N/A'}")
|
||||||
|
```
|
||||||
|
|
||||||
|
### activation_date → Optional[str]
|
||||||
|
|
||||||
|
Get activation date.
|
||||||
|
|
||||||
|
**Returns:**
|
||||||
|
- `str`: ISO formatted activation date or None if not available
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```python
|
||||||
|
print(f"Activation date: {client.activation_date}")
|
||||||
|
```
|
||||||
|
|
||||||
|
### deactivation_date → Optional[str]
|
||||||
|
|
||||||
|
Get deactivation date.
|
||||||
|
|
||||||
|
**Returns:**
|
||||||
|
- `str`: ISO formatted deactivation date or None if not available
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```python
|
||||||
|
print(f"Deactivation date: {client.deactivation_date}")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Tariffs
|
||||||
|
|
||||||
|
Methods for retrieving and displaying available tariffs.
|
||||||
|
|
||||||
|
### get_tariffs() → List[Dict[str, Any]]
|
||||||
|
|
||||||
|
Get all available tariffs for the current account.
|
||||||
|
|
||||||
|
**Returns:**
|
||||||
|
- `List[Dict[str, Any]]`: List of tariff objects with relevant information
|
||||||
|
|
||||||
|
**Cache TTL:** 60 seconds
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```python
|
||||||
|
tariffs = client.get_tariffs()
|
||||||
|
```
|
||||||
|
|
||||||
|
### print_tariffs() → None
|
||||||
|
|
||||||
|
Pretty-print available tariffs.
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```python
|
||||||
|
client.print_tariffs()
|
||||||
|
```
|
||||||
|
|
||||||
|
The output includes:
|
||||||
|
- Tariff name (German name if available)
|
||||||
|
- Tariff ID
|
||||||
|
- Details (cleaned from HTML, can look terrible)
|
||||||
|
- Featured status
|
||||||
|
- Visibility status
|
||||||
|
|
||||||
|
## Invoices and Payments
|
||||||
|
|
||||||
|
Methods for getting and displaying invoice and voucher history.
|
||||||
|
|
||||||
|
### get_invoices() → List[Dict[str, Any]]
|
||||||
|
|
||||||
|
Get list of invoices for the current account.
|
||||||
|
|
||||||
|
**Returns:**
|
||||||
|
- `List[Dict[str, Any]]`: List of invoice objects with transaction details
|
||||||
|
|
||||||
|
**Cache TTL:** 30 seconds
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```python
|
||||||
|
invoices = client.get_invoices()
|
||||||
|
```
|
||||||
|
|
||||||
|
### get_vouchers() → List[Dict[str, Any]]
|
||||||
|
|
||||||
|
Get list of consumed vouchers for the current account.
|
||||||
|
|
||||||
|
**Returns:**
|
||||||
|
- `List[Dict[str, Any]]`: List of voucher objects with transaction details
|
||||||
|
|
||||||
|
**Cache TTL:** 30 seconds
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```python
|
||||||
|
vouchers = client.get_vouchers()
|
||||||
|
```
|
||||||
|
|
||||||
|
### print_invoices() → None
|
||||||
|
|
||||||
|
Pretty-print invoice and voucher history.
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```python
|
||||||
|
client.print_invoices()
|
||||||
|
```
|
||||||
|
|
||||||
|
The output includes:
|
||||||
|
- Transaction details for invoices (ID, date, amount, payment method)
|
||||||
|
- Voucher details (ID, serial, value, consumption date)
|
||||||
|
|
||||||
|
### get_total_spent() → float
|
||||||
|
|
||||||
|
Calculate the total amount spent across all invoices and vouchers.
|
||||||
|
|
||||||
|
**Returns:**
|
||||||
|
- `float`: Total amount in euros
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```python
|
||||||
|
total = client.get_total_spent()
|
||||||
|
print(f"Total spent: €{total:.2f}")
|
||||||
|
```
|
||||||
|
|
||||||
|
### last_payment_date → Optional[str]
|
||||||
|
|
||||||
|
Get the date of the most recent payment from either invoices or vouchers.
|
||||||
|
|
||||||
|
**Returns:**
|
||||||
|
- `str`: ISO formatted date string or None if no payments
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```python
|
||||||
|
print(f"Last payment date: {client.last_payment_date}")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Utility Methods
|
||||||
|
|
||||||
|
General API utilities for Lidl Connect.
|
||||||
|
|
||||||
|
### make_api_request(url, data=None, method="POST") → Union[Dict[str, Any], str]
|
||||||
|
|
||||||
|
Make a generic API request to Lidl Connect.
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
- `url` (str): API endpoint to call
|
||||||
|
- `data` (Dict, optional): Payload to send
|
||||||
|
- `method` (str, optional): HTTP method (default: "POST")
|
||||||
|
|
||||||
|
**Returns:**
|
||||||
|
- `Dict[str, Any]` or `str`: API response (JSON parsed if Content-Type is application/json)
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```python
|
||||||
|
response = client.make_api_request(
|
||||||
|
"https://selfcare.lidl-connect.at/customer/some-endpoint",
|
||||||
|
data={"key": "value"}
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Caching
|
||||||
|
|
||||||
|
The library has a time-based cache for API requests to reduce load and improve performance.
|
||||||
|
|
||||||
|
Key caching parameters:
|
||||||
|
- Usage data: 5 seconds TTL
|
||||||
|
- User data: 30 seconds TTL
|
||||||
|
- Invoices and vouchers: 30 seconds TTL
|
||||||
|
- Tariffs: 60 seconds TTL
|
||||||
|
|
||||||
|
The cache is implemented using the `ttl_cache` decorator in helpers.py.
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
Most methods raise `ValueError` exceptions when:
|
||||||
|
- Not logged in or missing CSRF token
|
||||||
|
- API requests fail
|
||||||
|
- Required data is missing
|
||||||
|
|
||||||
|
All public properties safely handle exceptions internally and return None when errors occur, ensuring your application doesn't crash.
|
||||||
|
|
||||||
|
Example of error handling:
|
||||||
|
|
||||||
|
```python
|
||||||
|
try:
|
||||||
|
client = LidlConnect(identifier="069012345678", puk="12345678")
|
||||||
|
if not client.initialize():
|
||||||
|
print("Failed to initialize client")
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
# Your code here
|
||||||
|
|
||||||
|
except ValueError as e:
|
||||||
|
print(f"API error: {e}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Unexpected error: {e}")
|
||||||
|
finally:
|
||||||
|
if hasattr(client, 'logout') and client.logged_in:
|
||||||
|
client.logout()
|
||||||
|
```
|
||||||
Loading…
Add table
Add a link
Reference in a new issue