pyIntradel 0.0.6__tar.gz → 0.0.7__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyIntradel
3
- Version: 0.0.6
3
+ Version: 0.0.7
4
4
  Summary: Python interface for Intradel
5
5
  Project-URL: Homepage, https://github.com/thomasgermain/pyintradel
6
6
  Author-email: Thomas Germain <12560542+thomasgermain@users.noreply.github.com>
@@ -106,6 +106,12 @@ Here is an example of json:
106
106
 
107
107
  The `town` parameter is the name of the town, you can check it here: [towns](pyintradel/api/towns.py)
108
108
 
109
+ The intradel.be login form is now protected by an invisible reCAPTCHA, which can
110
+ reject a plain login/password POST (this notably happens when the browser blocks
111
+ third-party cookies, e.g. in private browsing, but can also affect this library).
112
+ As a workaround, you can authenticate once in a real browser, then reuse the
113
+ resulting session cookie instead of login/password.
114
+
109
115
  ### Python module
110
116
 
111
117
  ```python
@@ -113,12 +119,18 @@ import aiohttp
113
119
  from pyintradel import api
114
120
 
115
121
  async with aiohttp.ClientSession() as sess:
122
+ # Using login/password
116
123
  await api.get_data(sess, login, password, town)
124
+
125
+ # Using a session cookie captured from an already logged-in browser
126
+ await api.get_data(sess, cookie=cookie)
117
127
  ```
118
128
 
119
129
  ### Command line
120
130
  ```bash
121
131
  python3 -m pyintradel.main user passw town
132
+ # or
133
+ python3 -m pyintradel.main --cookie "PHPSESSID=..."
122
134
  ```
123
135
 
124
136
  ---
@@ -82,6 +82,12 @@ Here is an example of json:
82
82
 
83
83
  The `town` parameter is the name of the town, you can check it here: [towns](pyintradel/api/towns.py)
84
84
 
85
+ The intradel.be login form is now protected by an invisible reCAPTCHA, which can
86
+ reject a plain login/password POST (this notably happens when the browser blocks
87
+ third-party cookies, e.g. in private browsing, but can also affect this library).
88
+ As a workaround, you can authenticate once in a real browser, then reuse the
89
+ resulting session cookie instead of login/password.
90
+
85
91
  ### Python module
86
92
 
87
93
  ```python
@@ -89,12 +95,18 @@ import aiohttp
89
95
  from pyintradel import api
90
96
 
91
97
  async with aiohttp.ClientSession() as sess:
98
+ # Using login/password
92
99
  await api.get_data(sess, login, password, town)
100
+
101
+ # Using a session cookie captured from an already logged-in browser
102
+ await api.get_data(sess, cookie=cookie)
93
103
  ```
94
104
 
95
105
  ### Command line
96
106
  ```bash
97
107
  python3 -m pyintradel.main user passw town
108
+ # or
109
+ python3 -m pyintradel.main --cookie "PHPSESSID=..."
98
110
  ```
99
111
 
100
112
  ---
