pyIntradel 0.0.5__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.
- {pyintradel-0.0.5 → pyintradel-0.0.7}/PKG-INFO +14 -2
- {pyintradel-0.0.5 → pyintradel-0.0.7}/README.md +12 -0
- pyintradel-0.0.7/pyintradel/api/__init__.py +55 -0
- pyintradel-0.0.7/pyintradel/main.py +46 -0
- {pyintradel-0.0.5 → pyintradel-0.0.7}/pyproject.toml +2 -2
- pyintradel-0.0.7/tests/api_get_data_test.py +60 -0
- pyintradel-0.0.5/pyintradel/api/__init__.py +0 -30
- pyintradel-0.0.5/pyintradel/main.py +0 -24
- {pyintradel-0.0.5 → pyintradel-0.0.7}/.github/check_version.sh +0 -0
- {pyintradel-0.0.5 → pyintradel-0.0.7}/.github/workflows/build.yml +0 -0
- {pyintradel-0.0.5 → pyintradel-0.0.7}/.github/workflows/publish-pypi.yml +0 -0
- {pyintradel-0.0.5 → pyintradel-0.0.7}/.gitignore +0 -0
- {pyintradel-0.0.5 → pyintradel-0.0.7}/LICENSE +0 -0
- {pyintradel-0.0.5 → pyintradel-0.0.7}/pyintradel/__init__.py +0 -0
- {pyintradel-0.0.5 → pyintradel-0.0.7}/pyintradel/api/parser.py +0 -0
- {pyintradel-0.0.5 → pyintradel-0.0.7}/pyintradel/api/towns.py +0 -0
- {pyintradel-0.0.5 → pyintradel-0.0.7}/tests/__init__.py +0 -0
- {pyintradel-0.0.5 → pyintradel-0.0.7}/tests/api_test.py +0 -0
- {pyintradel-0.0.5 → pyintradel-0.0.7}/tests/mock.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pyIntradel
|
|
3
|
-
Version: 0.0.
|
|
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>
|
|
@@ -13,7 +13,7 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
13
13
|
Classifier: Programming Language :: Python :: 3.13
|
|
14
14
|
Classifier: Topic :: Home Automation
|
|
15
15
|
Requires-Python: >=3.12
|
|
16
|
-
Requires-Dist: aiohttp<4.0,>=3.
|
|
16
|
+
Requires-Dist: aiohttp<4.0,>=3.11
|
|
17
17
|
Requires-Dist: beautifulsoup4<5.0,>=4.15
|
|
18
18
|
Provides-Extra: dev
|
|
19
19
|
Requires-Dist: build>=1.5; extra == 'dev'
|
|
@@ -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.
|
|
7
|
+
version = "0.0.7"
|
|
8
8
|
description = "Python interface for Intradel"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "MIT" }
|
|
@@ -13,7 +13,7 @@ authors = [
|
|
|
13
13
|
{ name = "Thomas Germain", email = "12560542+thomasgermain@users.noreply.github.com" },
|
|
14
14
|
]
|
|
15
15
|
dependencies = [
|
|
16
|
-
"aiohttp>=3.
|
|
16
|
+
"aiohttp>=3.11,<4.0",
|
|
17
17
|
"beautifulsoup4>=4.15,<5.0",
|
|
18
18
|
]
|
|
19
19
|
classifiers = [
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|