pgw-api 1.2.0__tar.gz → 1.2.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pgw-api
3
- Version: 1.2.0
3
+ Version: 1.2.1
4
4
  Summary: Python API client for Philadelphia Gas Works (PGW)
5
5
  Author: zackwag
6
6
  License: MIT
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pgw-api
3
- Version: 1.2.0
3
+ Version: 1.2.1
4
4
  Summary: Python API client for Philadelphia Gas Works (PGW)
5
5
  Author: zackwag
6
6
  License: MIT
@@ -10,6 +10,7 @@ pgw_api.egg-info/SOURCES.txt
10
10
  pgw_api.egg-info/dependency_links.txt
11
11
  pgw_api.egg-info/requires.txt
12
12
  pgw_api.egg-info/top_level.txt
13
+ tests/test_billing.py
13
14
  tests/test_client.py
14
15
  tests/test_models.py
15
16
  tests/test_parsing.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pgw-api"
7
- version = "1.2.0"
7
+ version = "1.2.1"
8
8
  description = "Python API client for Philadelphia Gas Works (PGW)"
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
@@ -0,0 +1,138 @@
1
+ """Tests for billing HTML parsing and the BillingSummary model."""
2
+
3
+ from datetime import date
4
+ from unittest.mock import AsyncMock, MagicMock
5
+
6
+ import pytest
7
+
8
+ from pgw_api.client import PGWApiClient
9
+ from pgw_api.models import BillingSummary
10
+ from tests.conftest import make_response, make_login_success, make_webmethod_response
11
+
12
+
13
+ BILLING_HTML = """
14
+ <input type="hidden" id="hdnTotalBillOFCurrentMonth" value="$120.50" />
15
+ <input type="hidden" id="hdnGasUsageOFCurrentMonth" value="85.0" />
16
+ <input type="hidden" id="hdnnumOfDaysCurrentMonth" value="32" />
17
+ <input type="hidden" id="hdnTotalBillOFPreviousMonth" value="$95.00" />
18
+ <input type="hidden" id="hdnGasUsageOFPreviousMonth" value="60.0" />
19
+ <input type="hidden" id="hdnnumOfDaysPreviousMonth" value="30" />
20
+ <input type="hidden" id="hdnTotalBillOFPreviousYearPreviousMonth" value="$110.00" />
21
+ <input type="hidden" id="hdnGasUsageOFPreviousYearPreviousMonth" value="80.0" />
22
+ <input type="hidden" id="hdnPrevAmount" value="$120.50" />
23
+ <input type="hidden" id="hdnbillComparisionOFCurrentMonth" value="[{&quot;Periodfrom&quot;:&quot;2024-01-15T00:00:00&quot;,&quot;PeriodTo&quot;:&quot;2024-02-14T00:00:00&quot;}]" />
24
+ """
25
+
26
+
27
+ def _setup_session(responses):
28
+ session = MagicMock()
29
+ idx = {"i": 0}
30
+
31
+ def _ctx(resp):
32
+ cm = MagicMock()
33
+ cm.__aenter__ = AsyncMock(return_value=resp)
34
+ cm.__aexit__ = AsyncMock(return_value=False)
35
+ return cm
36
+
37
+ def _next(*args, **kwargs):
38
+ r = responses[idx["i"]]
39
+ idx["i"] += 1
40
+ return _ctx(r)
41
+
42
+ session.get = MagicMock(side_effect=_next)
43
+ session.post = MagicMock(side_effect=_next)
44
+ return session
45
+
46
+
47
+ class TestBillingParsing:
48
+ @pytest.mark.asyncio
49
+ async def test_parses_all_fields(self):
50
+ session = _setup_session([
51
+ make_response(), # GET login page
52
+ make_response(text=make_login_success()), # POST validateLogin
53
+ make_response(), # GET Dashboard
54
+ make_response(text=BILLING_HTML), # GET BillDashboard
55
+ ])
56
+
57
+ client = PGWApiClient("user", "pass")
58
+ billing = await client.async_get_billing(session)
59
+
60
+ assert billing.current_bill == 120.50
61
+ assert billing.current_usage_ccf == 85.0
62
+ assert billing.current_period_days == 32
63
+ assert billing.previous_bill == 95.00
64
+ assert billing.previous_usage_ccf == 60.0
65
+ assert billing.previous_period_days == 30
66
+ assert billing.previous_year_bill == 110.00
67
+ assert billing.previous_year_usage_ccf == 80.0
68
+ assert billing.balance_due == 120.50
69
+ assert billing.period_start == date(2024, 1, 15)
70
+ assert billing.period_end == date(2024, 2, 14)
71
+
72
+ @pytest.mark.asyncio
73
+ async def test_missing_fields_default_to_zero(self):
74
+ html = "<html><body>no hidden fields here</body></html>"
75
+ session = _setup_session([
76
+ make_response(),
77
+ make_response(text=make_login_success()),
78
+ make_response(),
79
+ make_response(text=html),
80
+ ])
81
+
82
+ client = PGWApiClient("user", "pass")
83
+ billing = await client.async_get_billing(session)
84
+
85
+ assert billing.current_bill == 0.0
86
+ assert billing.current_usage_ccf == 0.0
87
+ assert billing.current_period_days == 0
88
+ assert billing.period_start is None
89
+ assert billing.period_end is None
90
+
91
+ @pytest.mark.asyncio
92
+ async def test_malformed_period_json(self):
93
+ html = '<input type="hidden" id="hdnbillComparisionOFCurrentMonth" value="not json" />'
94
+ session = _setup_session([
95
+ make_response(),
96
+ make_response(text=make_login_success()),
97
+ make_response(),
98
+ make_response(text=html),
99
+ ])
100
+
101
+ client = PGWApiClient("user", "pass")
102
+ billing = await client.async_get_billing(session)
103
+ assert billing.period_start is None
104
+ assert billing.period_end is None
105
+
106
+
107
+ class TestBillingSummaryModel:
108
+ def test_current_usage_cf(self):
109
+ b = BillingSummary(
110
+ current_bill=100, current_usage_ccf=50, current_period_days=30,
111
+ previous_bill=80, previous_usage_ccf=40, previous_period_days=30,
112
+ previous_year_bill=90, previous_year_usage_ccf=45, balance_due=100,
113
+ )
114
+ assert b.current_usage_cf == 5000.0
115
+
116
+ def test_previous_usage_cf(self):
117
+ b = BillingSummary(
118
+ current_bill=100, current_usage_ccf=50, current_period_days=30,
119
+ previous_bill=80, previous_usage_ccf=40, previous_period_days=30,
120
+ previous_year_bill=90, previous_year_usage_ccf=45, balance_due=100,
121
+ )
122
+ assert b.previous_usage_cf == 4000.0
123
+
124
+ def test_current_rate(self):
125
+ b = BillingSummary(
126
+ current_bill=200, current_usage_ccf=100, current_period_days=30,
127
+ previous_bill=80, previous_usage_ccf=40, previous_period_days=30,
128
+ previous_year_bill=90, previous_year_usage_ccf=45, balance_due=200,
129
+ )
130
+ assert b.current_rate == 2.0
131
+
132
+ def test_current_rate_zero_usage(self):
133
+ b = BillingSummary(
134
+ current_bill=0, current_usage_ccf=0, current_period_days=0,
135
+ previous_bill=0, previous_usage_ccf=0, previous_period_days=0,
136
+ previous_year_bill=0, previous_year_usage_ccf=0, balance_due=0,
137
+ )
138
+ assert b.current_rate is None
@@ -0,0 +1,388 @@
1
+ """Tests for PGWApiClient using mocked HTTP responses."""
2
+
3
+ import json
4
+ from datetime import date, datetime
5
+ from unittest.mock import AsyncMock, MagicMock
6
+
7
+ import pytest
8
+
9
+ from pgw_api.client import PGWApiClient
10
+ from pgw_api.exceptions import PGWAuthError, PGWConnectionError
11
+ from tests.conftest import CSRF_HTML, make_login_success, make_response, make_webmethod_response
12
+
13
+
14
+ def _setup_session(responses):
15
+ """Wire up a mock session with a sequence of responses."""
16
+ session = MagicMock()
17
+ idx = {"i": 0}
18
+
19
+ def _ctx(resp):
20
+ cm = MagicMock()
21
+ cm.__aenter__ = AsyncMock(return_value=resp)
22
+ cm.__aexit__ = AsyncMock(return_value=False)
23
+ return cm
24
+
25
+ def _next(*args, **kwargs):
26
+ r = responses[idx["i"]]
27
+ idx["i"] += 1
28
+ return _ctx(r)
29
+
30
+ session.get = MagicMock(side_effect=_next)
31
+ session.post = MagicMock(side_effect=_next)
32
+ return session
33
+
34
+
35
+ class TestMonthlyUsage:
36
+ @pytest.mark.asyncio
37
+ async def test_parses_monthly_entries(self):
38
+ usage_payload = make_webmethod_response({
39
+ "objUsageGenerationResultSetTwo": [
40
+ {"Month": 1, "Year": 2024, "UsageValue": 85.0, "FromDate": "12/15/23", "ToDate": "01/16/24"},
41
+ {"Month": 12, "Year": 2023, "UsageValue": 72.0, "FromDate": "11/14/23", "ToDate": "12/15/23"},
42
+ ]
43
+ })
44
+ session = _setup_session([
45
+ make_response(), # GET login page
46
+ make_response(text=make_login_success()), # POST validateLogin
47
+ make_response(), # GET Dashboard
48
+ make_response(text=CSRF_HTML), # GET usage page
49
+ make_response(text=usage_payload), # POST LoadGasUsage
50
+ ])
51
+
52
+ client = PGWApiClient("user", "pass")
53
+ result = await client.async_get_usage(session)
54
+
55
+ assert len(result) == 2
56
+ assert result[0].month == date(2024, 1, 1)
57
+ assert result[0].ccf == 85.0
58
+ assert result[1].month == date(2023, 12, 1)
59
+
60
+ @pytest.mark.asyncio
61
+ async def test_empty_usage(self):
62
+ usage_payload = make_webmethod_response({"objUsageGenerationResultSetTwo": []})
63
+ session = _setup_session([
64
+ make_response(),
65
+ make_response(text=make_login_success()),
66
+ make_response(),
67
+ make_response(text=CSRF_HTML),
68
+ make_response(text=usage_payload),
69
+ ])
70
+
71
+ client = PGWApiClient("user", "pass")
72
+ result = await client.async_get_usage(session)
73
+ assert result == []
74
+
75
+
76
+ class TestDailyUsage:
77
+ @pytest.mark.asyncio
78
+ async def test_parses_daily_entries(self):
79
+ usage_payload = make_webmethod_response({
80
+ "objUsageGenerationResultSetTwo": [
81
+ {"FromDate": "01/15/24", "UsageValue": 3.2},
82
+ {"FromDate": "01/14/24", "UsageValue": 4.1},
83
+ {"FromDate": "01/16/24", "UsageValue": 2.8},
84
+ ]
85
+ })
86
+ session = _setup_session([
87
+ make_response(),
88
+ make_response(text=make_login_success()),
89
+ make_response(),
90
+ make_response(text=CSRF_HTML),
91
+ make_response(text=usage_payload),
92
+ ])
93
+
94
+ client = PGWApiClient("user", "pass")
95
+ result = await client.async_get_daily_usage(
96
+ session, date(2024, 1, 14), date(2024, 1, 16)
97
+ )
98
+
99
+ assert len(result) == 3
100
+ assert result[0].date == date(2024, 1, 16)
101
+ assert result[0].ccf == 2.8
102
+ assert result[-1].date == date(2024, 1, 14)
103
+
104
+ @pytest.mark.asyncio
105
+ async def test_skips_entries_missing_date(self):
106
+ usage_payload = make_webmethod_response({
107
+ "objUsageGenerationResultSetTwo": [
108
+ {"FromDate": "01/15/24", "UsageValue": 3.2},
109
+ {"FromDate": None, "UsageValue": 4.1},
110
+ ]
111
+ })
112
+ session = _setup_session([
113
+ make_response(),
114
+ make_response(text=make_login_success()),
115
+ make_response(),
116
+ make_response(text=CSRF_HTML),
117
+ make_response(text=usage_payload),
118
+ ])
119
+
120
+ client = PGWApiClient("user", "pass")
121
+ result = await client.async_get_daily_usage(
122
+ session, date(2024, 1, 1), date(2024, 1, 31)
123
+ )
124
+ assert len(result) == 1
125
+
126
+
127
+ class TestHourlyUsage:
128
+ @pytest.mark.asyncio
129
+ async def test_parses_hourly_entries(self):
130
+ usage_payload = make_webmethod_response({
131
+ "objUsageGenerationResultSetTwo": [
132
+ {"FromDate": "01/15/2024 01:00:00 AM", "UsageValue": 0.3},
133
+ {"FromDate": "01/15/2024 02:00:00 AM", "UsageValue": 0.4},
134
+ {"FromDate": "01/15/2024 12:00:00 PM", "UsageValue": 0.1},
135
+ ]
136
+ })
137
+ session = _setup_session([
138
+ make_response(),
139
+ make_response(text=make_login_success()),
140
+ make_response(),
141
+ make_response(text=CSRF_HTML),
142
+ make_response(text=usage_payload),
143
+ ])
144
+
145
+ client = PGWApiClient("user", "pass")
146
+ result = await client.async_get_hourly_usage(session, date(2024, 1, 15))
147
+
148
+ assert len(result) == 3
149
+ assert result[0].timestamp == datetime(2024, 1, 15, 12, 0, 0)
150
+ assert result[0].ccf == 0.1
151
+ assert result[-1].timestamp == datetime(2024, 1, 15, 1, 0, 0)
152
+
153
+ @pytest.mark.asyncio
154
+ async def test_empty_hourly(self):
155
+ usage_payload = make_webmethod_response({"objUsageGenerationResultSetTwo": []})
156
+ session = _setup_session([
157
+ make_response(),
158
+ make_response(text=make_login_success()),
159
+ make_response(),
160
+ make_response(text=CSRF_HTML),
161
+ make_response(text=usage_payload),
162
+ ])
163
+
164
+ client = PGWApiClient("user", "pass")
165
+ result = await client.async_get_hourly_usage(session, date(2024, 1, 15))
166
+ assert result == []
167
+
168
+
169
+ class TestGetAll:
170
+ @pytest.mark.asyncio
171
+ async def test_returns_usage_and_billing(self):
172
+ billing_html = """
173
+ <input type="hidden" id="hdnTotalBillOFCurrentMonth" value="$120.50" />
174
+ <input type="hidden" id="hdnGasUsageOFCurrentMonth" value="85.0" />
175
+ <input type="hidden" id="hdnnumOfDaysCurrentMonth" value="32" />
176
+ <input type="hidden" id="hdnTotalBillOFPreviousMonth" value="$95.00" />
177
+ <input type="hidden" id="hdnGasUsageOFPreviousMonth" value="60.0" />
178
+ <input type="hidden" id="hdnnumOfDaysPreviousMonth" value="30" />
179
+ <input type="hidden" id="hdnTotalBillOFPreviousYearPreviousMonth" value="$110.00" />
180
+ <input type="hidden" id="hdnGasUsageOFPreviousYearPreviousMonth" value="80.0" />
181
+ <input type="hidden" id="hdnPrevAmount" value="$120.50" />
182
+ """
183
+ usage_payload = make_webmethod_response({
184
+ "objUsageGenerationResultSetTwo": [
185
+ {"Month": 1, "Year": 2024, "UsageValue": 85.0, "FromDate": "12/15/23", "ToDate": "01/16/24"},
186
+ ]
187
+ })
188
+ session = _setup_session([
189
+ make_response(), # GET login page
190
+ make_response(text=make_login_success()), # POST validateLogin
191
+ make_response(), # GET Dashboard (billing)
192
+ make_response(text=billing_html), # GET BillDashboard
193
+ make_response(), # GET Dashboard (usage)
194
+ make_response(text=CSRF_HTML), # GET usage page
195
+ make_response(text=usage_payload), # POST LoadGasUsage
196
+ ])
197
+
198
+ client = PGWApiClient("user", "pass")
199
+ usage, billing = await client.async_get_all(session)
200
+
201
+ assert len(usage) == 1
202
+ assert usage[0].ccf == 85.0
203
+ assert billing.current_bill == 120.50
204
+ assert billing.balance_due == 120.50
205
+
206
+
207
+ class TestValidateCredentials:
208
+ @pytest.mark.asyncio
209
+ async def test_returns_true_on_success(self):
210
+ session = _setup_session([
211
+ make_response(),
212
+ make_response(text=make_login_success()),
213
+ ])
214
+
215
+ client = PGWApiClient("user", "pass")
216
+ assert await client.async_validate_credentials(session) is True
217
+
218
+
219
+ class TestErrorHandling:
220
+ @pytest.mark.asyncio
221
+ async def test_csrf_error_raises_auth_error(self):
222
+ error_payload = make_webmethod_response({
223
+ "dtException": [{"MessageInformation": "Invalid CSRF Token"}]
224
+ })
225
+ session = _setup_session([
226
+ make_response(),
227
+ make_response(text=make_login_success()),
228
+ make_response(),
229
+ make_response(text=CSRF_HTML),
230
+ make_response(text=error_payload),
231
+ ])
232
+
233
+ client = PGWApiClient("user", "pass")
234
+ with pytest.raises(PGWAuthError, match="CSRF"):
235
+ await client.async_get_usage(session)
236
+
237
+ @pytest.mark.asyncio
238
+ async def test_bad_credentials(self):
239
+ login_fail = make_webmethod_response(
240
+ [{"STATUS": 0, "Message": "Invalid username or password"}]
241
+ )
242
+ session = _setup_session([
243
+ make_response(),
244
+ make_response(text=login_fail),
245
+ ])
246
+
247
+ client = PGWApiClient("user", "wrong")
248
+ with pytest.raises(PGWAuthError, match="Invalid username"):
249
+ await client.async_get_usage(session)
250
+
251
+ @pytest.mark.asyncio
252
+ async def test_login_exception_response(self):
253
+ login_err = make_webmethod_response(
254
+ {"dtException": [{"MessageInformation": "Account locked"}]}
255
+ )
256
+ session = _setup_session([
257
+ make_response(),
258
+ make_response(text=login_err),
259
+ ])
260
+
261
+ client = PGWApiClient("user", "pass")
262
+ with pytest.raises(PGWAuthError, match="Account locked"):
263
+ await client.async_get_usage(session)
264
+
265
+ @pytest.mark.asyncio
266
+ async def test_login_empty_response(self):
267
+ login_empty = make_webmethod_response([])
268
+ session = _setup_session([
269
+ make_response(),
270
+ make_response(text=login_empty),
271
+ ])
272
+
273
+ client = PGWApiClient("user", "pass")
274
+ with pytest.raises(PGWAuthError, match="unexpected response"):
275
+ await client.async_get_usage(session)
276
+
277
+ @pytest.mark.asyncio
278
+ async def test_login_non_200(self):
279
+ session = _setup_session([
280
+ make_response(),
281
+ make_response(status=500),
282
+ ])
283
+
284
+ client = PGWApiClient("user", "pass")
285
+ with pytest.raises(PGWAuthError, match="status 500"):
286
+ await client.async_get_usage(session)
287
+
288
+ @pytest.mark.asyncio
289
+ async def test_login_page_non_200(self):
290
+ session = _setup_session([
291
+ make_response(status=503),
292
+ ])
293
+
294
+ client = PGWApiClient("user", "pass")
295
+ with pytest.raises(PGWConnectionError, match="status 503"):
296
+ await client.async_get_usage(session)
297
+
298
+ @pytest.mark.asyncio
299
+ async def test_missing_csrf_token(self):
300
+ session = _setup_session([
301
+ make_response(),
302
+ make_response(text=make_login_success()),
303
+ make_response(),
304
+ make_response(text="<html>no token here</html>"),
305
+ ])
306
+
307
+ client = PGWApiClient("user", "pass")
308
+ with pytest.raises(PGWConnectionError, match="CSRF token"):
309
+ await client.async_get_usage(session)
310
+
311
+ @pytest.mark.asyncio
312
+ async def test_usage_page_non_200(self):
313
+ session = _setup_session([
314
+ make_response(),
315
+ make_response(text=make_login_success()),
316
+ make_response(),
317
+ make_response(status=500),
318
+ ])
319
+
320
+ client = PGWApiClient("user", "pass")
321
+ with pytest.raises(PGWConnectionError, match="status 500"):
322
+ await client.async_get_usage(session)
323
+
324
+ @pytest.mark.asyncio
325
+ async def test_load_usage_non_200(self):
326
+ session = _setup_session([
327
+ make_response(),
328
+ make_response(text=make_login_success()),
329
+ make_response(),
330
+ make_response(text=CSRF_HTML),
331
+ make_response(status=500),
332
+ ])
333
+
334
+ client = PGWApiClient("user", "pass")
335
+ with pytest.raises(PGWConnectionError, match="status 500"):
336
+ await client.async_get_usage(session)
337
+
338
+ @pytest.mark.asyncio
339
+ async def test_load_usage_malformed_json(self):
340
+ session = _setup_session([
341
+ make_response(),
342
+ make_response(text=make_login_success()),
343
+ make_response(),
344
+ make_response(text=CSRF_HTML),
345
+ make_response(text="not json"),
346
+ ])
347
+
348
+ client = PGWApiClient("user", "pass")
349
+ with pytest.raises(PGWConnectionError, match="Unexpected response"):
350
+ await client.async_get_usage(session)
351
+
352
+ @pytest.mark.asyncio
353
+ async def test_general_exception_in_usage(self):
354
+ error_payload = make_webmethod_response({
355
+ "dtException": [{"MessageInformation": "Something went wrong"}]
356
+ })
357
+ session = _setup_session([
358
+ make_response(),
359
+ make_response(text=make_login_success()),
360
+ make_response(),
361
+ make_response(text=CSRF_HTML),
362
+ make_response(text=error_payload),
363
+ ])
364
+
365
+ client = PGWApiClient("user", "pass")
366
+ with pytest.raises(PGWConnectionError, match="Something went wrong"):
367
+ await client.async_get_usage(session)
368
+
369
+ @pytest.mark.asyncio
370
+ async def test_skips_monthly_entries_missing_fields(self):
371
+ usage_payload = make_webmethod_response({
372
+ "objUsageGenerationResultSetTwo": [
373
+ {"Month": 1, "Year": 2024, "UsageValue": 85.0, "FromDate": "12/15/23", "ToDate": "01/16/24"},
374
+ {"Month": None, "Year": 2024, "UsageValue": 50.0},
375
+ {"Month": 2, "Year": None, "UsageValue": 50.0},
376
+ ]
377
+ })
378
+ session = _setup_session([
379
+ make_response(),
380
+ make_response(text=make_login_success()),
381
+ make_response(),
382
+ make_response(text=CSRF_HTML),
383
+ make_response(text=usage_payload),
384
+ ])
385
+
386
+ client = PGWApiClient("user", "pass")
387
+ result = await client.async_get_usage(session)
388
+ assert len(result) == 1
@@ -1,199 +0,0 @@
1
- """Tests for PGWApiClient using mocked HTTP responses."""
2
-
3
- import json
4
- from datetime import date, datetime
5
- from unittest.mock import AsyncMock, MagicMock
6
-
7
- import pytest
8
-
9
- from pgw_api.client import PGWApiClient
10
- from pgw_api.exceptions import PGWAuthError, PGWConnectionError
11
- from tests.conftest import CSRF_HTML, make_login_success, make_response, make_webmethod_response
12
-
13
-
14
- def _setup_session(responses):
15
- """Wire up a mock session with a sequence of responses."""
16
- session = MagicMock()
17
- idx = {"i": 0}
18
-
19
- def _ctx(resp):
20
- cm = MagicMock()
21
- cm.__aenter__ = AsyncMock(return_value=resp)
22
- cm.__aexit__ = AsyncMock(return_value=False)
23
- return cm
24
-
25
- def _next(*args, **kwargs):
26
- r = responses[idx["i"]]
27
- idx["i"] += 1
28
- return _ctx(r)
29
-
30
- session.get = MagicMock(side_effect=_next)
31
- session.post = MagicMock(side_effect=_next)
32
- return session
33
-
34
-
35
- class TestMonthlyUsage:
36
- @pytest.mark.asyncio
37
- async def test_parses_monthly_entries(self):
38
- usage_payload = make_webmethod_response({
39
- "objUsageGenerationResultSetTwo": [
40
- {"Month": 1, "Year": 2024, "UsageValue": 85.0, "FromDate": "12/15/23", "ToDate": "01/16/24"},
41
- {"Month": 12, "Year": 2023, "UsageValue": 72.0, "FromDate": "11/14/23", "ToDate": "12/15/23"},
42
- ]
43
- })
44
- session = _setup_session([
45
- make_response(), # GET login page
46
- make_response(text=make_login_success()), # POST validateLogin
47
- make_response(), # GET Dashboard
48
- make_response(text=CSRF_HTML), # GET usage page
49
- make_response(text=usage_payload), # POST LoadGasUsage
50
- ])
51
-
52
- client = PGWApiClient("user", "pass")
53
- result = await client.async_get_usage(session)
54
-
55
- assert len(result) == 2
56
- assert result[0].month == date(2024, 1, 1)
57
- assert result[0].ccf == 85.0
58
- assert result[1].month == date(2023, 12, 1)
59
-
60
- @pytest.mark.asyncio
61
- async def test_empty_usage(self):
62
- usage_payload = make_webmethod_response({"objUsageGenerationResultSetTwo": []})
63
- session = _setup_session([
64
- make_response(),
65
- make_response(text=make_login_success()),
66
- make_response(),
67
- make_response(text=CSRF_HTML),
68
- make_response(text=usage_payload),
69
- ])
70
-
71
- client = PGWApiClient("user", "pass")
72
- result = await client.async_get_usage(session)
73
- assert result == []
74
-
75
-
76
- class TestDailyUsage:
77
- @pytest.mark.asyncio
78
- async def test_parses_daily_entries(self):
79
- usage_payload = make_webmethod_response({
80
- "objUsageGenerationResultSetTwo": [
81
- {"FromDate": "01/15/24", "UsageValue": 3.2},
82
- {"FromDate": "01/14/24", "UsageValue": 4.1},
83
- {"FromDate": "01/16/24", "UsageValue": 2.8},
84
- ]
85
- })
86
- session = _setup_session([
87
- make_response(),
88
- make_response(text=make_login_success()),
89
- make_response(),
90
- make_response(text=CSRF_HTML),
91
- make_response(text=usage_payload),
92
- ])
93
-
94
- client = PGWApiClient("user", "pass")
95
- result = await client.async_get_daily_usage(
96
- session, date(2024, 1, 14), date(2024, 1, 16)
97
- )
98
-
99
- assert len(result) == 3
100
- assert result[0].date == date(2024, 1, 16)
101
- assert result[0].ccf == 2.8
102
- assert result[-1].date == date(2024, 1, 14)
103
-
104
- @pytest.mark.asyncio
105
- async def test_skips_entries_missing_date(self):
106
- usage_payload = make_webmethod_response({
107
- "objUsageGenerationResultSetTwo": [
108
- {"FromDate": "01/15/24", "UsageValue": 3.2},
109
- {"FromDate": None, "UsageValue": 4.1},
110
- ]
111
- })
112
- session = _setup_session([
113
- make_response(),
114
- make_response(text=make_login_success()),
115
- make_response(),
116
- make_response(text=CSRF_HTML),
117
- make_response(text=usage_payload),
118
- ])
119
-
120
- client = PGWApiClient("user", "pass")
121
- result = await client.async_get_daily_usage(
122
- session, date(2024, 1, 1), date(2024, 1, 31)
123
- )
124
- assert len(result) == 1
125
-
126
-
127
- class TestHourlyUsage:
128
- @pytest.mark.asyncio
129
- async def test_parses_hourly_entries(self):
130
- usage_payload = make_webmethod_response({
131
- "objUsageGenerationResultSetTwo": [
132
- {"FromDate": "01/15/2024 01:00:00 AM", "UsageValue": 0.3},
133
- {"FromDate": "01/15/2024 02:00:00 AM", "UsageValue": 0.4},
134
- {"FromDate": "01/15/2024 12:00:00 PM", "UsageValue": 0.1},
135
- ]
136
- })
137
- session = _setup_session([
138
- make_response(),
139
- make_response(text=make_login_success()),
140
- make_response(),
141
- make_response(text=CSRF_HTML),
142
- make_response(text=usage_payload),
143
- ])
144
-
145
- client = PGWApiClient("user", "pass")
146
- result = await client.async_get_hourly_usage(session, date(2024, 1, 15))
147
-
148
- assert len(result) == 3
149
- assert result[0].timestamp == datetime(2024, 1, 15, 12, 0, 0)
150
- assert result[0].ccf == 0.1
151
- assert result[-1].timestamp == datetime(2024, 1, 15, 1, 0, 0)
152
-
153
- @pytest.mark.asyncio
154
- async def test_empty_hourly(self):
155
- usage_payload = make_webmethod_response({"objUsageGenerationResultSetTwo": []})
156
- session = _setup_session([
157
- make_response(),
158
- make_response(text=make_login_success()),
159
- make_response(),
160
- make_response(text=CSRF_HTML),
161
- make_response(text=usage_payload),
162
- ])
163
-
164
- client = PGWApiClient("user", "pass")
165
- result = await client.async_get_hourly_usage(session, date(2024, 1, 15))
166
- assert result == []
167
-
168
-
169
- class TestErrorHandling:
170
- @pytest.mark.asyncio
171
- async def test_csrf_error_raises_auth_error(self):
172
- error_payload = make_webmethod_response({
173
- "dtException": [{"MessageInformation": "Invalid CSRF Token"}]
174
- })
175
- session = _setup_session([
176
- make_response(),
177
- make_response(text=make_login_success()),
178
- make_response(),
179
- make_response(text=CSRF_HTML),
180
- make_response(text=error_payload),
181
- ])
182
-
183
- client = PGWApiClient("user", "pass")
184
- with pytest.raises(PGWAuthError, match="CSRF"):
185
- await client.async_get_usage(session)
186
-
187
- @pytest.mark.asyncio
188
- async def test_bad_credentials(self):
189
- login_fail = make_webmethod_response(
190
- [{"STATUS": 0, "Message": "Invalid username or password"}]
191
- )
192
- session = _setup_session([
193
- make_response(),
194
- make_response(text=login_fail),
195
- ])
196
-
197
- client = PGWApiClient("user", "wrong")
198
- with pytest.raises(PGWAuthError, match="Invalid username"):
199
- await client.async_get_usage(session)
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes