Skip to content

Commit

Permalink
Merge pull request #1 from wvanhed/develop
Browse files Browse the repository at this point in the history
to version v0.3.0
  • Loading branch information
wvanhed authored Dec 27, 2023
2 parents ad57da3 + 0b9136c commit 2edfd7d
Show file tree
Hide file tree
Showing 13 changed files with 160 additions and 49 deletions.
8 changes: 6 additions & 2 deletions .github/workflows/python-app.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,16 @@ jobs:

runs-on: ubuntu-latest

strategy:
matrix:
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]

steps:
- uses: actions/checkout@v3
- name: Set up Python 3.8
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v3
with:
python-version: "3.8"
python-version: ${{ matrix.python-version }}
- name: Install app & dependencies
run: |
python -m pip install --upgrade pip
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ Installeer via:

pip install mijnbib

Of, om een ugrade af te dwingen:

pip install --upgrade mijnbib

## Gebruik

Bijvoorbeeld, het opvragen van je ontleende items kan als volgt (na installatie):
Expand Down
5 changes: 5 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

new: new feature / impr: improvement / fix: bug fix

## v0.3.0 - 2023-12-27

- new: rename base exception to MijnbibError
- impr: general internal improvements

## v0.2.0 - 2023-12-20

- new: Add extend_by_ids method
Expand Down
9 changes: 8 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "mijnbib"
version = "0.2.0"
version = "0.3.0"
description = "Python API voor de website mijn.bibliotheek.be"
readme = "README.md"
authors = [{ name = "Ward Van Heddeghem", email = "[email protected]" }]
Expand Down Expand Up @@ -32,3 +32,10 @@ python_files = ["tests.py", "test_*.py", "*_tests.py"]

[tool.isort]
profile = "black"

[tool.ruff.lint]
extend-select = [
"W", # warning
"B", # flake8-bugbear
# "SIM", # flake8-simplify, for simplified code
]
9 changes: 5 additions & 4 deletions src/mijnbib/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@
# So user can do e.g.
# from mijnbib import MijnBibliotheek, Loan

from .mijnbibliotheek import MijnBibliotheek
from .models import Account, Loan, Reservation
from .plugin_errors import (
from .errors import (
AuthenticationError,
CanNotConnectError,
ExtendLoanError,
IncompatibleSourceError,
ItemAccessError,
PluginError,
MijnbibError,
TemporarySiteError,
)
from .mijnbibliotheek import MijnBibliotheek
from .models import Account, Loan, Reservation
20 changes: 19 additions & 1 deletion src/mijnbib/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,25 @@
import pprint as pp
import sys

from mijnbib import MijnBibliotheek
from mijnbib import AuthenticationError, MijnBibliotheek

CONFIG_FILE = "mijnbib.ini"


def _do_login(args: argparse.Namespace):
print("Trying to log in ...")

print(f"City: : {args.city}")
print(f"Username : {args.username}")

mb = MijnBibliotheek(args.username, args.password, args.city)
try:
mb.login()
except AuthenticationError as e:
print(str(e))
print(f"Logged in: {mb._logged_in}")


def _do_all(args: argparse.Namespace):
print("Retrieving all information ...")

Expand Down Expand Up @@ -98,6 +112,10 @@ def main():
"reservations", parents=[common_parser], help="retrieve reservations for account id"
)
parser_all.set_defaults(func=_do_reservations)
parser_all = subparsers.add_parser(
"login", parents=[common_parser], help="just log in, and report if success or not"
)
parser_all.set_defaults(func=_do_login)

# Add values from ini file as default values
config = configparser.ConfigParser()
Expand Down
18 changes: 11 additions & 7 deletions src/mijnbib/plugin_errors.py → src/mijnbib/errors.py
Original file line number Diff line number Diff line change
@@ -1,30 +1,30 @@
class PluginError(Exception):
class MijnbibError(Exception):
"""Base exception."""


# *** client-side errors ***


class AuthenticationError(PluginError):
class AuthenticationError(MijnbibError):
"""Raised when authentication has failed."""


class ItemAccessError(PluginError):
class ItemAccessError(MijnbibError):
"""Raised when an item (loan, reservation) could not be accessed.
This is likely a client-side error, but in rare cases might have a
server-side cause.
"""


class InvalidExtendLoanURL(PluginError):
class InvalidExtendLoanURL(MijnbibError):
"""Raised when the extending loan(s) url is not considered valid."""


# *** server-side errors ***


class CanNotConnectError(PluginError):
class CanNotConnectError(MijnbibError):
"""Raised when a url can not be reached.
Args:
Expand All @@ -37,7 +37,7 @@ def __init__(self, msg: str, url: str):
self.url = url


class IncompatibleSourceError(PluginError):
class IncompatibleSourceError(MijnbibError):
"""Raised for any general errors in parsing the source.
Args:
Expand All @@ -50,5 +50,9 @@ def __init__(self, msg, html_body: str):
self.html_body = html_body


