Hydro-Quebec-API-Wrapper 4.2.6__tar.gz → 4.3.0__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.
Files changed (114) hide show
  1. hydro_quebec_api_wrapper-4.3.0/.github/scripts/home_assistant_versions.py +49 -0
  2. hydro_quebec_api_wrapper-4.3.0/.github/workflows/ci.yml +121 -0
  3. hydro_quebec_api_wrapper-4.3.0/.github/workflows/release.yml +179 -0
  4. {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.0}/.pre-commit-config.yaml +6 -6
  5. {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.0/Hydro_Quebec_API_Wrapper.egg-info}/PKG-INFO +58 -6
  6. {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.0}/Hydro_Quebec_API_Wrapper.egg-info/SOURCES.txt +8 -1
  7. {hydro_quebec_api_wrapper-4.2.6/Hydro_Quebec_API_Wrapper.egg-info → hydro_quebec_api_wrapper-4.3.0}/PKG-INFO +58 -6
  8. {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.0}/README.md +55 -3
  9. {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.0}/docs/source/reference/hydroqc.hydro_api.rst +8 -0
  10. {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.0}/examples/hydro.py +21 -1
  11. hydro_quebec_api_wrapper-4.3.0/hydroqc/consts.py +28 -0
  12. hydro_quebec_api_wrapper-4.3.0/hydroqc/error.py +48 -0
  13. {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.0}/hydroqc/hydro_api/client.py +599 -130
  14. hydro_quebec_api_wrapper-4.3.0/hydroqc/hydro_api/mfa.py +762 -0
  15. {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.0}/hydroqc/webuser.py +65 -2
  16. {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.0}/pyproject.toml +2 -2
  17. hydro_quebec_api_wrapper-4.3.0/test_requirements.txt +15 -0
  18. {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.0}/tests/subtest_common.py +3 -2
  19. hydro_quebec_api_wrapper-4.3.0/tests/unit/__init__.py +1 -0
  20. hydro_quebec_api_wrapper-4.3.0/tests/unit/test_login_flow.py +379 -0
  21. hydro_quebec_api_wrapper-4.3.0/tests/unit/test_mfa.py +449 -0
  22. hydro_quebec_api_wrapper-4.2.6/hydroqc/consts.py +0 -8
  23. hydro_quebec_api_wrapper-4.2.6/hydroqc/error.py +0 -27
  24. hydro_quebec_api_wrapper-4.2.6/test_requirements.txt +0 -15
  25. {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.0}/.coveragerc +0 -0
  26. {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.0}/.devcontainer/Dockerfile +0 -0
  27. {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.0}/.devcontainer/devcontainer.json +0 -0
  28. {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.0}/.flake8 +0 -0
  29. {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.0}/.gitignore +0 -0
  30. {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.0}/.gitlab-ci.yml +0 -0
  31. {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.0}/.pylintrc +0 -0
  32. {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.0}/Hydro_Quebec_API_Wrapper.egg-info/dependency_links.txt +0 -0
  33. {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.0}/Hydro_Quebec_API_Wrapper.egg-info/entry_points.txt +0 -0
  34. {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.0}/Hydro_Quebec_API_Wrapper.egg-info/requires.txt +0 -0
  35. {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.0}/Hydro_Quebec_API_Wrapper.egg-info/top_level.txt +0 -0
  36. {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.0}/LICENSE +0 -0
  37. {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.0}/MANIFEST.in +0 -0
  38. {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.0}/config.default.yaml +0 -0
  39. {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.0}/conflict_test_requirements.txt +0 -0
  40. {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.0}/docs/Makefile +0 -0
  41. {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.0}/docs/make.bat +0 -0
  42. {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.0}/docs/source/_static/custom.css +0 -0
  43. {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.0}/docs/source/_static/hydroqc-logo-128.png +0 -0
  44. {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.0}/docs/source/_static/hydroqc-logo.svg +0 -0
  45. {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.0}/docs/source/conf.py +0 -0
  46. {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.0}/docs/source/external/description-des-codes-interruption.pdf +0 -0
  47. {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.0}/docs/source/index.rst +0 -0
  48. {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.0}/docs/source/reference/hydroqc.contract.rst +0 -0
  49. {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.0}/docs/source/reference/hydroqc.peak.cpc.rst +0 -0
  50. {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.0}/docs/source/reference/hydroqc.peak.dpc.rst +0 -0
  51. {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.0}/docs/source/reference/hydroqc.peak.rst +0 -0
  52. {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.0}/docs/source/reference/hydroqc.rst +0 -0
  53. {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.0}/docs/source/reference/hydroqc.types.rst +0 -0
  54. {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.0}/docs/source/reference/modules.rst +0 -0
  55. {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.0}/docs/source/todo.rst +0 -0
  56. {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.0}/hydroqc/__init__.py +0 -0
  57. {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.0}/hydroqc/account.py +0 -0
  58. {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.0}/hydroqc/contract/__init__.py +0 -0
  59. {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.0}/hydroqc/contract/common.py +0 -0
  60. {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.0}/hydroqc/contract/contract_d.py +0 -0
  61. {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.0}/hydroqc/contract/contract_d_cpc.py +0 -0
  62. {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.0}/hydroqc/contract/contract_dpc.py +0 -0
  63. {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.0}/hydroqc/contract/contract_dt.py +0 -0
  64. {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.0}/hydroqc/contract/contract_m.py +0 -0
  65. {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.0}/hydroqc/contract/contract_m_gdp.py +0 -0
  66. {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.0}/hydroqc/contract/contract_residential.py +0 -0
  67. {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.0}/hydroqc/contract/outage.py +0 -0
  68. {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.0}/hydroqc/customer.py +0 -0
  69. {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.0}/hydroqc/diagnostic.py +0 -0
  70. {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.0}/hydroqc/hydro_api/__init__.py +0 -0
  71. {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.0}/hydroqc/hydro_api/cache.py +0 -0
  72. {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.0}/hydroqc/hydro_api/consts.py +0 -0
  73. {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.0}/hydroqc/logger.py +0 -0
  74. {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.0}/hydroqc/peak/__init__.py +0 -0
  75. {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.0}/hydroqc/peak/basepeak.py +0 -0
  76. {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.0}/hydroqc/peak/basepeakhandler.py +0 -0
  77. {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.0}/hydroqc/peak/consts.py +0 -0
  78. {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.0}/hydroqc/peak/cpc/__init__.py +0 -0
  79. {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.0}/hydroqc/peak/cpc/consts.py +0 -0
  80. {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.0}/hydroqc/peak/cpc/handler.py +0 -0
  81. {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.0}/hydroqc/peak/cpc/peak.py +0 -0
  82. {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.0}/hydroqc/peak/dpc/__init__.py +0 -0
  83. {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.0}/hydroqc/peak/dpc/consts.py +0 -0
  84. {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.0}/hydroqc/peak/dpc/handler.py +0 -0
  85. {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.0}/hydroqc/peak/dpc/peak.py +0 -0
  86. {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.0}/hydroqc/public_client.py +0 -0
  87. {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.0}/hydroqc/py.typed +0 -0
  88. {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.0}/hydroqc/timerange.py +0 -0
  89. {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.0}/hydroqc/types/__init__.py +0 -0
  90. {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.0}/hydroqc/types/account.py +0 -0
  91. {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.0}/hydroqc/types/common.py +0 -0
  92. {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.0}/hydroqc/types/consump.py +0 -0
  93. {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.0}/hydroqc/types/contract.py +0 -0
  94. {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.0}/hydroqc/types/cpc.py +0 -0
  95. {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.0}/hydroqc/types/dpc.py +0 -0
  96. {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.0}/hydroqc/types/dt.py +0 -0
  97. {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.0}/hydroqc/types/outage.py +0 -0
  98. {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.0}/hydroqc/utils.py +0 -0
  99. {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.0}/logo/hydroqc-logo-128.png +0 -0
  100. {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.0}/logo/hydroqc-logo.svg +0 -0
  101. {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.0}/renovate.json5 +0 -0
  102. {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.0}/setup.cfg +0 -0
  103. {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.0}/tests/__init__.py +0 -0
  104. {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.0}/tests/conftest.py +0 -0
  105. {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.0}/tests/subtest_contract_d.py +0 -0
  106. {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.0}/tests/subtest_contract_d_cpc.py +0 -0
  107. {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.0}/tests/subtest_contract_dpc.py +0 -0
  108. {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.0}/tests/subtest_contract_dt.py +0 -0
  109. {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.0}/tests/subtest_contract_m.py +0 -0
  110. {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.0}/tests/subtest_contract_m_gdp.py +0 -0
  111. {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.0}/tests/test_basic.py +0 -0
  112. {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.0}/tests/test_timerange.py +0 -0
  113. {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.0}/tests/tools.py +0 -0
  114. {hydro_quebec_api_wrapper-4.2.6 → hydro_quebec_api_wrapper-4.3.0}/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.11.0
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.19.0
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.20250915"
50
- - "types-python-dateutil==2.9.0.20251115"
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.13.2"
52
+ - "aiohttp==3.14.3"
53
53
  - "aiocache==0.12.3"
