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.
- pyintradel-0.0.5/.github/check_version.sh +13 -0
- pyintradel-0.0.5/.github/workflows/build.yml +50 -0
- pyintradel-0.0.5/.github/workflows/publish-pypi.yml +37 -0
- pyintradel-0.0.5/.gitignore +131 -0
- pyintradel-0.0.5/PKG-INFO +125 -0
- pyintradel-0.0.5/README.md +101 -0
- {pyIntradel-0.0.3 → pyintradel-0.0.5}/pyintradel/api/__init__.py +2 -5
- pyintradel-0.0.5/pyintradel/api/parser.py +77 -0
- pyintradel-0.0.5/pyintradel/main.py +24 -0
- pyintradel-0.0.5/pyproject.toml +74 -0
- pyintradel-0.0.5/tests/__init__.py +5 -0
- pyintradel-0.0.5/tests/api_test.py +13 -0
- pyintradel-0.0.5/tests/mock.py +1216 -0
- pyIntradel-0.0.3/PKG-INFO +0 -54
- pyIntradel-0.0.3/README.md +0 -37
- pyIntradel-0.0.3/pyIntradel.egg-info/PKG-INFO +0 -54
- pyIntradel-0.0.3/pyIntradel.egg-info/SOURCES.txt +0 -15
- pyIntradel-0.0.3/pyIntradel.egg-info/dependency_links.txt +0 -1
- pyIntradel-0.0.3/pyIntradel.egg-info/not-zip-safe +0 -1
- pyIntradel-0.0.3/pyIntradel.egg-info/requires.txt +0 -2
- pyIntradel-0.0.3/pyIntradel.egg-info/top_level.txt +0 -1
- pyIntradel-0.0.3/pyintradel/api/parser.py +0 -74
- pyIntradel-0.0.3/pyintradel/main.py +0 -22
- pyIntradel-0.0.3/setup.cfg +0 -7
- pyIntradel-0.0.3/setup.py +0 -34
- {pyIntradel-0.0.3 → pyintradel-0.0.5}/LICENSE +0 -0
- {pyIntradel-0.0.3 → pyintradel-0.0.5}/pyintradel/__init__.py +0 -0
- {pyIntradel-0.0.3 → pyintradel-0.0.5}/pyintradel/api/towns.py +0 -0
|
@@ -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
|
+

|
|
28
|
+

|
|
29
|
+

|
|
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
|
+

|
|
4
|
+

|
|
5
|
+

|
|
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,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)
|