myconso 0.0.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.
@@ -0,0 +1,207 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[codz]
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
+ share/python-wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+ MANIFEST
28
+
29
+ # PyInstaller
30
+ # Usually these files are written by a python script from a template
31
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
32
+ *.manifest
33
+ *.spec
34
+
35
+ # Installer logs
36
+ pip-log.txt
37
+ pip-delete-this-directory.txt
38
+
39
+ # Unit test / coverage reports
40
+ htmlcov/
41
+ .tox/
42
+ .nox/
43
+ .coverage
44
+ .coverage.*
45
+ .cache
46
+ nosetests.xml
47
+ coverage.xml
48
+ *.cover
49
+ *.py.cover
50
+ .hypothesis/
51
+ .pytest_cache/
52
+ cover/
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
+ .pybuilder/
76
+ target/
77
+
78
+ # Jupyter Notebook
79
+ .ipynb_checkpoints
80
+
81
+ # IPython
82
+ profile_default/
83
+ ipython_config.py
84
+
85
+ # pyenv
86
+ # For a library or package, you might want to ignore these files since the code is
87
+ # intended to run in multiple environments; otherwise, check them in:
88
+ # .python-version
89
+
90
+ # pipenv
91
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
93
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
94
+ # install all needed dependencies.
95
+ #Pipfile.lock
96
+
97
+ # UV
98
+ # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
99
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
100
+ # commonly ignored for libraries.
101
+ #uv.lock
102
+
103
+ # poetry
104
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
105
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
106
+ # commonly ignored for libraries.
107
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
108
+ #poetry.lock
109
+ #poetry.toml
110
+
111
+ # pdm
112
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
113
+ # pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
114
+ # https://pdm-project.org/en/latest/usage/project/#working-with-version-control
115
+ #pdm.lock
116
+ #pdm.toml
117
+ .pdm-python
118
+ .pdm-build/
119
+
120
+ # pixi
121
+ # Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
122
+ #pixi.lock
123
+ # Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
124
+ # in the .venv directory. It is recommended not to include this directory in version control.
125
+ .pixi
126
+
127
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
128
+ __pypackages__/
129
+
130
+ # Celery stuff
131
+ celerybeat-schedule
132
+ celerybeat.pid
133
+
134
+ # SageMath parsed files
135
+ *.sage.py
136
+
137
+ # Environments
138
+ .env
139
+ .envrc
140
+ .venv
141
+ env/
142
+ venv/
143
+ ENV/
144
+ env.bak/
145
+ venv.bak/
146
+
147
+ # Spyder project settings
148
+ .spyderproject
149
+ .spyproject
150
+
151
+ # Rope project settings
152
+ .ropeproject
153
+
154
+ # mkdocs documentation
155
+ /site
156
+
157
+ # mypy
158
+ .mypy_cache/
159
+ .dmypy.json
160
+ dmypy.json
161
+
162
+ # Pyre type checker
163
+ .pyre/
164
+
165
+ # pytype static type analyzer
166
+ .pytype/
167
+
168
+ # Cython debug symbols
169
+ cython_debug/
170
+
171
+ # PyCharm
172
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
173
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
174
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
175
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
176
+ #.idea/
177
+
178
+ # Abstra
179
+ # Abstra is an AI-powered process automation framework.
180
+ # Ignore directories containing user credentials, local state, and settings.
181
+ # Learn more at https://abstra.io/docs
182
+ .abstra/
183
+
184
+ # Visual Studio Code
185
+ # Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
186
+ # that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
187
+ # and can be added to the global gitignore or merged into this file. However, if you prefer,
188
+ # you could uncomment the following to ignore the entire vscode folder
189
+ # .vscode/
190
+
191
+ # Ruff stuff:
192
+ .ruff_cache/
193
+
194
+ # PyPI configuration file
195
+ .pypirc
196
+
197
+ # Cursor
198
+ # Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to
199
+ # exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
200
+ # refer to https://docs.cursor.com/context/ignore-files
201
+ .cursorignore
202
+ .cursorindexingignore
203
+
204
+ # Marimo
205
+ marimo/_static/
206
+ marimo/_lsp/
207
+ __marimo__/
myconso-0.0.1/PKG-INFO ADDED
@@ -0,0 +1,119 @@
1
+ Metadata-Version: 2.4
2
+ Name: myconso
3
+ Version: 0.0.1
4
+ Summary: Async Python client to interact with myconso.net API
5
+ Project-URL: homepage, https://github.com/remijouannet/myconso.py
6
+ Project-URL: repository, https://github.com/remijouannet/myconso.py
7
+ Author-email: Rémi Jouannet <remijouannet@gmail.com>
8
+ Requires-Python: >=3.10
9
+ Requires-Dist: aiohttp
10
+ Requires-Dist: aiohttp-retry>=2.9.1
11
+ Requires-Dist: pyjwt[crypto]
12
+ Description-Content-Type: text/markdown
13
+
14
+
15
+ # Unofficial async python client for myconso (proxiserve) API
16
+
17
+ Unofficial fully asynchronous API client for the myconso api behind the [Myconso application](https://play.google.com/store/apps/details?id=fr.proxiserve.myconso&hl=fr). To easily retrieve information about your counters (waterhot, watercold, thermal, ...)
18
+
19
+ ## Installation
20
+
21
+ ```bash
22
+ pip install myconso
23
+ ```
24
+
25
+ ## Getting started
26
+
27
+ ### Client
28
+ ```python
29
+ import asyncio
30
+ import os
31
+ from datetime import datetime
32
+ from pprint import pprint
33
+
34
+ from myconso.api import MyConsoClient
35
+
36
+ MYCONSO_EMAIL = os.getenv("MYCONSO_EMAIL")
37
+ MYCONSO_PASSWORD = os.getenv("MYCONSO_PASSWORD")
38
+
39
+
40
+ async def main():
41
+ async with MyConsoClient(username=MYCONSO_EMAIL, password=MYCONSO_PASSWORD) as c:
42
+ pprint(await c.get_dashboard())
43
+ pprint(await c.get_housing())
44
+
45
+ ctrs = await c.get_counters()
46
+ pprint(ctrs)
47
+
48
+ pprint(await c.get_meter_info(counter=ctrs[0]["counter"]))
49
+
50
+ pprint(await c.get_consumption(fluidtype="waterHot"))
51
+
52
+ pprint(
53
+ await c.get_consumption(
54
+ fluidtype="waterHot",
55
+ startdate=datetime(2025, 12, 1),
56
+ enddate=datetime(2025, 12, 4),
57
+ )
58
+ )
59
+
60
+ pprint(await c.get_meter(counter=ctrs[0]["counter"]))
61
+
62
+ pprint(
63
+ await c.get_meter(
64
+ counter=ctrs[0]["counter"],
65
+ startdate=datetime(2025, 12, 1),
66
+ enddate=datetime(2025, 12, 4),
67
+ )
68
+ )
69
+
70
+
71
+ asyncio.run(main())
72
+ ```
73
+ ### cli
74
+
75
+ ```bash
76
+ .venv/bin/myconsocli --help
77
+ usage: myconsocli [-h] [--debug] --email EMAIL --password PASSWORD [--auth] [--dashboard] [--counters] [--housing] [--user] [--meter-info METER_INFO]
78
+ [--meter METER] [--consumption CONSUMPTION] [--start-date START_DATE] [--end-date END_DATE]
79
+
80
+ myconso cli
81
+
82
+ options:
83
+ -h, --help show this help message and exit
84
+ --debug enable debug logging
85
+ --email EMAIL email
86
+ --password PASSWORD password
87
+ --auth POST auth/
88
+ --dashboard GET /secured/consumption/{housing}/dashboard
89
+ --counters List counters from dashboard
90
+ --housing GET /secured/housing/{housing}
91
+ --user GET /secured/users/{user}
92
+ --meter-info METER_INFO
93
+ GET /secured/meter/{housing}/{fluidType}/{counter}/info
94
+ --meter METER GET /secured/meter/{housing}/{fluidType}/{counter}
95
+ --consumption CONSUMPTION
96
+ /secured/consumption/{housing}/{fluidtype}/day
97
+ --start-date START_DATE
98
+ start date for consumption and meter
99
+ --end-date END_DATE end date for consumption and meter
100
+
101
+ .venv/bin/myconsocli --email $MYCONSO_EMAIL --password $MYCONSO_PASSWORD --dashboard
102
+ {'currentMonth': {'endDate': '2025-12-13T12:01:00+00:00',
103
+ 'startDate': '2025-12-01T17:25:19+00:00',
104
+ 'values': [{'counters': ['123456'],
105
+ 'fluidType': 'waterHot',
106
+ 'maxValue': 1.0,
107
+ 'meterType': 'waterHot',
108
+ 'minValue': 1.0,
109
+ 'unit': 'm3',
110
+ 'value': 1.0,
111
+ 'weightedValue': None},
112
+ }
113
+
114
+ .venv/bin/myconsocli --email $MYCONSO_EMAIL --password $MYCONSO_PASSWORD --meter-info 123456789
115
+ {}
116
+
117
+ .venv/bin/myconsocli --email $MYCONSO_EMAIL --password $MYCONSO_PASSWORD --meter 123456789 --start-date 2025-11-01 --end-date 2025-11-05
118
+ {}
119
+ ```
@@ -0,0 +1,106 @@
1
+
2
+ # Unofficial async python client for myconso (proxiserve) API
3
+
4
+ Unofficial fully asynchronous API client for the myconso api behind the [Myconso application](https://play.google.com/store/apps/details?id=fr.proxiserve.myconso&hl=fr). To easily retrieve information about your counters (waterhot, watercold, thermal, ...)
5
+
6
+ ## Installation
7
+
8
+ ```bash
9
+ pip install myconso
10
+ ```
11
+
12
+ ## Getting started
13
+
14
+ ### Client
15
+ ```python
16
+ import asyncio
17
+ import os
18
+ from datetime import datetime
19
+ from pprint import pprint
20
+
21
+ from myconso.api import MyConsoClient
22
+
23
+ MYCONSO_EMAIL = os.getenv("MYCONSO_EMAIL")
24
+ MYCONSO_PASSWORD = os.getenv("MYCONSO_PASSWORD")
25
+
26
+
27
+ async def main():
28
+ async with MyConsoClient(username=MYCONSO_EMAIL, password=MYCONSO_PASSWORD) as c:
29
+ pprint(await c.get_dashboard())
30
+ pprint(await c.get_housing())
31
+
32
+ ctrs = await c.get_counters()
33
+ pprint(ctrs)
34
+
35
+ pprint(await c.get_meter_info(counter=ctrs[0]["counter"]))
36
+
37
+ pprint(await c.get_consumption(fluidtype="waterHot"))
38
+
39
+ pprint(
40
+ await c.get_consumption(
41
+ fluidtype="waterHot",
42
+ startdate=datetime(2025, 12, 1),
43
+ enddate=datetime(2025, 12, 4),
44
+ )
45
+ )
46
+
47
+ pprint(await c.get_meter(counter=ctrs[0]["counter"]))
48
+
49
+ pprint(
50
+ await c.get_meter(
51
+ counter=ctrs[0]["counter"],
52
+ startdate=datetime(2025, 12, 1),
53
+ enddate=datetime(2025, 12, 4),
54
+ )
55
+ )
56
+
57
+
58
+ asyncio.run(main())
59
+ ```
60
+ ### cli
61
+
62
+ ```bash
63
+ .venv/bin/myconsocli --help
64
+ usage: myconsocli [-h] [--debug] --email EMAIL --password PASSWORD [--auth] [--dashboard] [--counters] [--housing] [--user] [--meter-info METER_INFO]
65
+ [--meter METER] [--consumption CONSUMPTION] [--start-date START_DATE] [--end-date END_DATE]
66
+
67
+ myconso cli
68
+
69
+ options:
70
+ -h, --help show this help message and exit
71
+ --debug enable debug logging
72
+ --email EMAIL email
73
+ --password PASSWORD password
74
+ --auth POST auth/
75
+ --dashboard GET /secured/consumption/{housing}/dashboard
76
+ --counters List counters from dashboard
77
+ --housing GET /secured/housing/{housing}
78
+ --user GET /secured/users/{user}
79
+ --meter-info METER_INFO
80
+ GET /secured/meter/{housing}/{fluidType}/{counter}/info
81
+ --meter METER GET /secured/meter/{housing}/{fluidType}/{counter}
82
+ --consumption CONSUMPTION
83
+ /secured/consumption/{housing}/{fluidtype}/day
84
+ --start-date START_DATE
85
+ start date for consumption and meter
86
+ --end-date END_DATE end date for consumption and meter
87
+
88
+ .venv/bin/myconsocli --email $MYCONSO_EMAIL --password $MYCONSO_PASSWORD --dashboard
89
+ {'currentMonth': {'endDate': '2025-12-13T12:01:00+00:00',
90
+ 'startDate': '2025-12-01T17:25:19+00:00',
91
+ 'values': [{'counters': ['123456'],
92
+ 'fluidType': 'waterHot',
93
+ 'maxValue': 1.0,
94
+ 'meterType': 'waterHot',
95
+ 'minValue': 1.0,
96
+ 'unit': 'm3',
97
+ 'value': 1.0,
98
+ 'weightedValue': None},
99
+ }
100
+
101
+ .venv/bin/myconsocli --email $MYCONSO_EMAIL --password $MYCONSO_PASSWORD --meter-info 123456789
102
+ {}
103
+
104
+ .venv/bin/myconsocli --email $MYCONSO_EMAIL --password $MYCONSO_PASSWORD --meter 123456789 --start-date 2025-11-01 --end-date 2025-11-05
105
+ {}
106
+ ```
File without changes
@@ -0,0 +1,248 @@
1
+ import asyncio
2
+ import logging
3
+ import time
4
+ from datetime import datetime
5
+ from types import TracebackType
6
+
7
+ from aiohttp import ClientHandlerType, ClientRequest, ClientResponse, ClientSession
8
+
9
+ from myconso.middlewares import exponential_backoff_middleware
10
+ from myconso.utils import (
11
+ clean_json_ld,
12
+ decode_jwt,
13
+ first_day_of_the_month,
14
+ last_day_of_the_month,
15
+ )
16
+
17
+ log = logging.getLogger(__name__)
18
+
19
+ MYCONSO_API = "https://api.myconso.net"
20
+ MYCONSO_USER_AGENT = "MyConso"
21
+
22
+
23
+ def check_auth(func):
24
+ async def wrapper(self, *args, **kwargs):
25
+ if not self.token and (self.username and self.password):
26
+ # class has been initialized with username/password
27
+ async with self.lock:
28
+ await self.auth()
29
+ elif not self._housing and self.token and self.refresh_token:
30
+ # class has been initialized with token/refresh_token
31
+ async with self.lock:
32
+ await self.auth_refresh()
33
+ return await func(self, *args, **kwargs)
34
+
35
+ return wrapper
36
+
37
+
38
+ class MyConsoClient:
39
+ username: str | None
40
+ password: str | None
41
+ token: str | None
42
+ refresh_token: str | None
43
+ _counters: list[dict]
44
+
45
+ def __init__(
46
+ self,
47
+ username: str | None = None,
48
+ password: str | None = None,
49
+ token: str | None = None,
50
+ refresh_token: str | None = None,
51
+ ) -> None:
52
+ if token and refresh_token:
53
+ self.token = token
54
+ self.token_exp, self.token_iat = decode_jwt(self.token)
55
+ self.refresh_token = refresh_token
56
+ elif username and password:
57
+ self.token = None
58
+ self.refresh_token = None
59
+ self.username = username
60
+ self.password = password
61
+ else:
62
+ raise ValueError(
63
+ "You must either provide username/password or token/refresh_token"
64
+ )
65
+
66
+ self._housing = None
67
+ self._counters = []
68
+ self.lock = asyncio.Lock()
69
+ self.session = ClientSession(
70
+ base_url=MYCONSO_API,
71
+ headers={"user-agent": MYCONSO_USER_AGENT},
72
+ raise_for_status=True,
73
+ middlewares=(
74
+ exponential_backoff_middleware,
75
+ self._auth_refresh_middleware,
76
+ ),
77
+ )
78
+
79
+ async def __aenter__(self) -> "MyConsoClient":
80
+ return self
81
+
82
+ async def __aexit__(
83
+ self,
84
+ exc_type: type[BaseException] | None,
85
+ exc_value: BaseException | None,
86
+ traceback: TracebackType | None,
87
+ ) -> None:
88
+ await self.close()
89
+
90
+ async def close(self) -> None:
91
+ await self.session.close()
92
+
93
+ async def _auth_refresh_middleware(
94
+ self, req: ClientRequest, handler: ClientHandlerType
95
+ ) -> ClientResponse:
96
+ epoch_now = time.time()
97
+ if epoch_now > self.token_exp:
98
+ log.debug(
99
+ "token is expired, refresh it, exp: %s, time: %s",
100
+ self.token_exp,
101
+ epoch_now,
102
+ )
103
+ async with self.lock:
104
+ await self.auth_refresh()
105
+
106
+ for _ in range(2):
107
+ res = await handler(req)
108
+ if res.status in {401}:
109
+ log.debug("received %s, try to refresh the bearer token", res.status)
110
+ async with self.lock:
111
+ await self.auth_refresh()
112
+ else:
113
+ return res
114
+
115
+ return res
116
+
117
+ async def auth(self) -> dict:
118
+ async with self.session.post(
119
+ "/auth",
120
+ json={
121
+ "email": self.username,
122
+ "password": self.password,
123
+ },
124
+ middlewares=(),
125
+ ) as res:
126
+ res = await res.json()
127
+ self._user = res["user"]["email"]
128
+ self._housing = res["housing"]
129
+ self.token = res["token"]
130
+ self.refresh_token = res["refresh_token"]
131
+ self.token_exp, self.token_iat = decode_jwt(self.token)
132
+ self.session.headers.update({"authorization": f"Bearer {self.token}"})
133
+
134
+ log.debug("successful authentification for housing: %s", self._housing)
135
+
136
+ return res
137
+
138
+ async def auth_refresh(self) -> dict:
139
+ async with self.session.post(
140
+ "/auth/refresh",
141
+ json={
142
+ "refresh_token": self.refresh_token,
143
+ },
144
+ middlewares=(),
145
+ ) as res:
146
+ res = await res.json()
147
+ self._user = res["user"]["email"]
148
+ self._housing = res["housing"]
149
+ self.token = res["token"]
150
+ self.refresh_token = res["refresh_token"]
151
+ self.token_exp, self.token_iat = decode_jwt(self.token)
152
+ self.session.headers["authorization"] = f"Bearer {self.token}"
153
+
154
+ return res
155
+
156
+ @check_auth
157
+ async def get_user(self) -> dict:
158
+ async with self.session.get(f"/secured/users/{self._user}") as res:
159
+ return clean_json_ld(await res.json())
160
+
161
+ @check_auth
162
+ async def get_housing(self) -> dict:
163
+ async with self.session.get(f"/secured/housing/{self._housing}") as res:
164
+ return clean_json_ld(await res.json())
165
+
166
+ @check_auth
167
+ async def get_dashboard(self) -> dict:
168
+ async with self.session.get(
169
+ f"/secured/consumption/{self._housing}/dashboard"
170
+ ) as res:
171
+ return clean_json_ld(await res.json())
172
+
173
+ @check_auth
174
+ async def get_counters(self) -> list[dict]:
175
+ if not self._counters:
176
+ res = await self.get_dashboard()
177
+ for v in res["currentMonth"]["values"]:
178
+ for counter in v["counters"]:
179
+ self._counters.append(
180
+ {
181
+ "counter": counter,
182
+ "fluidType": v["fluidType"],
183
+ "meterType": v["meterType"],
184
+ "unit": v["unit"],
185
+ }
186
+ )
187
+ return self._counters
188
+
189
+ @check_auth
190
+ async def get_consumption(
191
+ self,
192
+ fluidtype: str,
193
+ startdate: datetime | None = None,
194
+ enddate: datetime | None = None,
195
+ ) -> dict | None:
196
+ if not startdate:
197
+ startdate = first_day_of_the_month()
198
+ if not enddate:
199
+ enddate = last_day_of_the_month()
200
+
201
+ async with self.session.get(
202
+ f"/secured/consumption/{self._housing}/{fluidtype}/day",
203
+ params={
204
+ "startDate": startdate.isoformat(timespec="milliseconds"),
205
+ "endDate": enddate.isoformat(timespec="milliseconds"),
206
+ },
207
+ ) as res:
208
+ return clean_json_ld(await res.json())
209
+
210
+ @check_auth
211
+ async def get_meter_info(self, counter: str) -> dict | None:
212
+ if not self._counters:
213
+ await self.get_counters()
214
+
215
+ for c in self._counters:
216
+ if c["counter"] == counter:
217
+ async with self.session.get(
218
+ f"/secured/meter/{self._housing}/{c['fluidType']}/{c['counter']}/info",
219
+ ) as res:
220
+ return clean_json_ld(await res.json())
221
+ return None
222
+
223
+ @check_auth
224
+ async def get_meter(
225
+ self,
226
+ counter: str,
227
+ startdate: datetime | None = None,
228
+ enddate: datetime | None = None,
229
+ ) -> dict | None:
230
+ if not startdate:
231
+ startdate = first_day_of_the_month()
232
+ if not enddate:
233
+ enddate = last_day_of_the_month()
234
+
235
+ if not self._counters:
236
+ await self.get_counters()
237
+
238
+ for c in self._counters:
239
+ if c["counter"] == counter:
240
+ async with self.session.get(
241
+ f"/secured/meter/{self._housing}/{c['fluidType']}/{c['counter']}",
242
+ params={
243
+ "startDate": startdate.isoformat(timespec="milliseconds"),
244
+ "endDate": enddate.isoformat(timespec="milliseconds"),
245
+ },
246
+ ) as res:
247
+ return clean_json_ld(await res.json())
248
+ return None
@@ -0,0 +1,139 @@
1
+ import argparse
2
+ import asyncio
3
+ import datetime
4
+ import logging
5
+ from pprint import pprint
6
+
7
+ from myconso.api import MyConsoClient
8
+
9
+ logging.basicConfig(level=logging.INFO)
10
+ log = logging.getLogger(__name__)
11
+
12
+
13
+ async def cli() -> None:
14
+ parser = argparse.ArgumentParser(description="myconso cli")
15
+ parser.add_argument(
16
+ "--debug",
17
+ dest="debug",
18
+ default=False,
19
+ action="store_true",
20
+ help="enable debug logging",
21
+ )
22
+ parser.add_argument(
23
+ "--email",
24
+ dest="email",
25
+ type=str,
26
+ required=True,
27
+ help="email",
28
+ )
29
+ parser.add_argument(
30
+ "--password",
31
+ dest="password",
32
+ type=str,
33
+ required=True,
34
+ help="password",
35
+ )
36
+ parser.add_argument(
37
+ "--auth",
38
+ dest="auth",
39
+ default=False,
40
+ action="store_true",
41
+ help="POST auth/",
42
+ )
43
+ parser.add_argument(
44
+ "--dashboard",
45
+ dest="dashboard",
46
+ default=False,
47
+ action="store_true",
48
+ help="GET /secured/consumption/{housing}/dashboard",
49
+ )
50
+ parser.add_argument(
51
+ "--counters",
52
+ dest="counters",
53
+ default=False,
54
+ action="store_true",
55
+ help="List counters from dashboard",
56
+ )
57
+ parser.add_argument(
58
+ "--housing",
59
+ dest="housing",
60
+ default=False,
61
+ action="store_true",
62
+ help="GET /secured/housing/{housing}",
63
+ )
64
+ parser.add_argument(
65
+ "--user",
66
+ dest="user",
67
+ default=False,
68
+ action="store_true",
69
+ help="GET /secured/users/{user}",
70
+ )
71
+ parser.add_argument(
72
+ "--meter-info",
73
+ dest="meter_info",
74
+ default=None,
75
+ type=str,
76
+ help="GET /secured/meter/{housing}/{fluidType}/{counter}/info",
77
+ )
78
+ parser.add_argument(
79
+ "--meter",
80
+ dest="meter",
81
+ default=None,
82
+ type=str,
83
+ help="GET /secured/meter/{housing}/{fluidType}/{counter}",
84
+ )
85
+ parser.add_argument(
86
+ "--consumption",
87
+ dest="consumption",
88
+ default=None,
89
+ type=str,
90
+ help="/secured/consumption/{housing}/{fluidtype}/day",
91
+ )
92
+ parser.add_argument(
93
+ "--start-date",
94
+ dest="start_date",
95
+ default=None,
96
+ type=lambda s: datetime.datetime.strptime(s, "%Y-%m-%d"),
97
+ help="start date for consumption and meter",
98
+ )
99
+ parser.add_argument(
100
+ "--end-date",
101
+ dest="end_date",
102
+ default=None,
103
+ type=lambda s: datetime.datetime.strptime(s, "%Y-%m-%d"),
104
+ help="end date for consumption and meter",
105
+ )
106
+
107
+ args = parser.parse_args()
108
+
109
+ if args.debug:
110
+ logging.getLogger().setLevel(logging.DEBUG)
111
+ log.debug('debug enabled')
112
+
113
+ async with MyConsoClient(username=args.email, password=args.password) as myconso:
114
+ if args.auth:
115
+ pprint(await myconso.auth())
116
+ elif args.dashboard:
117
+ pprint(await myconso.get_dashboard())
118
+ elif args.counters:
119
+ pprint(await myconso.get_counters())
120
+ elif args.housing:
121
+ pprint(await myconso.get_housing())
122
+ elif args.user:
123
+ pprint(await myconso.get_user())
124
+ elif args.meter_info:
125
+ pprint(await myconso.get_meter_info(args.meter_info))
126
+ elif args.meter:
127
+ pprint(await myconso.get_meter(args.meter, args.start_date, args.end_date))
128
+ elif args.consumption:
129
+ pprint(
130
+ await myconso.get_consumption(
131
+ args.consumption, args.start_date, args.end_date
132
+ )
133
+ )
134
+ else:
135
+ pprint(await myconso.get_dashboard())
136
+
137
+
138
+ def main() -> None:
139
+ asyncio.run(cli())
@@ -0,0 +1,35 @@
1
+ import asyncio
2
+ import logging
3
+ import random
4
+
5
+ from aiohttp import ClientHandlerType, ClientRequest, ClientResponse
6
+
7
+ log = logging.getLogger(__name__)
8
+
9
+ BACKOFF_STATUS_CODES = {429, 503}
10
+ BACKOFF_MAX_RETRIES = 2
11
+ BACKOFF_FACTOR = 2.0
12
+ BACKOFF_MAX_DELAY = 60.0
13
+ BACKOFF_JITTER = 2
14
+
15
+
16
+ async def exponential_backoff_middleware(
17
+ req: ClientRequest, handler: ClientHandlerType
18
+ ) -> ClientResponse:
19
+ retry_count = 1
20
+
21
+ res = await handler(req)
22
+ while retry_count <= BACKOFF_MAX_RETRIES:
23
+ if retry_count < BACKOFF_MAX_RETRIES and res.status in BACKOFF_STATUS_CODES:
24
+ delay = round(
25
+ min(BACKOFF_FACTOR * (2**retry_count), BACKOFF_MAX_DELAY)
26
+ + random.uniform(0, BACKOFF_JITTER),
27
+ 3,
28
+ )
29
+ log.debug("retry backoff for %s, sleep for %ss", res.status, str(delay))
30
+ await asyncio.sleep(delay)
31
+ retry_count += 1
32
+ res = await handler(req)
33
+ else:
34
+ break
35
+ return res
@@ -0,0 +1,45 @@
1
+ import calendar
2
+ from datetime import datetime, timezone
3
+
4
+ import jwt
5
+
6
+
7
+ def clean_json_ld(obj: dict) -> dict:
8
+ # json-ld add keys that starts with @
9
+ # we don't need thoses, pop them
10
+ if isinstance(obj, dict):
11
+ keys_to_pop = [key for key in obj if key.startswith("@")]
12
+ for key in keys_to_pop:
13
+ obj.pop(key, None)
14
+ return obj
15
+
16
+
17
+ def decode_jwt(token: str) -> tuple[int, int]:
18
+ token_jwt = jwt.decode(
19
+ token,
20
+ algorithms=["RS256"],
21
+ key="",
22
+ options={"verify_signature": False},
23
+ )
24
+ return (token_jwt["exp"], token_jwt["iat"])
25
+
26
+
27
+ def last_day_of_the_month() -> datetime:
28
+ # last day of the current month
29
+ return datetime.now(timezone.utc).replace(
30
+ day=calendar.monthrange(
31
+ datetime.now(timezone.utc).year,
32
+ datetime.now(timezone.utc).month,
33
+ )[1],
34
+ hour=23,
35
+ minute=59,
36
+ second=59,
37
+ microsecond=0,
38
+ )
39
+
40
+
41
+ def first_day_of_the_month() -> datetime:
42
+ # first day of the current month
43
+ return datetime.now(timezone.utc).replace(
44
+ day=1, hour=0, minute=0, second=0, microsecond=0
45
+ )
@@ -0,0 +1,77 @@
1
+ [project]
2
+ name = "myconso"
3
+ version = "0.0.1"
4
+ description = "Async Python client to interact with myconso.net API"
5
+ readme = "README.md"
6
+ authors = [
7
+ {name = "Rémi Jouannet", email = "remijouannet@gmail.com"},
8
+ ]
9
+ requires-python = ">=3.10"
10
+ packages = [
11
+ { include = "myconso" }
12
+ ]
13
+ dependencies = [
14
+ "aiohttp",
15
+ "aiohttp-retry>=2.9.1",
16
+ "pyjwt[crypto]",
17
+ ]
18
+
19
+ [project.urls]
20
+ homepage = "https://github.com/remijouannet/myconso.py"
21
+ repository = "https://github.com/remijouannet/myconso.py"
22
+
23
+ [project.scripts]
24
+ myconsocli = "myconso.cli:main"
25
+
26
+ [dependency-groups]
27
+ dev = [
28
+ "ruff",
29
+ "mypy",
30
+ "pre-commit>=4.2.0",
31
+ "pytest>=9.0.1",
32
+ "pytest-asyncio>=1.3.0",
33
+ "pytest-aiohttp>=1.1.0",
34
+ ]
35
+
36
+ [build-system]
37
+ requires = ["hatchling"]
38
+ build-backend = "hatchling.build"
39
+
40
+ [tool.hatch.build.targets.sdist]
41
+ include = [
42
+ "myconso/*",
43
+ ]
44
+
45
+ [tool.hatch.build.targets.wheel]
46
+ packages = ["myconso"]
47
+ only-packages = true
48
+
49
+ [tool.ruff]
50
+ line-length = 88
51
+ include = ["tests/*.py", "myconso/*.py"]
52
+
53
+ [tool.ruff.lint]
54
+ select = [
55
+ # flake8-async
56
+ "ASYNC",
57
+ # pycodestyle
58
+ "E",
59
+ # Pyflakes
60
+ "F",
61
+ # pyupgrade
62
+ "UP",
63
+ # flake8-bugbear
64
+ "B",
65
+ # flake8-simplify
66
+ "SIM",
67
+ # isort
68
+ "I",
69
+ # ruff
70
+ "RUF",
71
+ # flake8-quotes
72
+ "Q"
73
+ ]
74
+
75
+ [tool.mypy]
76
+ exclude = "tests/"
77
+ files = "myconso/*.py"