54
54
  - "pkce==1.0.3"
55
- - "pytest-asyncio==1.3.0"
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.2.6
3
+ Version: 4.3.0
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://gitlab.com/hydroqc/hydroqc
9
- Project-URL: Bug Reports, https://gitlab.com/hydroqc/hydroqc/-/issues
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
- [![coverage report](https://gitlab.com/hydroqc/hydroqc/badges/main/coverage.svg)](https://gitlab.com/hydroqc/hydroqc/-/commits/main)
29
- [![pipeline status](https://gitlab.com/hydroqc/hydroqc/badges/main/pipeline.svg)](https://gitlab.com/hydroqc/hydroqc/-/commits/main)
28
+ [![CI](https://github.com/hydroqc/hydroqc/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/hydroqc/hydroqc/actions/workflows/ci.yml)
29
+ [![PyPI](https://img.shields.io/pypi/v/Hydro-Quebec-API-Wrapper.svg)](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://gitlab.com/hydroqc/hydroqc.git
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.2.6
3
+ Version: 4.3.0
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://gitlab.com/hydroqc/hydroqc
9
- Project-URL: Bug Reports, https://gitlab.com/hydroqc/hydroqc/-/issues
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
- [![coverage report](https://gitlab.com/hydroqc/hydroqc/badges/main/coverage.svg)](https://gitlab.com/hydroqc/hydroqc/-/commits/main)
29
- [![pipeline status](https://gitlab.com/hydroqc/hydroqc/badges/main/pipeline.svg)](https://gitlab.com/hydroqc/hydroqc/-/commits/main)
28
+ [![CI](https://github.com/hydroqc/hydroqc/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/hydroqc/hydroqc/actions/workflows/ci.yml)
29
+ [![PyPI](https://img.shields.io/pypi/v/Hydro-Quebec-API-Wrapper.svg)](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://gitlab.com/hydroqc/hydroqc.git
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
- [![coverage report](https://gitlab.com/hydroqc/hydroqc/badges/main/coverage.svg)](https://gitlab.com/hydroqc/hydroqc/-/commits/main)
5
- [![pipeline status](https://gitlab.com/hydroqc/hydroqc/badges/main/pipeline.svg)](https://gitlab.com/hydroqc/hydroqc/-/commits/main)
4
+ [![CI](https://github.com/hydroqc/hydroqc/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/hydroqc/hydroqc/actions/workflows/ci.yml)
5
+ [![PyPI](https://img.shields.io/pypi/v/Hydro-Quebec-API-Wrapper.svg)](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://gitlab.com/hydroqc/hydroqc.git
135
+ git clone https://github.com/hydroqc/hydroqc.git
84
136
  ```
85
137
 
86
138
  2. Create a virtual-env