@@ -0,0 +1,55 @@
1
+ """pyIntradel"""
2
+
3
+ import logging
4
+ from typing import Any
5
+
6
+ import aiohttp
7
+
8
+ from . import parser, towns
9
+
10
+ _LOGGER = logging.getLogger(__name__)
11
+ _URL = "https://www.intradel.be/particulier/"
12
+ _HEADERS = {"Content-Type": "application/x-www-form-urlencoded"}
13
+
14
+
15
+ async def get_data(
16
+ session: aiohttp.ClientSession,
17
+ login: str | None = None,
18
+ password: str | None = None,
19
+ town: str | None = None,
20
+ *,
21
+ cookie: str | None = None,
22
+ ) -> list[Any]:
23
+ """Fetch waste collection data.
24
+
25
+ Authenticate either with `login`, `password` and `town`, or with a `cookie`
26
+ (the raw `Cookie` header captured from an already logged-in browser session).
27
+ The website now gates the login form behind an invisible reCAPTCHA, so a plain
28
+ login/password POST can be rejected; the cookie skips the login step entirely.
29
+ """
30
+ if cookie:
31
+ _LOGGER.info("Will query data using a session cookie")
32
+ headers = {"Cookie": cookie}
33
+
34
+ async with session.get(
35
+ "https://www.intradel.be/particulier/data.php", headers=headers
36
+ ) as resp:
37
+ if resp.status != 200:
38
+ raise ValueError(f"Received error {resp.status}", await resp.text())
39
+ return parser.parse(await resp.text())
40
+
41
+ if not login or not password or not town:
42
+ raise ValueError("Either 'cookie' or 'login', 'password' and 'town' must be provided")
43
+
44
+ town_id = towns.TOWNS_MAP.get(town.upper())
45
+ if not town_id:
46
+ raise ValueError("Town not found", town)
47
+
48
+ _LOGGER.info("Will query data for user %s and city %s", login, town)
49
+ data = {"llogin": "YES", "login": login, "pass": password, "commune": town_id}
50
+
51
+ async with session.post(_URL, data=data, headers=_HEADERS) as resp:
52
+ if resp.status != 200:
53
+ raise ValueError(f"Received error {resp.status}", await resp.text())
54
+ else:
55
+ return parser.parse(await resp.text())
@@ -0,0 +1,46 @@
1
+ import argparse
2
+ import asyncio
3
+ import json
4
+ import sys
5
+
6
+ import aiohttp
7
+
8
+ from pyintradel import api
9
+
10
+
11
+ async def _run(
12
+ login: str | None, password: str | None, town: str | None, cookie: str | None
13
+ ) -> None:
14
+ async with aiohttp.ClientSession() as sess:
15
+ data = await api.get_data(sess, login, password, town, cookie=cookie)
16
+ json.dump(data, sys.stdout, indent=2)
17
+
18
+
19
+ def _parse_args(argv: list[str]) -> argparse.Namespace:
20
+ parser = argparse.ArgumentParser(
21
+ prog="pyintradel",
22
+ description="Usage: pyintradel user pass town OR pyintradel --cookie COOKIE",
23
+ )
24
+ parser.add_argument("login", nargs="?", help="Login")
25
+ parser.add_argument("password", nargs="?", help="Password")
26
+ parser.add_argument("town", nargs="?", help="Town, see pyintradel/api/towns.py")
27
+ parser.add_argument(
28
+ "--cookie",
29
+ help="Session cookie captured from an already logged-in browser, "
30
+ "used instead of login/password/town",
31
+ )
32
+ return parser.parse_args(argv)
33
+
34
+
35
+ def main() -> None:
36
+ args = _parse_args(sys.argv[1:])
37
+
38
+ if not args.cookie and not (args.login and args.password and args.town):
39
+ print("Usage: pyintradel user pass town OR pyintradel --cookie COOKIE")
40
+ sys.exit(1)
41
+
42
+ asyncio.run(_run(args.login, args.password, args.town, args.cookie))
43
+
44
+
45
+ if __name__ == "__main__":
46
+ main()
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "pyIntradel"
7
- version = "0.0.6"
7
+ version = "0.0.7"
8
8
  description = "Python interface for Intradel"
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -0,0 +1,60 @@
1
+ import unittest
2
+ from typing import Any
3
+
4
+ from pyintradel import api
5
+ from tests.mock import CORRECT_RESPONSE
6
+
7
+
8
+ class _FakeResponse:
9
+ def __init__(self, status: int, text: str) -> None:
10
+ self.status = status
11
+ self._text = text
12
+
13
+ async def text(self) -> str:
14
+ return self._text
15
+
16
+ async def __aenter__(self) -> "_FakeResponse":
17
+ return self
18
+
19
+ async def __aexit__(self, *exc: Any) -> bool:
20
+ return False
21
+
22
+
23
+ class _FakeSession:
24
+ def __init__(self, response: _FakeResponse) -> None:
25
+ self._response = response
26
+ self.get_calls: list[tuple[str, dict[str, str]]] = []
27
+ self.post_calls: list[tuple[str, dict[str, str], dict[str, str]]] = []
28
+
29
+ def get(self, url: str, headers: dict[str, str]) -> _FakeResponse:
30
+ self.get_calls.append((url, headers))
31
+ return self._response
32
+
33
+ def post(self, url: str, data: dict[str, str], headers: dict[str, str]) -> _FakeResponse:
34
+ self.post_calls.append((url, data, headers))
35
+ return self._response
36
+
37
+
38
+ class GetDataTest(unittest.IsolatedAsyncioTestCase):
39
+ async def test_missing_credentials_and_cookie_raises(self) -> None:
40
+ session = _FakeSession(_FakeResponse(200, CORRECT_RESPONSE))
41
+ with self.assertRaises(ValueError):
42
+ await api.get_data(session) # type: ignore[arg-type]
43
+
44
+ async def test_cookie_uses_get_and_skips_login(self) -> None:
45
+ session = _FakeSession(_FakeResponse(200, CORRECT_RESPONSE))
46
+ result = await api.get_data(session, cookie="PHPSESSID=abc") # type: ignore[arg-type]
47
+
48
+ self.assertEqual(len(result), 3)
49
+ self.assertEqual(len(session.get_calls), 1)
50
+ self.assertEqual(len(session.post_calls), 0)
51
+ _, headers = session.get_calls[0]
52
+ self.assertEqual(headers["Cookie"], "PHPSESSID=abc")
53
+
54
+ async def test_login_uses_post(self) -> None:
55
+ session = _FakeSession(_FakeResponse(200, CORRECT_RESPONSE))
56
+ result = await api.get_data(session, "user", "pass", "LIEGE") # type: ignore[arg-type]
57
+
58
+ self.assertEqual(len(result), 3)
59
+ self.assertEqual(len(session.post_calls), 1)
60
+ self.assertEqual(len(session.get_calls), 0)
@@ -1,30 +0,0 @@
1
- """pyIntradel"""
2
-
3
- import logging
4
- from typing import Any
5
-
6
- import aiohttp
7
-
8
- from . import parser, towns
9
-
10
- _LOGGER = logging.getLogger(__name__)
11
- _URL = "https://www.intradel.be/particulier/"
12
- _HEADERS = {"Content-Type": "application/x-www-form-urlencoded"}
13
-
14
-
15
- async def get_data(
16
- session: aiohttp.ClientSession, login: str, password: str, town: str
17
- ) -> list[Any]:
18
- _LOGGER.info("Will query data for user %s and city %s", login, town)
19
-
20
- town_id = towns.TOWNS_MAP.get(town.upper())
21
- if not town_id:
22
- raise ValueError("Town not found", town)
23
-
24
- data = {"llogin": "YES", "login": login, "pass": password, "commune": town_id}
25
-
26
- async with session.post(_URL, data=data, headers=_HEADERS) as resp:
27
- if resp.status != 200:
28
- raise ValueError(f"Received error {resp.status}", await resp.text())
29
- else:
30
- return parser.parse(await resp.text())
@@ -1,24 +0,0 @@
1
- import asyncio
2
- import json
3
- import sys
4
-
5
- import aiohttp
6
-
7
- from pyintradel import api
8
-
9
-
10
- async def _run(login: str, password: str, town: str) -> None:
11
- async with aiohttp.ClientSession() as sess:
12
- json.dump(await api.get_data(sess, login, password, town), sys.stdout, indent=2)
13
-
14
-
15
- def main() -> None:
16
- if len(sys.argv) != 4:
17
- print("Usage: pyintradel user pass town")
18
- sys.exit(0)
19
-
20
- asyncio.run(_run(sys.argv[1], sys.argv[2], sys.argv[3]))
21
-
22
-
23
- if __name__ == "__main__":
24
- main()
File without changes
File without changes
File without changes
File without changes
File without changes