Hydro-Quebec-API-Wrapper 4.2.6__tar.gz → 4.3.1__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.
- hydro_quebec_api_wrapper-4.3.1/.github/scripts/home_assistant_versions.py +49 -0
- hydro_quebec_api_wrapper-4.3.1/.github/workflows/ci.yml +121 -0
- hydro_quebec_api_wrapper-4.3.1/.github/workflows/release.yml +179 -0
- {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.1}/.pre-commit-config.yaml +6 -6
- {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.1/Hydro_Quebec_API_Wrapper.egg-info}/PKG-INFO +58 -6
- {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.1}/Hydro_Quebec_API_Wrapper.egg-info/SOURCES.txt +8 -1
- {hydro_quebec_api_wrapper-4.2.6/Hydro_Quebec_API_Wrapper.egg-info → hydro_quebec_api_wrapper-4.3.1}/PKG-INFO +58 -6
- {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.1}/README.md +55 -3
- {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.1}/docs/source/reference/hydroqc.hydro_api.rst +8 -0
- {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.1}/examples/hydro.py +21 -1
- hydro_quebec_api_wrapper-4.3.1/hydroqc/consts.py +28 -0
- hydro_quebec_api_wrapper-4.3.1/hydroqc/error.py +48 -0
- {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.1}/hydroqc/hydro_api/client.py +604 -130
- hydro_quebec_api_wrapper-4.3.1/hydroqc/hydro_api/mfa.py +762 -0
- {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.1}/hydroqc/webuser.py +65 -2
- {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.1}/pyproject.toml +2 -2
- hydro_quebec_api_wrapper-4.3.1/test_requirements.txt +15 -0
- {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.1}/tests/subtest_common.py +3 -2
- hydro_quebec_api_wrapper-4.3.1/tests/unit/__init__.py +1 -0
- hydro_quebec_api_wrapper-4.3.1/tests/unit/test_login_flow.py +396 -0
- hydro_quebec_api_wrapper-4.3.1/tests/unit/test_mfa.py +449 -0
- hydro_quebec_api_wrapper-4.2.6/hydroqc/consts.py +0 -8
- hydro_quebec_api_wrapper-4.2.6/hydroqc/error.py +0 -27
- hydro_quebec_api_wrapper-4.2.6/test_requirements.txt +0 -15
- {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.1}/.coveragerc +0 -0
- {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.1}/.devcontainer/Dockerfile +0 -0
- {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.1}/.devcontainer/devcontainer.json +0 -0
- {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.1}/.flake8 +0 -0
- {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.1}/.gitignore +0 -0
- {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.1}/.gitlab-ci.yml +0 -0
- {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.1}/.pylintrc +0 -0
- {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.1}/Hydro_Quebec_API_Wrapper.egg-info/dependency_links.txt +0 -0
- {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.1}/Hydro_Quebec_API_Wrapper.egg-info/entry_points.txt +0 -0
- {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.1}/Hydro_Quebec_API_Wrapper.egg-info/requires.txt +0 -0
- {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.1}/Hydro_Quebec_API_Wrapper.egg-info/top_level.txt +0 -0
- {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.1}/LICENSE +0 -0
- {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.1}/MANIFEST.in +0 -0
- {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.1}/config.default.yaml +0 -0
- {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.1}/conflict_test_requirements.txt +0 -0
- {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.1}/docs/Makefile +0 -0
- {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.1}/docs/make.bat +0 -0
- {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.1}/docs/source/_static/custom.css +0 -0
- {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.1}/docs/source/_static/hydroqc-logo-128.png +0 -0
- {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.1}/docs/source/_static/hydroqc-logo.svg +0 -0
- {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.1}/docs/source/conf.py +0 -0
- {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.1}/docs/source/external/description-des-codes-interruption.pdf +0 -0
- {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.1}/docs/source/index.rst +0 -0
- {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.1}/docs/source/reference/hydroqc.contract.rst +0 -0
- {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.1}/docs/source/reference/hydroqc.peak.cpc.rst +0 -0
- {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.1}/docs/source/reference/hydroqc.peak.dpc.rst +0 -0
- {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.1}/docs/source/reference/hydroqc.peak.rst +0 -0
- {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.1}/docs/source/reference/hydroqc.rst +0 -0
- {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.1}/docs/source/reference/hydroqc.types.rst +0 -0
- {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.1}/docs/source/reference/modules.rst +0 -0
- {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.1}/docs/source/todo.rst +0 -0
- {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.1}/hydroqc/__init__.py +0 -0
- {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.1}/hydroqc/account.py +0 -0
- {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.1}/hydroqc/contract/__init__.py +0 -0
- {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.1}/hydroqc/contract/common.py +0 -0
- {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.1}/hydroqc/contract/contract_d.py +0 -0
- {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.1}/hydroqc/contract/contract_d_cpc.py +0 -0
- {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.1}/hydroqc/contract/contract_dpc.py +0 -0
- {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.1}/hydroqc/contract/contract_dt.py +0 -0
- {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.1}/hydroqc/contract/contract_m.py +0 -0
- {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.1}/hydroqc/contract/contract_m_gdp.py +0 -0
- {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.1}/hydroqc/contract/contract_residential.py +0 -0
- {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.1}/hydroqc/contract/outage.py +0 -0
- {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.1}/hydroqc/customer.py +0 -0
- {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.1}/hydroqc/diagnostic.py +0 -0
- {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.1}/hydroqc/hydro_api/__init__.py +0 -0
- {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.1}/hydroqc/hydro_api/cache.py +0 -0
- {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.1}/hydroqc/hydro_api/consts.py +0 -0
- {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.1}/hydroqc/logger.py +0 -0
- {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.1}/hydroqc/peak/__init__.py +0 -0
- {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.1}/hydroqc/peak/basepeak.py +0 -0
- {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.1}/hydroqc/peak/basepeakhandler.py +0 -0
- {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.1}/hydroqc/peak/consts.py +0 -0
- {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.1}/hydroqc/peak/cpc/__init__.py +0 -0
- {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.1}/hydroqc/peak/cpc/consts.py +0 -0
- {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.1}/hydroqc/peak/cpc/handler.py +0 -0
- {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.1}/hydroqc/peak/cpc/peak.py +0 -0
- {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.1}/hydroqc/peak/dpc/__init__.py +0 -0
- {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.1}/hydroqc/peak/dpc/consts.py +0 -0
- {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.1}/hydroqc/peak/dpc/handler.py +0 -0
- {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.1}/hydroqc/peak/dpc/peak.py +0 -0
- {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.1}/hydroqc/public_client.py +0 -0
- {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.1}/hydroqc/py.typed +0 -0
- {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.1}/hydroqc/timerange.py +0 -0
- {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.1}/hydroqc/types/__init__.py +0 -0
- {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.1}/hydroqc/types/account.py +0 -0
- {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.1}/hydroqc/types/common.py +0 -0
- {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.1}/hydroqc/types/consump.py +0 -0
- {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.1}/hydroqc/types/contract.py +0 -0
- {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.1}/hydroqc/types/cpc.py +0 -0
- {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.1}/hydroqc/types/dpc.py +0 -0
- {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.1}/hydroqc/types/dt.py +0 -0
- {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.1}/hydroqc/types/outage.py +0 -0
- {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.1}/hydroqc/utils.py +0 -0
- {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.1}/logo/hydroqc-logo-128.png +0 -0
- {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.1}/logo/hydroqc-logo.svg +0 -0
- {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.1}/renovate.json5 +0 -0
- {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.1}/setup.cfg +0 -0
- {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.1}/tests/__init__.py +0 -0
- {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.1}/tests/conftest.py +0 -0
- {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.1}/tests/subtest_contract_d.py +0 -0
- {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.1}/tests/subtest_contract_d_cpc.py +0 -0
- {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.1}/tests/subtest_contract_dpc.py +0 -0
- {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.1}/tests/subtest_contract_dt.py +0 -0
- {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.1}/tests/subtest_contract_m.py +0 -0
- {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.1}/tests/subtest_contract_m_gdp.py +0 -0
- {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.1}/tests/test_basic.py +0 -0
- {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.1}/tests/test_timerange.py +0 -0
- {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.1}/tests/tools.py +0 -0
- {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.1}/tox.ini +0 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Select the latest Home Assistant release and a compatible runner Python."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
from urllib.request import urlopen
|
|
6
|
+
|
|
7
|
+
from packaging.specifiers import SpecifierSet
|
|
8
|
+
from packaging.version import Version
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def load_json(url):
|
|
12
|
+
"""Fetch JSON metadata from a public endpoint."""
|
|
13
|
+
with urlopen(url, timeout=30) as response:
|
|
14
|
+
return json.load(response)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
home_assistant = load_json("https://pypi.org/pypi/homeassistant/json")["info"]
|
|
18
|
+
home_assistant_version = home_assistant["version"]
|
|
19
|
+
if Version(home_assistant_version).is_prerelease:
|
|
20
|
+
raise SystemExit("The latest Home Assistant release is not stable")
|
|
21
|
+
|
|
22
|
+
requires_python = SpecifierSet(home_assistant["requires_python"])
|
|
23
|
+
manifest = load_json(
|
|
24
|
+
"https://raw.githubusercontent.com/actions/python-versions/main/versions-manifest.json"
|
|
25
|
+
)
|
|
26
|
+
available = [
|
|
27
|
+
Version(entry["version"])
|
|
28
|
+
for entry in manifest
|
|
29
|
+
if entry["stable"]
|
|
30
|
+
and Version(entry["version"]) in requires_python
|
|
31
|
+
and any(
|
|
32
|
+
file["platform"] == "linux"
|
|
33
|
+
and file["arch"] == "x64"
|
|
34
|
+
and file.get("platform_version") == "24.04"
|
|
35
|
+
for file in entry["files"]
|
|
36
|
+
)
|
|
37
|
+
]
|
|
38
|
+
if not available:
|
|
39
|
+
raise SystemExit(f"No runner Python version satisfies {requires_python}")
|
|
40
|
+
|
|
41
|
+
oldest_minor = min((version.major, version.minor) for version in available)
|
|
42
|
+
python_version = str(
|
|
43
|
+
max(version for version in available if version.release[:2] == oldest_minor)
|
|
44
|
+
)
|
|
45
|
+
print(f"Home Assistant {home_assistant_version} requires Python {requires_python}")
|
|
46
|
+
print(f"Selected Python {python_version}")
|
|
47
|
+
with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as output:
|
|
48
|
+
print(f"home_assistant={home_assistant_version}", file=output)
|
|
49
|
+
print(f"python={python_version}", file=output)
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
workflow_dispatch:
|
|
9
|
+
|
|
10
|
+
permissions:
|
|
11
|
+
contents: read
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
quality:
|
|
15
|
+
name: Quality
|
|
16
|
+
runs-on: ubuntu-24.04
|
|
17
|
+
steps:
|
|
18
|
+
- uses: actions/checkout@v4
|
|
19
|
+
with:
|
|
20
|
+
fetch-depth: 0
|
|
21
|
+
- uses: actions/setup-python@v5
|
|
22
|
+
with:
|
|
23
|
+
python-version: '3.13'
|
|
24
|
+
cache: pip
|
|
25
|
+
cache-dependency-path: test_requirements.txt
|
|
26
|
+
- name: Install system dependencies
|
|
27
|
+
run: sudo apt-get update && sudo apt-get install -y build-essential graphviz
|
|
28
|
+
- name: Install tox
|
|
29
|
+
run: python -m pip install tox
|
|
30
|
+
- name: Run quality checks
|
|
31
|
+
run: tox -e pylint,flake8,pydocstyle,typing,black --parallel auto
|
|
32
|
+
- name: Upload quality reports
|
|
33
|
+
if: always()
|
|
34
|
+
uses: actions/upload-artifact@v4
|
|
35
|
+
with:
|
|
36
|
+
name: quality-reports
|
|
37
|
+
if-no-files-found: ignore
|
|
38
|
+
path: |
|
|
39
|
+
mypy-junit.xml
|
|
40
|
+
mypy-txt-report/
|
|
41
|
+
mypy-html-report/
|
|
42
|
+
classes.png
|
|
43
|
+
classes.mmd
|
|
44
|
+
classes.html
|
|
45
|
+
packages.png
|
|
46
|
+
packages.mmd
|
|
47
|
+
packages.html
|
|
48
|
+
|
|
49
|
+
home-assistant:
|
|
50
|
+
name: Home Assistant compatibility
|
|
51
|
+
runs-on: ubuntu-24.04
|
|
52
|
+
steps:
|
|
53
|
+
- uses: actions/checkout@v4
|
|
54
|
+
with:
|
|
55
|
+
fetch-depth: 0
|
|
56
|
+
- uses: actions/setup-python@v5
|
|
57
|
+
with:
|
|
58
|
+
python-version: '3.13'
|
|
59
|
+
- name: Install version parser
|
|
60
|
+
run: python -m pip install packaging
|
|
61
|
+
- name: Select Home Assistant and Python versions
|
|
62
|
+
id: versions
|
|
63
|
+
run: python .github/scripts/home_assistant_versions.py
|
|
64
|
+
- uses: actions/setup-python@v5
|
|
65
|
+
with:
|
|
66
|
+
python-version: ${{ steps.versions.outputs.python }}
|
|
67
|
+
- name: Check dependency compatibility
|
|
68
|
+
env:
|
|
69
|
+
HOME_ASSISTANT_VERSION: ${{ steps.versions.outputs.home_assistant }}
|
|
70
|
+
run: |
|
|
71
|
+
python -m pip install "homeassistant==${HOME_ASSISTANT_VERSION}" .
|
|
72
|
+
python -m pip check
|
|
73
|
+
|
|
74
|
+
package:
|
|
75
|
+
name: Build package
|
|
76
|
+
runs-on: ubuntu-24.04
|
|
77
|
+
steps:
|
|
78
|
+
- uses: actions/checkout@v4
|
|
79
|
+
with:
|
|
80
|
+
fetch-depth: 0
|
|
81
|
+
- uses: actions/setup-python@v5
|
|
82
|
+
with:
|
|
83
|
+
python-version: '3.13'
|
|
84
|
+
- name: Install build tools
|
|
85
|
+
run: python -m pip install build twine
|
|
86
|
+
- name: Build and verify distributions
|
|
87
|
+
run: |
|
|
88
|
+
python -m build
|
|
89
|
+
python -m twine check --strict dist/*
|
|
90
|
+
- name: Upload distributions
|
|
91
|
+
if: always()
|
|
92
|
+
uses: actions/upload-artifact@v4
|
|
93
|
+
with:
|
|
94
|
+
name: distributions
|
|
95
|
+
if-no-files-found: ignore
|
|
96
|
+
path: dist/
|
|
97
|
+
|
|
98
|
+
renovate:
|
|
99
|
+
name: Validate Renovate configuration
|
|
100
|
+
if: github.event_name == 'pull_request'
|
|
101
|
+
runs-on: ubuntu-24.04
|
|
102
|
+
steps:
|
|
103
|
+
- uses: actions/checkout@v4
|
|
104
|
+
with:
|
|
105
|
+
fetch-depth: 0
|
|
106
|
+
- name: Check for configuration changes
|
|
107
|
+
id: changes
|
|
108
|
+
env:
|
|
109
|
+
BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
|
110
|
+
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
|
111
|
+
run: |
|
|
112
|
+
if git diff --name-only "$BASE_SHA" "$HEAD_SHA" -- renovate.json5 | grep -q .; then
|
|
113
|
+
echo 'changed=true' >> "$GITHUB_OUTPUT"
|
|
114
|
+
fi
|
|
115
|
+
- uses: actions/setup-node@v4
|
|
116
|
+
if: steps.changes.outputs.changed == 'true'
|
|
117
|
+
with:
|
|
118
|
+
node-version: '24'
|
|
119
|
+
- name: Validate Renovate configuration
|
|
120
|
+
if: steps.changes.outputs.changed == 'true'
|
|
121
|
+
run: npx --yes --package renovate -- renovate-config-validator --strict renovate.json5
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
name: Create Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_dispatch:
|
|
5
|
+
inputs:
|
|
6
|
+
version:
|
|
7
|
+
description: Release version (for example, 1.2.3)
|
|
8
|
+
required: true
|
|
9
|
+
type: string
|
|
10
|
+
commit_sha:
|
|
11
|
+
description: Commit SHA on main (defaults to the latest commit or the existing tag)
|
|
12
|
+
required: false
|
|
13
|
+
type: string
|
|
14
|
+
publish_to_pypi:
|
|
15
|
+
description: Publish to PyPI
|
|
16
|
+
required: true
|
|
17
|
+
default: false
|
|
18
|
+
type: boolean
|
|
19
|
+
|
|
20
|
+
permissions: {}
|
|
21
|
+
|
|
22
|
+
concurrency:
|
|
23
|
+
group: release-${{ inputs.version }}
|
|
24
|
+
cancel-in-progress: false
|
|
25
|
+
|
|
26
|
+
jobs:
|
|
27
|
+
tag:
|
|
28
|
+
name: Create or verify tag
|
|
29
|
+
runs-on: ubuntu-24.04
|
|
30
|
+
permissions:
|
|
31
|
+
actions: read
|
|
32
|
+
contents: write
|
|
33
|
+
outputs:
|
|
34
|
+
tag: ${{ steps.release.outputs.tag }}
|
|
35
|
+
sha: ${{ steps.release.outputs.sha }}
|
|
36
|
+
steps:
|
|
37
|
+
- name: Require the main workflow ref
|
|
38
|
+
env:
|
|
39
|
+
WORKFLOW_REF: ${{ github.ref }}
|
|
40
|
+
run: |
|
|
41
|
+
if [[ "$WORKFLOW_REF" != refs/heads/main ]]; then
|
|
42
|
+
echo 'Run this workflow from main.' >&2
|
|
43
|
+
exit 1
|
|
44
|
+
fi
|
|
45
|
+
- uses: actions/checkout@v4
|
|
46
|
+
with:
|
|
47
|
+
ref: main
|
|
48
|
+
fetch-depth: 0
|
|
49
|
+
- name: Verify the release and create the tag
|
|
50
|
+
id: release
|
|
51
|
+
env:
|
|
52
|
+
GH_TOKEN: ${{ github.token }}
|
|
53
|
+
RELEASE_VERSION: ${{ inputs.version }}
|
|
54
|
+
COMMIT_SHA: ${{ inputs.commit_sha }}
|
|
55
|
+
run: |
|
|
56
|
+
set -euo pipefail
|
|
57
|
+
if [[ ! "$RELEASE_VERSION" =~ ^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ ]]; then
|
|
58
|
+
echo 'Version must use the format 1.2.3.' >&2
|
|
59
|
+
exit 1
|
|
60
|
+
fi
|
|
61
|
+
if [[ -n "$COMMIT_SHA" && ! "$COMMIT_SHA" =~ ^[0-9a-fA-F]{7,40}$ ]]; then
|
|
62
|
+
echo 'Commit SHA must contain 7 to 40 hexadecimal characters.' >&2
|
|
63
|
+
exit 1
|
|
64
|
+
fi
|
|
65
|
+
|
|
66
|
+
tag="v${RELEASE_VERSION}"
|
|
67
|
+
if git show-ref --verify --quiet "refs/tags/$tag"; then
|
|
68
|
+
tag_sha=$(git rev-parse "refs/tags/$tag^{commit}")
|
|
69
|
+
target_sha=$tag_sha
|
|
70
|
+
if [[ -n "$COMMIT_SHA" ]]; then
|
|
71
|
+
target_sha=$(git rev-parse --verify "${COMMIT_SHA}^{commit}")
|
|
72
|
+
if [[ "$target_sha" != "$tag_sha" ]]; then
|
|
73
|
+
echo 'The existing tag points to a different commit.' >&2
|
|
74
|
+
exit 1
|
|
75
|
+
fi
|
|
76
|
+
fi
|
|
77
|
+
echo "Using existing tag $tag at $target_sha."
|
|
78
|
+
else
|
|
79
|
+
target_sha=$(git rev-parse --verify "${COMMIT_SHA:-refs/remotes/origin/main}^{commit}")
|
|
80
|
+
fi
|
|
81
|
+
|
|
82
|
+
if ! git merge-base --is-ancestor "$target_sha" refs/remotes/origin/main; then
|
|
83
|
+
echo 'The release commit must be in the history of main.' >&2
|
|
84
|
+
exit 1
|
|
85
|
+
fi
|
|
86
|
+
|
|
87
|
+
conclusion=$(gh api "repos/${GITHUB_REPOSITORY}/actions/workflows/ci.yml/runs?branch=main&event=push&head_sha=${target_sha}&per_page=100" \
|
|
88
|
+
--jq '.workflow_runs[0].conclusion // "missing"')
|
|
89
|
+
if [[ "$conclusion" != success ]]; then
|
|
90
|
+
echo "The latest CI run for $target_sha on main must succeed (status: $conclusion)." >&2
|
|
91
|
+
exit 1
|
|
92
|
+
fi
|
|
93
|
+
|
|
94
|
+
if ! git show-ref --verify --quiet "refs/tags/$tag"; then
|
|
95
|
+
git tag "$tag" "$target_sha"
|
|
96
|
+
git push origin "refs/tags/$tag"
|
|
97
|
+
fi
|
|
98
|
+
echo "tag=$tag" >> "$GITHUB_OUTPUT"
|
|
99
|
+
echo "sha=$target_sha" >> "$GITHUB_OUTPUT"
|
|
100
|
+
|
|
101
|
+
publish:
|
|
102
|
+
name: Publish to PyPI
|
|
103
|
+
if: inputs.publish_to_pypi
|
|
104
|
+
needs: tag
|
|
105
|
+
runs-on: ubuntu-24.04
|
|
106
|
+
permissions:
|
|
107
|
+
contents: read
|
|
108
|
+
steps:
|
|
109
|
+
- uses: actions/checkout@v4
|
|
110
|
+
with:
|
|
111
|
+
ref: ${{ needs.tag.outputs.sha }}
|
|
112
|
+
fetch-depth: 0
|
|
113
|
+
- uses: actions/setup-python@v5
|
|
114
|
+
with:
|
|
115
|
+
python-version: '3.13'
|
|
116
|
+
- name: Verify tag and PyPI version
|
|
117
|
+
env:
|
|
118
|
+
RELEASE_TAG: ${{ needs.tag.outputs.tag }}
|
|
119
|
+
RELEASE_SHA: ${{ needs.tag.outputs.sha }}
|
|
120
|
+
RELEASE_VERSION: ${{ inputs.version }}
|
|
121
|
+
run: |
|
|
122
|
+
set -euo pipefail
|
|
123
|
+
if [[ "$(git rev-parse "refs/tags/${RELEASE_TAG}^{commit}")" != "$RELEASE_SHA" ]]; then
|
|
124
|
+
echo 'The release tag no longer points to the selected commit.' >&2
|
|
125
|
+
exit 1
|
|
126
|
+
fi
|
|
127
|
+
python - <<'PY'
|
|
128
|
+
import os
|
|
129
|
+
from urllib.error import HTTPError
|
|
130
|
+
from urllib.request import urlopen
|
|
131
|
+
|
|
132
|
+
version = os.environ["RELEASE_VERSION"]
|
|
133
|
+
url = f"https://pypi.org/pypi/Hydro-Quebec-API-Wrapper/{version}/json"
|
|
134
|
+
try:
|
|
135
|
+
with urlopen(url, timeout=30):
|
|
136
|
+
raise SystemExit(f"Version {version} already exists on PyPI")
|
|
137
|
+
except HTTPError as error:
|
|
138
|
+
if error.code != 404:
|
|
139
|
+
raise
|
|
140
|
+
PY
|
|
141
|
+
- name: Install build tools
|
|
142
|
+
run: python -m pip install build twine packaging
|
|
143
|
+
- name: Build and verify distributions
|
|
144
|
+
env:
|
|
145
|
+
RELEASE_VERSION: ${{ inputs.version }}
|
|
146
|
+
run: |
|
|
147
|
+
python -m build
|
|
148
|
+
python -m twine check --strict dist/*
|
|
149
|
+
python - <<'PY'
|
|
150
|
+
import os
|
|
151
|
+
from pathlib import Path
|
|
152
|
+
from packaging.utils import parse_sdist_filename, parse_wheel_filename
|
|
153
|
+
from packaging.version import Version
|
|
154
|
+
|
|
155
|
+
expected = Version(os.environ["RELEASE_VERSION"])
|
|
156
|
+
distributions = list(Path("dist").iterdir())
|
|
157
|
+
if len(distributions) != 2:
|
|
158
|
+
raise SystemExit("Expected one wheel and one source distribution")
|
|
159
|
+
versions = []
|
|
160
|
+
formats = set()
|
|
161
|
+
for path in distributions:
|
|
162
|
+
if path.name.endswith(".whl"):
|
|
163
|
+
formats.add("wheel")
|
|
164
|
+
versions.append(parse_wheel_filename(path.name)[1])
|
|
165
|
+
elif path.name.endswith(".tar.gz"):
|
|
166
|
+
formats.add("sdist")
|
|
167
|
+
versions.append(parse_sdist_filename(path.name)[1])
|
|
168
|
+
else:
|
|
169
|
+
raise SystemExit(f"Unexpected distribution: {path.name}")
|
|
170
|
+
if formats != {"wheel", "sdist"}:
|
|
171
|
+
raise SystemExit("Expected one wheel and one source distribution")
|
|
172
|
+
if any(version != expected for version in versions):
|
|
173
|
+
raise SystemExit(f"Distribution versions {versions} do not match {expected}")
|
|
174
|
+
PY
|
|
175
|
+
- name: Publish distributions
|
|
176
|
+
env:
|
|
177
|
+
TWINE_USERNAME: __token__
|
|
178
|
+
TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }}
|
|
179
|
+
run: python -m twine upload --non-interactive dist/*
|
|
@@ -16,7 +16,7 @@ repos:
|
|
|
16
16
|
- id: check-ast
|
|
17
17
|
|
|
18
18
|
- repo: https://github.com/psf/black
|
|
19
|
-
rev: 25.
|
|
19
|
+
rev: 25.12.0
|
|
20
20
|
hooks:
|
|
21
21
|
- id: black
|
|
22
22
|
language_version: python3
|
|
@@ -40,16 +40,16 @@ repos:
|
|
|
40
40
|
- id: doc8
|
|
41
41
|
|
|
42
42
|
- repo: https://github.com/pre-commit/mirrors-mypy
|
|
43
|
-
rev: v1.
|
|
43
|
+
rev: v1.20.2
|
|
44
44
|
hooks:
|
|
45
45
|
- id: mypy
|
|
46
46
|
args: [--strict, --pretty, --show-error-codes]
|
|
47
47
|
additional_dependencies:
|
|
48
48
|
- "types-pytz==2025.2.0.20251108"
|
|
49
|
-
- "types-PyYAML==6.0.12.
|
|
50
|
-
- "types-python-dateutil==2.9.0.
|
|
49
|
+
- "types-PyYAML==6.0.12.20260724"
|
|
50
|
+
- "types-python-dateutil==2.9.0.20260807"
|
|
51
51
|
- "pytest-stub==1.1.0"
|
|
52
|
-
- "aiohttp==3.
|
|
52
|
+
- "aiohttp==3.14.3"
|
|
53
53
|
- "aiocache==0.12.3"
|
|
54
54
|
- "pkce==1.0.3"
|
|
55
|
-
- "pytest-asyncio==1.
|
|
55
|
+
- "pytest-asyncio==1.4.0"
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: Hydro-Quebec-API-Wrapper
|
|
3
|
-
Version: 4.
|
|
3
|
+
Version: 4.3.1
|
|
4
4
|
Summary: A wrapper library to access hydro quebec API and more
|
|
5
5
|
Home-page: https://hydroqc.ca
|
|
6
6
|
Author-email: Hydroqc Team <info@hydroqc.ca>
|
|
7
7
|
License: LGPL-3.0-or-later
|
|
8
|
-
Project-URL: Source Code, https://
|
|
9
|
-
Project-URL: Bug Reports, https://
|
|
8
|
+
Project-URL: Source Code, https://github.com/hydroqc/hydroqc
|
|
9
|
+
Project-URL: Bug Reports, https://github.com/hydroqc/hydroqc/issues
|
|
10
10
|
Project-URL: Home-page, https://hydroqc.ca
|
|
11
11
|
Keywords: hydroquebec,lib
|
|
12
12
|
Platform: any
|
|
@@ -25,8 +25,8 @@ Dynamic: license-file
|
|
|
25
25
|
# HydroQC - Hydro Quebec API wrapper
|
|
26
26
|
|
|
27
27
|
|
|
28
|
-
[](https://github.com/hydroqc/hydroqc/actions/workflows/ci.yml)
|
|
29
|
+
[](https://pypi.org/project/Hydro-Quebec-API-Wrapper/)
|
|
30
30
|
|
|
31
31
|
|
|
32
32
|
|
|
@@ -89,6 +89,58 @@ We don't have anyone to test with. Please reach out to us if you have this plan
|
|
|
89
89
|
|
|
90
90
|
If you need more information about the usage and current integrations of the library please see our website [https://hydroqc.ca](https://hydroqc.ca)
|
|
91
91
|
|
|
92
|
+
## E-mail verification (MFA)
|
|
93
|
+
|
|
94
|
+
Hydro-Québec can ask for a one-time code sent by e-mail after the password round
|
|
95
|
+
trip. `login()` returns a `LoginStatus` instead of a plain boolean:
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
from hydroqc.consts import LoginStatus
|
|
99
|
+
from hydroqc.error import HydroQcMFAInvalidCodeError
|
|
100
|
+
|
|
101
|
+
status = await webuser.login()
|
|
102
|
+
while status is LoginStatus.MFA_REQUIRED:
|
|
103
|
+
code = input("Verification code (empty = send a new one): ")
|
|
104
|
+
if not code.strip():
|
|
105
|
+
await webuser.request_mfa_code()
|
|
106
|
+
continue
|
|
107
|
+
try:
|
|
108
|
+
status = await webuser.submit_mfa_code(code)
|
|
109
|
+
except HydroQcMFAInvalidCodeError:
|
|
110
|
+
print("Code refused, try again.")
|
|
111
|
+
|
|
112
|
+
if status is not LoginStatus.SUCCESS:
|
|
113
|
+
raise RuntimeError("Login failed")
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
`bool(status)` is only true for `LoginStatus.SUCCESS`, so existing code using
|
|
117
|
+
`if await webuser.login():` keeps working. The code is requested automatically
|
|
118
|
+
when the step is detected; `request_mfa_code()` sends a new one. The
|
|
119
|
+
verification step expires after a few minutes (3 minutes on the current
|
|
120
|
+
portal): `HydroQcMFAExpiredError` means the login has to be restarted.
|
|
121
|
+
|
|
122
|
+
A silent background login cannot ask for a code and raises
|
|
123
|
+
`HydroQcMFARequiredError` instead of failing quietly.
|
|
124
|
+
|
|
125
|
+
### Reusing a session
|
|
126
|
+
|
|
127
|
+
`export_session_state()` returns the OAuth tokens plus the Azure AD B2C cookies;
|
|
128
|
+
`import_session_state()` restores them. Persisting that state lets a later run
|
|
129
|
+
skip the password (and a new verification code) while the refresh token or the
|
|
130
|
+
single sign-on cookie is still valid. The current portal issues 15-minute access
|
|
131
|
+
tokens and a 24-hour refresh token, so the stored session is normally reusable
|
|
132
|
+
for about a day; once the refresh token and the single sign-on cookie are both
|
|
133
|
+
refused, a new interactive login (and a new code) is required:
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
state = webuser.export_session_state() # store it somewhere safe
|
|
137
|
+
# ... next run ...
|
|
138
|
+
webuser = WebUser(username, password, verify_ssl=True)
|
|
139
|
+
webuser.import_session_state(state)
|
|
140
|
+
if await webuser.try_silent_login() is not LoginStatus.SUCCESS:
|
|
141
|
+
await webuser.login()
|
|
142
|
+
```
|
|
143
|
+
|
|
92
144
|
## Goal
|
|
93
145
|
|
|
94
146
|
Make it easy to fetch and manipulate data from Hydro-Quebec, especially the winter credit periods
|
|
@@ -104,7 +156,7 @@ This uses python 3.10
|
|
|
104
156
|
1. Clone the repo
|
|
105
157
|
|
|
106
158
|
```bash
|
|
107
|
-
git clone https://
|
|
159
|
+
git clone https://github.com/hydroqc/hydroqc.git
|
|
108
160
|
```
|
|
109
161
|
|
|
110
162
|
2. Create a virtual-env
|
|
@@ -16,6 +16,9 @@ test_requirements.txt
|
|
|
16
16
|
tox.ini
|
|
17
17
|
.devcontainer/Dockerfile
|
|
18
18
|
.devcontainer/devcontainer.json
|
|
19
|
+
.github/scripts/home_assistant_versions.py
|
|
20
|
+
.github/workflows/ci.yml
|
|
21
|
+
.github/workflows/release.yml
|
|
19
22
|
Hydro_Quebec_API_Wrapper.egg-info/PKG-INFO
|
|
20
23
|
Hydro_Quebec_API_Wrapper.egg-info/SOURCES.txt
|
|
21
24
|
Hydro_Quebec_API_Wrapper.egg-info/dependency_links.txt
|
|
@@ -66,6 +69,7 @@ hydroqc/hydro_api/__init__.py
|
|
|
66
69
|
hydroqc/hydro_api/cache.py
|
|
67
70
|
hydroqc/hydro_api/client.py
|
|
68
71
|
hydroqc/hydro_api/consts.py
|
|
72
|
+
hydroqc/hydro_api/mfa.py
|
|
69
73
|
hydroqc/peak/__init__.py
|
|
70
74
|
hydroqc/peak/basepeak.py
|
|
71
75
|
hydroqc/peak/basepeakhandler.py
|
|
@@ -100,4 +104,7 @@ tests/subtest_contract_m.py
|
|
|
100
104
|
tests/subtest_contract_m_gdp.py
|
|
101
105
|
tests/test_basic.py
|
|
102
106
|
tests/test_timerange.py
|
|
103
|
-
tests/tools.py
|
|
107
|
+
tests/tools.py
|
|
108
|
+
tests/unit/__init__.py
|
|
109
|
+
tests/unit/test_login_flow.py
|
|
110
|
+
tests/unit/test_mfa.py
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: Hydro-Quebec-API-Wrapper
|
|
3
|
-
Version: 4.
|
|
3
|
+
Version: 4.3.1
|
|
4
4
|
Summary: A wrapper library to access hydro quebec API and more
|
|
5
5
|
Home-page: https://hydroqc.ca
|
|
6
6
|
Author-email: Hydroqc Team <info@hydroqc.ca>
|
|
7
7
|
License: LGPL-3.0-or-later
|
|
8
|
-
Project-URL: Source Code, https://
|
|
9
|
-
Project-URL: Bug Reports, https://
|
|
8
|
+
Project-URL: Source Code, https://github.com/hydroqc/hydroqc
|
|
9
|
+
Project-URL: Bug Reports, https://github.com/hydroqc/hydroqc/issues
|
|
10
10
|
Project-URL: Home-page, https://hydroqc.ca
|
|
11
11
|
Keywords: hydroquebec,lib
|
|
12
12
|
Platform: any
|
|
@@ -25,8 +25,8 @@ Dynamic: license-file
|
|
|
25
25
|
# HydroQC - Hydro Quebec API wrapper
|
|
26
26
|
|
|
27
27
|
|
|
28
|
-
[](https://github.com/hydroqc/hydroqc/actions/workflows/ci.yml)
|
|
29
|
+
[](https://pypi.org/project/Hydro-Quebec-API-Wrapper/)
|
|
30
30
|
|
|
31
31
|
|
|
32
32
|
|
|
@@ -89,6 +89,58 @@ We don't have anyone to test with. Please reach out to us if you have this plan
|
|
|
89
89
|
|
|
90
90
|
If you need more information about the usage and current integrations of the library please see our website [https://hydroqc.ca](https://hydroqc.ca)
|
|
91
91
|
|
|
92
|
+
## E-mail verification (MFA)
|
|
93
|
+
|
|
94
|
+
Hydro-Québec can ask for a one-time code sent by e-mail after the password round
|
|
95
|
+
trip. `login()` returns a `LoginStatus` instead of a plain boolean:
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
from hydroqc.consts import LoginStatus
|
|
99
|
+
from hydroqc.error import HydroQcMFAInvalidCodeError
|
|
100
|
+
|
|
101
|
+
status = await webuser.login()
|
|
102
|
+
while status is LoginStatus.MFA_REQUIRED:
|
|
103
|
+
code = input("Verification code (empty = send a new one): ")
|
|
104
|
+
if not code.strip():
|
|
105
|
+
await webuser.request_mfa_code()
|
|
106
|
+
continue
|
|
107
|
+
try:
|
|
108
|
+
status = await webuser.submit_mfa_code(code)
|
|
109
|
+
except HydroQcMFAInvalidCodeError:
|
|
110
|
+
print("Code refused, try again.")
|
|
111
|
+
|
|
112
|
+
if status is not LoginStatus.SUCCESS:
|
|
113
|
+
raise RuntimeError("Login failed")
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
`bool(status)` is only true for `LoginStatus.SUCCESS`, so existing code using
|
|
117
|
+
`if await webuser.login():` keeps working. The code is requested automatically
|
|
118
|
+
when the step is detected; `request_mfa_code()` sends a new one. The
|
|
119
|
+
verification step expires after a few minutes (3 minutes on the current
|
|
120
|
+
portal): `HydroQcMFAExpiredError` means the login has to be restarted.
|
|
121
|
+
|
|
122
|
+
A silent background login cannot ask for a code and raises
|
|
123
|
+
`HydroQcMFARequiredError` instead of failing quietly.
|
|
124
|
+
|
|
125
|
+
### Reusing a session
|
|
126
|
+
|
|
127
|
+
`export_session_state()` returns the OAuth tokens plus the Azure AD B2C cookies;
|
|
128
|
+
`import_session_state()` restores them. Persisting that state lets a later run
|
|
129
|
+
skip the password (and a new verification code) while the refresh token or the
|
|
130
|
+
single sign-on cookie is still valid. The current portal issues 15-minute access
|
|
131
|
+
tokens and a 24-hour refresh token, so the stored session is normally reusable
|
|
132
|
+
for about a day; once the refresh token and the single sign-on cookie are both
|
|
133
|
+
refused, a new interactive login (and a new code) is required:
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
state = webuser.export_session_state() # store it somewhere safe
|
|
137
|
+
# ... next run ...
|
|
138
|
+
webuser = WebUser(username, password, verify_ssl=True)
|
|
139
|
+
webuser.import_session_state(state)
|
|
140
|
+
if await webuser.try_silent_login() is not LoginStatus.SUCCESS:
|
|
141
|
+
await webuser.login()
|
|
142
|
+
```
|
|
143
|
+
|
|
92
144
|
## Goal
|
|
93
145
|
|
|
94
146
|
Make it easy to fetch and manipulate data from Hydro-Quebec, especially the winter credit periods
|
|
@@ -104,7 +156,7 @@ This uses python 3.10
|
|
|
104
156
|
1. Clone the repo
|
|
105
157
|
|
|
106
158
|
```bash
|
|
107
|
-
git clone https://
|
|
159
|
+
git clone https://github.com/hydroqc/hydroqc.git
|
|
108
160
|
```
|
|
109
161
|
|
|
110
162
|
2. Create a virtual-env
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# HydroQC - Hydro Quebec API wrapper
|
|
2
2
|
|
|
3
3
|
|
|
4
|
-
[](https://github.com/hydroqc/hydroqc/actions/workflows/ci.yml)
|
|
5
|
+
[](https://pypi.org/project/Hydro-Quebec-API-Wrapper/)
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
|
|
@@ -65,6 +65,58 @@ We don't have anyone to test with. Please reach out to us if you have this plan
|
|
|
65
65
|
|
|
66
66
|
If you need more information about the usage and current integrations of the library please see our website [https://hydroqc.ca](https://hydroqc.ca)
|
|
67
67
|
|
|
68
|
+
## E-mail verification (MFA)
|
|
69
|
+
|
|
70
|
+
Hydro-Québec can ask for a one-time code sent by e-mail after the password round
|
|
71
|
+
trip. `login()` returns a `LoginStatus` instead of a plain boolean:
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
from hydroqc.consts import LoginStatus
|
|
75
|
+
from hydroqc.error import HydroQcMFAInvalidCodeError
|
|
76
|
+
|
|
77
|
+
status = await webuser.login()
|
|
78
|
+
while status is LoginStatus.MFA_REQUIRED:
|
|
79
|
+
code = input("Verification code (empty = send a new one): ")
|
|
80
|
+
if not code.strip():
|
|
81
|
+
await webuser.request_mfa_code()
|
|
82
|
+
continue
|
|
83
|
+
try:
|
|
84
|
+
status = await webuser.submit_mfa_code(code)
|
|
85
|
+
except HydroQcMFAInvalidCodeError:
|
|
86
|
+
print("Code refused, try again.")
|
|
87
|
+
|
|
88
|
+
if status is not LoginStatus.SUCCESS:
|
|
89
|
+
raise RuntimeError("Login failed")
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
`bool(status)` is only true for `LoginStatus.SUCCESS`, so existing code using
|
|
93
|
+
`if await webuser.login():` keeps working. The code is requested automatically
|
|
94
|
+
when the step is detected; `request_mfa_code()` sends a new one. The
|
|
95
|
+
verification step expires after a few minutes (3 minutes on the current
|
|
96
|
+
portal): `HydroQcMFAExpiredError` means the login has to be restarted.
|
|
97
|
+
|
|
98
|
+
A silent background login cannot ask for a code and raises
|
|
99
|
+
`HydroQcMFARequiredError` instead of failing quietly.
|
|
100
|
+
|
|
101
|
+
### Reusing a session
|
|
102
|
+
|
|
103
|
+
`export_session_state()` returns the OAuth tokens plus the Azure AD B2C cookies;
|
|
104
|
+
`import_session_state()` restores them. Persisting that state lets a later run
|
|
105
|
+
skip the password (and a new verification code) while the refresh token or the
|
|
106
|
+
single sign-on cookie is still valid. The current portal issues 15-minute access
|
|
107
|
+
tokens and a 24-hour refresh token, so the stored session is normally reusable
|
|
108
|
+
for about a day; once the refresh token and the single sign-on cookie are both
|
|
109
|
+
refused, a new interactive login (and a new code) is required:
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
state = webuser.export_session_state() # store it somewhere safe
|
|
113
|
+
# ... next run ...
|
|
114
|
+
webuser = WebUser(username, password, verify_ssl=True)
|
|
115
|
+
webuser.import_session_state(state)
|
|
116
|
+
if await webuser.try_silent_login() is not LoginStatus.SUCCESS:
|
|
117
|
+
await webuser.login()
|
|
118
|
+
```
|
|
119
|
+
|
|
68
120
|
## Goal
|
|
69
121
|
|
|
70
122
|
Make it easy to fetch and manipulate data from Hydro-Quebec, especially the winter credit periods
|
|
@@ -80,7 +132,7 @@ This uses python 3.10
|
|
|
80
132
|
1. Clone the repo
|
|
81
133
|
|
|
82
134
|
```bash
|
|
83
|
-
git clone https://
|
|
135
|
+
git clone https://github.com/hydroqc/hydroqc.git
|
|
84
136
|
```
|
|
85
137
|
|
|
86
138
|
2. Create a virtual-env
|