class ExtendLoanError(PluginError):
class ExtendLoanError(MijnbibError):
"""Raised when extending loan(s) failed for unclear reasons."""


class TemporarySiteError(MijnbibError):
"""Raised when the site reports a temporary error."""
43 changes: 24 additions & 19 deletions src/mijnbib/mijnbibliotheek.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,25 +10,25 @@

import logging
import urllib.error
import urllib.parse
from dataclasses import asdict

import mechanize

from mijnbib.models import Account, Loan, Reservation
from mijnbib.parsers import (
AccountsListPageParser,
ExtendResponsePageParser,
LoansListPageParser,
ReservationsPageParser,
)
from mijnbib.plugin_errors import (
from mijnbib.errors import (
AuthenticationError,
CanNotConnectError,
ExtendLoanError,
IncompatibleSourceError,
InvalidExtendLoanURL,
ItemAccessError,
TemporarySiteError,
)
from mijnbib.models import Account, Loan, Reservation
from mijnbib.parsers import (
AccountsListPageParser,
ExtendResponsePageParser,
LoansListPageParser,
ReservationsPageParser,
)

_log = logging.getLogger(__name__)
Expand Down Expand Up @@ -75,6 +75,7 @@ def get_loans(self, account_id: str) -> list[Loan]:
AuthenticationError
IncompatibleSourceError
ItemAccessError: something went wrong fetching loans
TemporarySiteError
"""
if not self._logged_in:
self.login()
Expand All @@ -83,6 +84,8 @@ def get_loans(self, account_id: str) -> list[Loan]:
html_string = self._open_account_loans_page(url)
try:
loans = LoansListPageParser(html_string, self.BASE_URL, account_id).parse()
except TemporarySiteError as e:
raise e
except Exception as e:
raise IncompatibleSourceError(
f"Problem scraping loans ({str(e)})", html_body=""
Expand Down Expand Up @@ -188,7 +191,7 @@ def extend_loans(self, extend_url: str, execute: bool = False) -> tuple[bool, di
InvalidExtendLoanURL
ExtendLoanError
"""
# TODO: would make more sense to return loan list (since final page is loan page)
# NOTE: would make more sense to return loan list (since final page is loan page)
# Perhaps retrieving those loans again, and check extendability would also be good idea.
if not self._logged_in:
self.login()
Expand All @@ -198,14 +201,16 @@ def extend_loans(self, extend_url: str, execute: bool = False) -> tuple[bool, di
response = self._br.open(extend_url) # pylint: disable=assignment-from-none
except mechanize.HTTPError as e:
if e.code == 500:
raise InvalidExtendLoanURL(f"Probably invalid extend loan URL: {extend_url}")
raise InvalidExtendLoanURL(
f"Probably invalid extend loan URL: {extend_url}"
) from e
else:
raise e

try:
self._br.select_form(id="my-library-extend-loan-form")
except mechanize.FormNotFoundError:
raise IncompatibleSourceError("Can not find extend loan form", html_body="")
except mechanize.FormNotFoundError as e:
raise IncompatibleSourceError("Can not find extend loan form", html_body="") from e

if not execute:
_log.warning("SIMULATING extending the loan. Will stop now.")
Expand All @@ -219,12 +224,12 @@ def extend_loans(self, extend_url: str, execute: bool = False) -> tuple[bool, di
# (e.g. nonexisting id, ids that belong to different library accounts)
# However, if multiple id's, some of them *might* have been extended,
# even if 500 response
raise ExtendLoanError(f"Could not extend loans using url: {extend_url}")
raise ExtendLoanError(f"Could not extend loans using url: {extend_url}") from e
else:
raise e

# disclaimer: not sure if other codes are realistic
success = True if response.code == 200 else False
success = response.code == 200

if success:
_log.debug("Looks like extending the loan(s) was successful")
Expand Down Expand Up @@ -281,16 +286,16 @@ def _log_in(self, url):
self._br["email"] = self._username
self._br["password"] = self._pwd
response = self._br.submit() # pylint: disable=assignment-from-none
except mechanize.FormNotFoundError:
except mechanize.FormNotFoundError as e:
raise IncompatibleSourceError(
"Can not find login form", html_body=html_string_start_page
)
) from e
except urllib.error.URLError as e:
# We specifically catch this because site periodically (maintenance?)
# throws a 500, 502 or 504
raise CanNotConnectError(
f"Error while trying to log in at: {url} ({str(e)})", url
)
) from e
return response

def _validate_logged_in(self, response):
Expand All @@ -316,7 +321,7 @@ def _open_account_loans_page(self, acc_url: str) -> str:
if e.code == 500:
# duh, server crashes on incorrect or nonexisting ID in the link
raise ItemAccessError(
f"Loans url can not be opened. Likely incorrect or "
"Loans url can not be opened. Likely incorrect or "
f"nonexisting account ID in the url '{acc_url}'"
) from e
raise ItemAccessError(
Expand Down
28 changes: 18 additions & 10 deletions src/mijnbib/parsers.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

from bs4 import BeautifulSoup

from mijnbib.errors import TemporarySiteError
from mijnbib.models import Account, Loan, Reservation

_log = logging.getLogger(__name__)
Expand Down Expand Up @@ -110,10 +111,12 @@ def parse(self) -> list[Loan]:
)
# Sometimes, this error is present
if soup.find(string=re.compile(error_msg)) is not None:
# TODO: probably better to thrown an exception instead
_log.warning(
raise TemporarySiteError(
f"Loans or reservations can not be retrieved. Site reports: {error_msg}"
)
# _log.warning(
# f"Loans or reservations can not be retrieved. Site reports: {error_msg}"
# )
return loans

# Unfortunately, the branch names are interwoven siblings of the loans,
Expand All @@ -123,7 +126,6 @@ def parse(self) -> list[Loan]:
for child in children:
if child.name == "h2": # we expect this to be the first child
branch_name = child.get_text().strip()
# TODO: check if this resolves to the same https://github.com/myTselection/bibliotheek_be/blob/fec95c3481f78d98062c1117627da652ec8d032d/custom_components/bibliotheek_be/utils.py#L306
elif child.name == "div": # loan div
# we convert child soup object to string, so called function
# can be used also easily for unit tests
Expand All @@ -137,9 +139,6 @@ def parse(self) -> list[Loan]:

def _get_loan_info_from_div(self, loan_div_html: str, branch: str) -> Loan:
"""Return loan from html loan_div blob"""
self._base_url
self._acc_id

loan_div = BeautifulSoup(loan_div_html, "html.parser")
loan = {}

Expand Down Expand Up @@ -292,7 +291,17 @@ def parse(self) -> list[Account]:
"div", class_="my-library-user-library-account-list__account"
)
for acc_div in acc_divs:
# TODO: get details from json object, see https://github.com/myTselection/bibliotheek_be/blob/fec95c3481f78d98062c1117627da652ec8d032d/custom_components/bibliotheek_be/utils.py#L145C53-L145C75
# Some details also available in this json blob; perhaps useful for later
# (credits to see https://github.com/myTselection/bibliotheek_be/blob/fec95c3481f78d98062c1117627da652ec8d032d/custom_components/bibliotheek_be/utils.py#L145C53-L145C75)
# {'id', 'libraryName', 'userName', 'email', 'alertEmailSync', 'barcode'}
# try:
# details = acc_div.find(attrs={":default-active-account": True}).get(
# ":default-active-account"
# )
# details = json.loads(details)
# except (AttributeError, json.JSONDecodeError):
# details = {}

# Get id from <a href="/mijn-bibliotheek/lidmaatschappen/374047">
acc_id = acc_div.a["href"].strip().split("/")[3]

Expand Down Expand Up @@ -384,7 +393,6 @@ def _parse_item_count_from_li(acc_div, class_: str) -> int | None:
class ReservationsPageParser(Parser):
def __init__(self, html: str):
self._html = html
# self._base_url = base_url

def parse(self) -> list[Reservation]:
"""Return list of holds
Expand Down Expand Up @@ -532,7 +540,7 @@ def parse(self) -> list[Reservation]:

class ExtendResponsePageParser(Parser):
def __init__(self, html: str):
self.html = html
self._html = html

def parse(self) -> dict:
"""For dict structure, see the called method"""
Expand All @@ -551,7 +559,7 @@ def find_between(s: str, start: str, end: str):
return s[s.find(start) + len(start) : s.rfind(end)]

# find relevant snippet
soup = BeautifulSoup(self.html, "html.parser")
soup = BeautifulSoup(self._html, "html.parser")
script_txt = soup.find(
"script", string=re.compile("(Statusbericht|Foutmelding)")
).get_text()
Expand Down
5 changes: 3 additions & 2 deletions tests/save_testref.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
"""This script allows to create a reference response set for mijnbibliotheek.
When ran, it create reference files, which can be used in the mijnbibliotheek
tests as expected data. When the files do not exist, the idea is that the
When ran, it create reference files, which can be used in the mijnbibliotheek
tests as expected data. When the files do not exist, the idea is that the
relevant tests will be skipped.
"""

import configparser
import pickle
import sys
Expand Down
Loading

0 comments on commit 2edfd7d

Please sign in to comment.