pyIntradel 0.0.4__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/
@@ -1,31 +1,26 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyIntradel
3
- Version: 0.0.4
3
+ Version: 0.0.5
4
4
  Summary: Python interface for Intradel
5
- Home-page: https://github.com/thomasgermain/pyintradel.git
6
- Author: Thomas Germain
7
- Author-email: 12560542+thomasgermain@users.noreply.github.com
5
+ Project-URL: Homepage, https://github.com/thomasgermain/pyintradel
6
+ Author-email: Thomas Germain <12560542+thomasgermain@users.noreply.github.com>
8
7
  License: MIT
9
- Classifier: License :: OSI Approved :: MIT License
8
+ License-File: LICENSE
10
9
  Classifier: Development Status :: 4 - Beta
10
+ Classifier: License :: OSI Approved :: MIT License
11
11
  Classifier: Programming Language :: Python
12
- Classifier: Programming Language :: Python :: 3.9
13
- Classifier: Programming Language :: Python :: 3.10
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Programming Language :: Python :: 3.13
14
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'
15
23
  Description-Content-Type: text/markdown
16
- License-File: LICENSE
17
- Requires-Dist: aiohttp<4.0.0,>=3.8.0
18
- Requires-Dist: beautifulsoup4<5.0.0,>=4.11.0
19
- Dynamic: author
20
- Dynamic: author-email
21
- Dynamic: classifier
22
- Dynamic: description
23
- Dynamic: description-content-type
24
- Dynamic: home-page
25
- Dynamic: license
26
- Dynamic: license-file
27
- Dynamic: requires-dist
28
- Dynamic: summary
29
24
 
30
25
  # pyIntradel
31
26
 
@@ -123,7 +118,7 @@ async with aiohttp.ClientSession() as sess:
123
118
 
124
119
  ### Command line
125
120
  ```bash
126
- python3 main.py user passw town
121
+ python3 -m pyintradel.main user passw town
127
122
  ```
128
123
 
129
124
  ---
@@ -94,7 +94,7 @@ async with aiohttp.ClientSession() as sess:
94
94
 
95
95
  ### Command line
96
96
  ```bash
97
- python3 main.py user passw town
97
+ python3 -m pyintradel.main user passw town
98
98
  ```
99
99
 
100
100
  ---
@@ -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
 
@@ -1,26 +1,19 @@
1
- from __future__ import annotations
1
+ from typing import Any
2
2
 
3
- from typing import Any, Dict
4
-
5
- from bs4 import BeautifulSoup
3
+ from bs4 import BeautifulSoup, Tag
6
4
 
7
5
 
8
6
  def parse(response: str) -> list[Any]:
9
7
  """Parse response from intradel"""
10
- results = []
8
+ results: list[Any] = []
11
9
  soup = BeautifulSoup(response, features="html.parser")
12
10
 
13
- if soup.find(attrs={"name": "pLogin"}) is not None:
11
+ if soup.select_one('[name="pLogin"]') is not None:
14
12
  raise ValueError("Wrong response received, login/password seems incorrect", response)
15
13
 
16
- content = (
17
- soup.find(attrs={"class": "grid"})
18
- .find(attrs={"class": "row"})
19
- .findChildren(attrs={"class": "post__content"})
20
- )
21
- for data in content:
22
- result: Dict[str, Any] = {}
23
- if data.find(name="h3") is not None:
14
+ for data in soup.select(".grid .row .post__content"):
15
+ result: dict[str, Any] = {}
16
+ if data.find("h3") is not None:
24
17
  name = _name(data)
25
18
  start_date = _start_date(data)
26
19
  chip_id = _chip_id(data) or name
@@ -37,31 +30,38 @@ def parse(response: str) -> list[Any]:
37
30
  return results
38
31
 
39
32
 
40
- def _name(data: BeautifulSoup) -> str:
41
- return str(data.find(name="h3").text.strip())
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
42
 
43
43
 
44
- def _start_date(data: BeautifulSoup) -> str:
45
- info = data.findAll(name="p")
44
+ def _start_date(data: Tag) -> str:
45
+ info = data.find_all("p")
46
46
  start_date = info[0]
47
47
  if len(info) > 1:
48
48
  start_date = info[3]
49
49
 
50
- return str(start_date.text.split(":")[1].strip())
50
+ return start_date.text.split(":")[1].strip()
51
51
 
52
52
 
53
- def _chip_id(data: BeautifulSoup) -> str | None:
53
+ def _chip_id(data: Tag) -> str | None:
54
54
  chip_id = None
55
- possible_chip_id = data.findChildren(name="p")
55
+ possible_chip_id = data.find_all("p")
56
56
  if len(possible_chip_id) > 1:
57
- chip_id = str(data.findChildren(name="p")[1].text.split(":")[1].strip())
57
+ chip_id = possible_chip_id[1].text.split(":")[1].strip()
58
58
  return chip_id
59
59
 
60
60
 
61
- def _details(data: BeautifulSoup) -> list[Any]:
61
+ def _details(data: Tag) -> list[Any]:
62
62
  attrs = []
63
- for row in data.find(name="tbody").findChildren(name="tr"):
64
- tds = row.findAll(name="td")
63
+ for row in _require_tag(data.find("tbody")).find_all("tr"):
64
+ tds = row.find_all("td")
65
65
  # When the list is empty, the website still includes an empty row.
66
66
  # Let's just skip empty rows, as a valid row needs a date anyway.
67
67
  if tds[0].text:
@@ -69,9 +69,9 @@ def _details(data: BeautifulSoup) -> list[Any]:
69
69
  return attrs
70
70
 
71
71
 
72
- def _total(data: BeautifulSoup) -> str | None:
72
+ def _total(data: Tag) -> str | None:
73
73
  total = None
74
- possible_total = data.find(name="tfoot").findChildren("td")
74
+ possible_total = _require_tag(data.find("tfoot")).find_all("td")
75
75
  if possible_total:
76
- total = str(possible_total[2].text.split(" ")[0].strip())
76
+ total = possible_total[2].text.split(" ")[0].strip()
77
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)