Skip to content

Commit cd6b75c

Browse files
committed
feat: add holds list command
1 parent e4dec69 commit cd6b75c

22 files changed

Lines changed: 400 additions & 43 deletions
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import typer
2+
from returns.unsafe import unsafe_perform_io
3+
from rich import print
4+
5+
from mediathequeroubaix.config import Config, get_config, User
6+
7+
8+
def _load_config_or_raise() -> Config:
9+
try:
10+
result = get_config()
11+
io = result.unwrap()
12+
config = unsafe_perform_io(io)
13+
except Exception as e:
14+
print("❌ Unable to read configuration!", e)
15+
raise typer.Exit(1) from e
16+
else:
17+
return config
18+
19+
20+
def _get_users_or_raise(config: Config) -> list[User]:
21+
if not config.users:
22+
print("❌ No user defined in configuration")
23+
raise typer.Exit(1)
24+
return config.users

src/mediathequeroubaix/get_holds/__init__.py

Whitespace-only changes.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
from returns.result import Result
2+
3+
from mediathequeroubaix.shared.extract_input_value import extract_input_value
4+
5+
6+
def extract_token(s: str) -> Result[str, Exception]:
7+
return extract_input_value("my_holds", s)
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
from returns.result import Success
2+
3+
from mediathequeroubaix.get_holds.extract_token import extract_token
4+
5+
6+
def test_ok() -> None:
7+
html = """
8+
<div class="panel-body">
9+
<input type="hidden" id="my_holds" value="abc" />
10+
</div>
11+
"""
12+
13+
assert extract_token(html) == Success("abc")
14+
15+
16+
def test_empty_html() -> None:
17+
html = ""
18+
19+
assert str(extract_token(html)) == "<Failure: Token not found>"
20+
21+
22+
def test_empty_token() -> None:
23+
html = """
24+
<div class="panel-body">
25+
<input type="hidden" id="my_holds" value="" />
26+
</div>
27+
"""
28+
29+
assert str(extract_token(html)) == "<Failure: Token not found>"
30+
31+
32+
def test_two_tokens() -> None:
33+
html = """
34+
<div class="panel-body">
35+
<input type="hidden" id="my_holds" value="first" />
36+
<input type="hidden" id="my_holds" value="another" />
37+
</div>
38+
"""
39+
40+
assert extract_token(html) == Success("first")
41+
42+
43+
def test_contain_equal() -> None:
44+
html = """
45+
<div class="panel-body">
46+
<input type="hidden" id="my_holds" value="b64str=" />
47+
</div>
48+
"""
49+
50+
assert extract_token(html) == Success("b64str=")
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
from returns.io import IOResultE
2+
from returns.pipeline import flow
3+
from returns.pointfree import bind_result
4+
from returns.result import safe
5+
6+
from mediathequeroubaix.auth.authenticated_session import AuthenticatedSession
7+
from mediathequeroubaix.get_holds.extract_token import extract_token
8+
from mediathequeroubaix.get_holds.hold import Holds
9+
from mediathequeroubaix.get_holds.parse_holds import parse_holds
10+
from mediathequeroubaix.shared.base64decode import base64decode
11+
from mediathequeroubaix.shared.get_personal_space import get_personal_space
12+
13+
14+
def get_holds(session: AuthenticatedSession) -> IOResultE[Holds]:
15+
return flow(
16+
session,
17+
get_personal_space,
18+
bind_result(extract_token),
19+
bind_result(base64decode),
20+
bind_result(parse_holds),
21+
bind_result(safe(lambda holds: Holds(username=session.username, items=holds))),
22+
)
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import base64
2+
3+
import requests
4+
from requests_mock import mock
5+
from returns.pipeline import is_successful
6+
from returns.unsafe import unsafe_perform_io
7+
8+
from mediathequeroubaix.auth.authenticated_session import AuthenticatedSession, Username
9+
from mediathequeroubaix.get_holds.get_holds import get_holds
10+
11+
12+
def test_ok(requests_mock: mock) -> None:
13+
holds = base64.b64encode(
14+
'[{"hold_id":507643,"title":"Apprendre, si par bonheur","itemnumber":null,"barcode":"","found":null,"cancellationdate":null,"rank":2,"biblionumber":329375,"itemcallnumber":"","reservedate":"2025-07-14","branchcode":"MED","branchname":"M\u00e9diath\u00e8que"}]'.encode()
15+
).decode("utf-8")
16+
requests_mock.get(
17+
"http://www.mediathequederoubaix.fr/espace_personnel",
18+
text=f"""
19+
Welcome
20+
<input type="hidden" id="my_holds" value="{holds}" />
21+
""",
22+
)
23+
24+
result = get_holds(AuthenticatedSession(requests.Session(), Username("John Doe")))
25+
26+
assert is_successful(result)
27+
actual = unsafe_perform_io(result.unwrap())
28+
assert actual.username == "John Doe"
29+
assert len(actual.items) == 1
30+
assert actual.items[0].title == "Apprendre, si par bonheur"
31+
32+
33+
def test_no_token(requests_mock: mock) -> None:
34+
requests_mock.get(
35+
"http://www.mediathequederoubaix.fr/espace_personnel", text="some html"
36+
)
37+
38+
result = get_holds(AuthenticatedSession(requests.Session(), Username("John doe")))
39+
40+
assert is_successful(result) is False
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
from datetime import date, datetime
2+
3+
from pydantic import field_validator
4+
from pydantic.main import BaseModel
5+
6+
from mediathequeroubaix.auth.authenticated_session import Username
7+
8+
9+
class Hold(BaseModel):
10+
# 1234
11+
hold_id: int
12+
# Vampi
13+
title: str
14+
# 1234567
15+
itemnumber: int | None = None
16+
# C2500011705
17+
barcode: str
18+
# 0
19+
rank: int
20+
# E BD/FON
21+
itemcallnumber: str | None = None
22+
# 2025-07-14
23+
reservedate: date | None = None
24+
25+
@field_validator("reservedate", mode="before")
26+
@classmethod
27+
def parse_reservedate(cls, value: str) -> date:
28+
return datetime.strptime(value, "%Y-%m-%d").date()
29+
30+
31+
class Holds(BaseModel):
32+
username: Username
33+
items: list[Hold]
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import datetime
2+
3+
from mediathequeroubaix.get_holds.hold import Hold
4+
5+
6+
def test_convert_due_date_to_date() -> None:
7+
hold = Hold(
8+
hold_id=1234,
9+
title="Test Book",
10+
barcode="c123",
11+
rank=1,
12+
reservedate="2023-04-05",
13+
)
14+
15+
assert hold.reservedate == datetime.date.fromisoformat("2023-04-05")
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
from returns.result import safe
2+
from typing import List
3+
from pydantic import BaseModel, Json
4+
5+
from mediathequeroubaix.get_holds.hold import Hold
6+
7+
8+
class JsonStringWrapper(BaseModel):
9+
json_obj: Json[List[Hold]]
10+
11+
12+
@safe
13+
def parse_holds(s: str) -> list[Hold]:
14+
return JsonStringWrapper(json_obj=s).json_obj
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
from returns.pipeline import is_successful
2+
from returns.result import Success
3+
4+
from mediathequeroubaix.get_holds.hold import Hold
5+
from mediathequeroubaix.get_holds.parse_holds import parse_holds
6+
7+
8+
def test_empty() -> None:
9+
assert is_successful(parse_holds("")) is False
10+
11+
12+
def test_object() -> None:
13+
assert is_successful(parse_holds("{}")) is False
14+
15+
16+
def test_empty_array() -> None:
17+
assert parse_holds("[]") == Success([])
18+
19+
20+
def test_some_holds() -> None:
21+
assert parse_holds(
22+
'[{"hold_id":507643,"title":"Apprendre, si par bonheur","itemnumber":null,"barcode":"","found":null,'
23+
'"cancellationdate":null,"rank":2,"biblionumber":329375,"itemcallnumber":"","reservedate":"2025-07-14",'
24+
'"branchcode":"MED","branchname":"M\u00e9diath\u00e8que"},{"hold_id":507642,"cancellationdate":null,'
25+
'"rank":0,"biblionumber":302152,"found":"W","itemnumber":368084,"barcode":"C1400002454",'
26+
'"title":"L\'espace d\'un an","reservedate":"2025-07-14","itemcallnumber":"SF\/CHA",'
27+
'"branchname":"M\u00e9diath\u00e8que","branchcode":"MED"}]'
28+
) == Success(
29+
[
30+
Hold(
31+
hold_id=507643,
32+
title="Apprendre, si par bonheur",
33+
itemnumber=None,
34+
barcode="",
35+
rank=2,
36+
itemcallnumber="",
37+
reservedate="2025-07-14",
38+
),
39+
Hold(
40+
hold_id=507642,
41+
title="L'espace d'un an",
42+
itemnumber=368084,
43+
barcode="C1400002454",
44+
rank=0,
45+
itemcallnumber="SF/CHA",
46+
reservedate="2025-07-14",
47+
),
48+
]
49+
)

0 commit comments

Comments
 (0)