pyIntradel 0.0.3__tar.gz → 0.0.5__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.
@@ -0,0 +1,13 @@
1
+ #!/bin/bash
2
+
3
+ version=$(python3 -c "import tomllib; print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])")
4
+ result=$(curl -s https://pypi.org/pypi/pyIntradel/json | jq -r '.releases | keys[]' | grep -w "$version")
5
+
6
+ if [ -z "$result" ]
7
+ then
8
+ echo Version "$version" not found in pypi, all good
9
+ exit 0
10
+ else
11
+ echo Version "$version" already exist in pypi
12
+ exit 1
13
+ fi
@@ -0,0 +1,50 @@
1
+ # This workflow will install Python dependencies, run tests and lint with a variety of Python versions
2
+ # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
3
+
4
+ name: Build
5
+
6
+ on:
7
+ push:
8
+ pull_request:
9
+
10
+ jobs:
11
+ build:
12
+
13
+ runs-on: ubuntu-latest
14
+ strategy:
15
+ fail-fast: false
16
+ matrix:
17
+ python-version: ['3.12', '3.13']
18
+
19
+ steps:
20
+ - uses: actions/checkout@v4
21
+
22
+ - name: Set up Python ${{ matrix.python-version }}
23
+ uses: actions/setup-python@v5
24
+ with:
25
+ python-version: ${{ matrix.python-version }}
26
+
27
+ - name: Install dependencies
28
+ run: |
29
+ python -m pip install --upgrade pip
30
+ pip install -e .[dev]
31
+
32
+ - name: Lint with ruff
33
+ run: ruff check pyintradel tests
34
+ if: ${{ always() }}
35
+
36
+ - name: Check formatting with ruff
37
+ run: ruff format --check pyintradel tests
38
+ if: ${{ always() }}
39
+
40
+ - name: Type check with mypy
41
+ run: mypy pyintradel tests
42
+ if: ${{ always() }}
43
+
44
+ - name: Run tests
45
+ run: pytest
46
+ if: ${{ always() }}
47
+
48
+ - name: Check new pypi version is correct
49
+ run: (chmod +x .github/check_version.sh && sh .github/check_version.sh)
50
+ if: ${{ always() }}
@@ -0,0 +1,37 @@
1
+ # This workflow will upload a Python Package using Twine when a release is created
2
+ # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries
3
+
4
+ # This workflow uses actions that are not certified by GitHub.
5
+ # They are provided by a third-party and are governed by
6
+ # separate terms of service, privacy policy, and support
7
+ # documentation.
8
+
9
+ name: Publish to pypi
10
+
11
+ on:
12
+ release:
13
+ types: [published]
14
+
15
+ jobs:
16
+ deploy:
17
+
18
+ runs-on: ubuntu-latest
19
+ environment: pypi
20
+ permissions:
21
+ # Required for PyPI trusted publishing (OIDC). No token/secret needed.
22
+ id-token: write
23
+
24
+ steps:
25
+ - uses: actions/checkout@v4
26
+ - name: Set up Python
27
+ uses: actions/setup-python@v5
28
+ with:
29
+ python-version: '3.x'
30
+ - name: Install dependencies
31
+ run: |
32
+ python -m pip install --upgrade pip
33
+ pip install build
34
+ - name: Build a binary wheel and a source tarball
35
+ run: python -m build --sdist --wheel --outdir dist/
36
+ - name: Publish package
37
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,131 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ pip-wheel-metadata/
24
+ share/python-wheels/
25
+ *.egg-info/
26
+ .installed.cfg
27
+ *.egg
28
+ MANIFEST
29
+
30
+ # PyInstaller
31
+ # Usually these files are written by a python script from a template
32
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
33
+ *.manifest
34
+ *.spec
35
+
36
+ # Installer logs
37
+ pip-log.txt
38
+ pip-delete-this-directory.txt
39
+
40
+ # Unit test / coverage reports
41
+ htmlcov/
42
+ .tox/
43
+ .nox/
44
+ .coverage
45
+ .coverage.*
46
+ .cache
47
+ nosetests.xml
48
+ coverage.xml
49
+ *.cover
50
+ *.py,cover
51
+ .hypothesis/
52
+ .pytest_cache/
53
+
54
+ # Translations
55
+ *.mo
56
+ *.pot
57
+
58
+ # Django stuff:
59
+ *.log
60
+ local_settings.py
61
+ db.sqlite3
62
+ db.sqlite3-journal
63
+
64
+ # Flask stuff:
65
+ instance/
66
+ .webassets-cache
67
+
68
+ # Scrapy stuff:
69
+ .scrapy
70
+
71
+ # Sphinx documentation
72
+ docs/_build/
73
+
74
+ # PyBuilder
75
+ target/
76
+
77
+ # Jupyter Notebook
78
+ .ipynb_checkpoints
79
+
80
+ # IPython
81
+ profile_default/
82
+ ipython_config.py
83
+
84
+ # pyenv
85
+ .python-version
86
+
87
+ # pipenv
88
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
89
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
90
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
91
+ # install all needed dependencies.
92
+ #Pipfile.lock
93
+
94
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow
95
+ __pypackages__/
96
+
97
+ # Celery stuff
98
+ celerybeat-schedule
99
+ celerybeat.pid
100
+
101
+ # SageMath parsed files
102
+ *.sage.py
103
+
104
+ # Environments
105
+ .env
106
+ .venv
107
+ env/
108
+ venv/
109
+ ENV/
110
+ env.bak/
111
+ venv.bak/
112
+
113
+ # Spyder project settings
114
+ .spyderproject
115
+ .spyproject
116
+
117
+ # Rope project settings
118
+ .ropeproject
119
+
120
+ # mkdocs documentation
121
+ /site
122
+
123
+ # mypy
124
+ .mypy_cache/
125
+ .dmypy.json
126
+ dmypy.json
127
+
128
+ # Pyre type checker
129
+ .pyre/
130
+
131
+ .idea/
@@ -0,0 +1,125 @@
1
+ Metadata-Version: 2.4
2
+ Name: pyIntradel
3
+ Version: 0.0.5
4
+ Summary: Python interface for Intradel
5
+ Project-URL: Homepage, https://github.com/thomasgermain/pyintradel
6
+ Author-email: Thomas Germain <12560542+thomasgermain@users.noreply.github.com>
7
+ License: MIT
8
+ License-File: LICENSE
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Programming Language :: Python
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Programming Language :: Python :: 3.13
14
+ Classifier: Topic :: Home Automation
15
+ Requires-Python: >=3.12
16
+ Requires-Dist: aiohttp<4.0,>=3.14
17
+ Requires-Dist: beautifulsoup4<5.0,>=4.15
18
+ Provides-Extra: dev
19
+ Requires-Dist: build>=1.5; extra == 'dev'
20
+ Requires-Dist: mypy>=2.1; extra == 'dev'
21
+ Requires-Dist: pytest>=9.0; extra == 'dev'
22
+ Requires-Dist: ruff>=0.15; extra == 'dev'
23
+ Description-Content-Type: text/markdown
24
+
25
+ # pyIntradel
26
+
27
+ ![PyPI - License](https://img.shields.io/github/license/thomasgermain/pyIntradel)
28
+ ![PyPI](https://img.shields.io/pypi/v/pyIntradel)
29
+ ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/pyIntradel.svg)
30
+
31
+ A python connector for waste collection for province of Liège. This connector is using screen scraping to collect
32
+ following data (for the current year), in json:
33
+ - "Green bin" (organic waste) and "black bin" residual waste
34
+ - Total weight
35
+ - Number of collections
36
+ - details of all the collections
37
+ - chip number
38
+ - starting date (01/01 of the current year)
39
+ - Recypark
40
+ - details of all the visits
41
+ - (01/01 of the current year)
42
+
43
+ Here is an example of json:
44
+
45
+ ```json
46
+ [
47
+ {
48
+ "name": "ORGANIQUE",
49
+ "start_date": "01-01-2022",
50
+ "id": "123456",
51
+ "details":
52
+ [
53
+ {
54
+ "date": "20-01-2022",
55
+ "detail": "34.0"
56
+ },
57
+ {
58
+ "date": "17-02-2022",
59
+ "detail": "27.0"
60
+ },
61
+ {
62
+ "date": "07-04-2022",
63
+ "detail": "36.0"
64
+ }
65
+ ],
66
+ "total": "97"
67
+ },
68
+ {
69
+ "name": "RESIDUEL",
70
+ "start_date": "01-01-2022",
71
+ "id": "78810",
72
+ "details":
73
+ [
74
+ {
75
+ "date": "20-01-2022",
76
+ "detail": "14.5"
77
+ },
78
+ {
79
+ "date": "07-04-2022",
80
+ "detail": "11.5"
81
+ },
82
+ {
83
+ "date": "21-04-2022",
84
+ "detail": "11.5"
85
+ }
86
+ ],
87
+ "total": "37.5"
88
+ },
89
+ {
90
+ "name": "RECYPARC",
91
+ "start_date": "01-01-2022",
92
+ "id": "RECYPARC",
93
+ "details":
94
+ [
95
+ {
96
+ "date": "14-04-2022",
97
+ "detail": "Encombrants (0.35 m³), Petits Bruns (0.00 pièce)"
98
+ }
99
+ ],
100
+ "total": "1"
101
+ }
102
+ ]
103
+ ```
104
+
105
+ ## Usage
106
+
107
+ The `town` parameter is the name of the town, you can check it here: [towns](pyintradel/api/towns.py)
108
+
109
+ ### Python module
110
+
111
+ ```python
112
+ import aiohttp
113
+ from pyintradel import api
114
+
115
+ async with aiohttp.ClientSession() as sess:
116
+ await api.get_data(sess, login, password, town)
117
+ ```
118
+
119
+ ### Command line
120
+ ```bash
121
+ python3 -m pyintradel.main user passw town
122
+ ```
123
+
124
+ ---
125
+ <a href="https://www.buymeacoffee.com/tgermain" target="_blank"><img src="https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png" alt="Buy Me A Coffee" style="height: auto !important;width: auto !important;" ></a>
@@ -0,0 +1,101 @@
1
+ # pyIntradel
2
+
3
+ ![PyPI - License](https://img.shields.io/github/license/thomasgermain/pyIntradel)
4
+ ![PyPI](https://img.shields.io/pypi/v/pyIntradel)
5
+ ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/pyIntradel.svg)
6
+
7
+ A python connector for waste collection for province of Liège. This connector is using screen scraping to collect
8
+ following data (for the current year), in json:
9
+ - "Green bin" (organic waste) and "black bin" residual waste
10
+ - Total weight
11
+ - Number of collections
12
+ - details of all the collections
13
+ - chip number
14
+ - starting date (01/01 of the current year)
15
+ - Recypark
16
+ - details of all the visits
17
+ - (01/01 of the current year)
18
+
19
+ Here is an example of json:
20
+
21
+ ```json
22
+ [
23
+ {
24
+ "name": "ORGANIQUE",
25
+ "start_date": "01-01-2022",
26
+ "id": "123456",
27
+ "details":
28
+ [
29
+ {
30
+ "date": "20-01-2022",
31
+ "detail": "34.0"
32
+ },
33
+ {
34
+ "date": "17-02-2022",
35
+ "detail": "27.0"
36
+ },
37
+ {
38
+ "date": "07-04-2022",
39
+ "detail": "36.0"
40
+ }
41
+ ],
42
+ "total": "97"
43
+ },
44
+ {
45
+ "name": "RESIDUEL",
46
+ "start_date": "01-01-2022",
47
+ "id": "78810",
48
+ "details":
49
+ [
50
+ {
51
+ "date": "20-01-2022",
52
+ "detail": "14.5"
53
+ },
54
+ {
55
+ "date": "07-04-2022",
56
+ "detail": "11.5"
57
+ },
58
+ {
59
+ "date": "21-04-2022",
60
+ "detail": "11.5"
61
+ }
62
+ ],
63
+ "total": "37.5"
64
+ },
65
+ {
66
+ "name": "RECYPARC",
67
+ "start_date": "01-01-2022",
68
+ "id": "RECYPARC",
69
+ "details":
70
+ [
71
+ {
72
+ "date": "14-04-2022",
73
+ "detail": "Encombrants (0.35 m³), Petits Bruns (0.00 pièce)"
74
+ }
75
+ ],
76
+ "total": "1"
77
+ }
78
+ ]
79
+ ```
80
+
81
+ ## Usage
82
+
83
+ The `town` parameter is the name of the town, you can check it here: [towns](pyintradel/api/towns.py)
84
+
85
+ ### Python module
86
+
87
+ ```python
88
+ import aiohttp
89
+ from pyintradel import api
90
+
91
+ async with aiohttp.ClientSession() as sess:
92
+ await api.get_data(sess, login, password, town)
93
+ ```
94
+
95
+ ### Command line
96
+ ```bash
97
+ python3 -m pyintradel.main user passw town
98
+ ```
99
+
100
+ ---
101
+ <a href="https://www.buymeacoffee.com/tgermain" target="_blank"><img src="https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png" alt="Buy Me A Coffee" style="height: auto !important;width: auto !important;" ></a>
@@ -5,10 +5,7 @@ from typing import Any
5
5
 
6
6
  import aiohttp
7
7
 
8
- from . import parser
9
- from . import towns
10
-
11
- logging.basicConfig(level=logging.INFO, format="%(asctime)s:%(levelname)s:%(name)s: %(message)s")
8
+ from . import parser, towns
12
9
 
13
10
  _LOGGER = logging.getLogger(__name__)
14
11
  _URL = "https://www.intradel.be/particulier/"
@@ -22,7 +19,7 @@ async def get_data(
22
19
 
23
20
  town_id = towns.TOWNS_MAP.get(town.upper())
24
21
  if not town_id:
25
- ValueError("Town not found", town)
22
+ raise ValueError("Town not found", town)
26
23
 
27
24
  data = {"llogin": "YES", "login": login, "pass": password, "commune": town_id}
28
25
 
@@ -0,0 +1,77 @@
1
+ from typing import Any
2
+
3
+ from bs4 import BeautifulSoup, Tag
4
+
5
+
6
+ def parse(response: str) -> list[Any]:
7
+ """Parse response from intradel"""
8
+ results: list[Any] = []
9
+ soup = BeautifulSoup(response, features="html.parser")
10
+
11
+ if soup.select_one('[name="pLogin"]') is not None:
12
+ raise ValueError("Wrong response received, login/password seems incorrect", response)
13
+
14
+ for data in soup.select(".grid .row .post__content"):
15
+ result: dict[str, Any] = {}
16
+ if data.find("h3") is not None:
17
+ name = _name(data)
18
+ start_date = _start_date(data)
19
+ chip_id = _chip_id(data) or name
20
+ details = _details(data)
21
+ total = _total(data) or str(len(details))
22
+
23
+ result.update({"name": name})
24
+ result.update({"start_date": start_date})
25
+ result.update({"id": chip_id})
26
+ result.update({"details": details})
27
+ result.update({"total": total})
28
+ results.append(result)
29
+
30
+ return results
31
+
32
+
33
+ def _require_tag(node: object) -> Tag:
34
+ """Narrow a bs4 lookup result to a Tag, failing loudly on unexpected markup."""
35
+ if not isinstance(node, Tag):
36
+ raise ValueError("Unexpected response structure from intradel")
37
+ return node
38
+
39
+
40
+ def _name(data: Tag) -> str:
41
+ return _require_tag(data.find("h3")).text.strip()
42
+
43
+
44
+ def _start_date(data: Tag) -> str:
45
+ info = data.find_all("p")
46
+ start_date = info[0]
47
+ if len(info) > 1:
48
+ start_date = info[3]
49
+
50
+ return start_date.text.split(":")[1].strip()
51
+
52
+
53
+ def _chip_id(data: Tag) -> str | None:
54
+ chip_id = None
55
+ possible_chip_id = data.find_all("p")
56
+ if len(possible_chip_id) > 1:
57
+ chip_id = possible_chip_id[1].text.split(":")[1].strip()
58
+ return chip_id
59
+
60
+
61
+ def _details(data: Tag) -> list[Any]:
62
+ attrs = []
63
+ for row in _require_tag(data.find("tbody")).find_all("tr"):
64
+ tds = row.find_all("td")
65
+ # When the list is empty, the website still includes an empty row.
66
+ # Let's just skip empty rows, as a valid row needs a date anyway.
67
+ if tds[0].text:
68
+ attrs.append({"date": tds[0].text, "detail": tds[2].text})
69
+ return attrs
70
+
71
+
72
+ def _total(data: Tag) -> str | None:
73
+ total = None
74
+ possible_total = _require_tag(data.find("tfoot")).find_all("td")
75
+ if possible_total:
76
+ total = possible_total[2].text.split(" ")[0].strip()
77
+ return total
@@ -0,0 +1,24 @@
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()
@@ -0,0 +1,74 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "pyIntradel"
7
+ version = "0.0.5"
8
+ description = "Python interface for Intradel"
9
+ readme = "README.md"
10
+ license = { text = "MIT" }
11
+ requires-python = ">=3.12"
12
+ authors = [
13
+ { name = "Thomas Germain", email = "12560542+thomasgermain@users.noreply.github.com" },
14
+ ]
15
+ dependencies = [
16
+ "aiohttp>=3.14,<4.0",
17
+ "beautifulsoup4>=4.15,<5.0",
18
+ ]
19
+ classifiers = [
20
+ "License :: OSI Approved :: MIT License",
21
+ "Development Status :: 4 - Beta",
22
+ "Programming Language :: Python",
23
+ "Programming Language :: Python :: 3.12",
24
+ "Programming Language :: Python :: 3.13",
25
+ "Topic :: Home Automation",
26
+ ]
27
+
28
+ [project.urls]
29
+ Homepage = "https://github.com/thomasgermain/pyintradel"
30
+
31
+ [project.optional-dependencies]
32
+ dev = [
33
+ "ruff>=0.15",
34
+ "mypy>=2.1",
35
+ "pytest>=9.0",
36
+ "build>=1.5",
37
+ ]
38
+
39
+ [project.scripts]
40
+ pyintradel = "pyintradel.main:main"
41
+
42
+ [tool.hatch.build.targets.wheel]
43
+ packages = ["pyintradel"]
44
+
45
+ [tool.ruff]
46
+ line-length = 100
47
+ target-version = "py312"
48
+ # Large scraped HTML fixture, kept verbatim for parser tests.
49
+ extend-exclude = ["tests/mock.py"]
50
+
51
+ [tool.ruff.lint]
52
+ select = [
53
+ "E", # pycodestyle errors
54
+ "W", # pycodestyle warnings
55
+ "F", # pyflakes
56
+ "I", # isort
57
+ "B", # flake8-bugbear
58
+ "C4", # flake8-comprehensions
59
+ "ERA", # eradicate (commented-out code)
60
+ "UP", # pyupgrade
61
+ ]
62
+
63
+ [tool.mypy]
64
+ strict = true
65
+ ignore_missing_imports = true
66
+ show_error_context = true
67
+ show_column_numbers = true
68
+
69
+ [[tool.mypy.overrides]]
70
+ module = "pyintradel.*"
71
+ implicit_reexport = true
72
+
73
+ [tool.pytest.ini_options]
74
+ testpaths = ["tests"]
@@ -0,0 +1,5 @@
1
+ """pyIntradel tests."""
2
+
3
+ import logging
4
+
5
+ logging.basicConfig(level=logging.ERROR, format="%(asctime)s:%(levelname)s:%(name)s: %(message)s")
@@ -0,0 +1,13 @@
1
+ import unittest
2
+
3
+ from pyintradel.api.parser import parse
4
+ from tests.mock import CORRECT_RESPONSE, INCORRECT_LOGIN
5
+
6
+
7
+ class ParsingTest(unittest.TestCase):
8
+ def test_parsing_correct(self) -> None:
9
+ result = parse(CORRECT_RESPONSE)
10
+ self.assertEqual(len(result), 3)
11
+
12
+ def test_parsing_login_error(self) -> None:
13
+ self.assertRaises(ValueError, parse, INCORRECT_LOGIN)