bwt-api 1.0.2__tar.gz → 1.0.3__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 (51) hide show
  1. {bwt_api-1.0.2 → bwt_api-1.0.3}/PKG-INFO +1 -1
  2. {bwt_api-1.0.2 → bwt_api-1.0.3}/src/bwt_api/data.py +79 -5
  3. {bwt_api-1.0.2 → bwt_api-1.0.3}/src/bwt_api/smart_dos_api.py +37 -10
  4. {bwt_api-1.0.2 → bwt_api-1.0.3}/src/bwt_api.egg-info/PKG-INFO +1 -1
  5. {bwt_api-1.0.2 → bwt_api-1.0.3}/src/bwt_api.egg-info/SOURCES.txt +14 -4
  6. {bwt_api-1.0.2 → bwt_api-1.0.3}/src/bwt_api.egg-info/scm_file_list.json +30 -21
  7. bwt_api-1.0.3/src/bwt_api.egg-info/scm_version.json +8 -0
  8. bwt_api-1.0.3/tests/data/perla/GetCurrentData.json +33 -0
  9. bwt_api-1.0.3/tests/data/perla/GetDailyData.json +50 -0
  10. bwt_api-1.0.3/tests/data/perla/GetMonthlyData.json +33 -0
  11. bwt_api-1.0.3/tests/data/perla/GetYearlyData.json +14 -0
  12. bwt_api-1.0.3/tests/data/silk/registers.json +3 -0
  13. bwt_api-1.0.3/tests/data/smartdos/gatt_0104.json +13 -0
  14. bwt_api-1.0.3/tests/data/smartdos/gatt_0201.json +17 -0
  15. bwt_api-1.0.3/tests/data/smartdos/gatt_0202.json +24 -0
  16. bwt_api-1.0.3/tests/data/smartdos/gatt_0208.json +4 -0
  17. bwt_api-1.0.3/tests/data/smartdos/gatt_0401.json +8 -0
  18. bwt_api-1.0.3/tests/data/smartdos/gatt_0402.json +8 -0
  19. bwt_api-1.0.3/tests/data/smartdos/gatt_0503.json +12 -0
  20. bwt_api-1.0.3/tests/data/smartdos/gatt_0505.json +3 -0
  21. {bwt_api-1.0.2 → bwt_api-1.0.3}/tests/test_api.py +101 -5
  22. bwt_api-1.0.2/AUTHORS.rst +0 -5
  23. bwt_api-1.0.2/CHANGELOG.rst +0 -8
  24. bwt_api-1.0.2/CONTRIBUTING.rst +0 -278
  25. bwt_api-1.0.2/src/bwt_api.egg-info/scm_version.json +0 -8
  26. {bwt_api-1.0.2 → bwt_api-1.0.3}/.coveragerc +0 -0
  27. {bwt_api-1.0.2 → bwt_api-1.0.3}/.devcontainer/devcontainer.json +0 -0
  28. {bwt_api-1.0.2 → bwt_api-1.0.3}/.github/workflows/python-package.yml +0 -0
  29. {bwt_api-1.0.2 → bwt_api-1.0.3}/.github/workflows/python-publish.yml +0 -0
  30. {bwt_api-1.0.2 → bwt_api-1.0.3}/.gitignore +0 -0
  31. {bwt_api-1.0.2 → bwt_api-1.0.3}/LICENSE.txt +0 -0
  32. {bwt_api-1.0.2 → bwt_api-1.0.3}/README.rst +0 -0
  33. {bwt_api-1.0.2 → bwt_api-1.0.3}/pyproject.toml +0 -0
  34. {bwt_api-1.0.2 → bwt_api-1.0.3}/setup.cfg +0 -0
  35. {bwt_api-1.0.2 → bwt_api-1.0.3}/setup.py +0 -0
  36. {bwt_api-1.0.2 → bwt_api-1.0.3}/src/bwt_api/__init__.py +0 -0
  37. {bwt_api-1.0.2 → bwt_api-1.0.3}/src/bwt_api/api.py +0 -0
  38. {bwt_api-1.0.2 → bwt_api-1.0.3}/src/bwt_api/bwt.py +0 -0
  39. {bwt_api-1.0.2 → bwt_api-1.0.3}/src/bwt_api/bwt_api.py +0 -0
  40. {bwt_api-1.0.2 → bwt_api-1.0.3}/src/bwt_api/error.py +0 -0
  41. {bwt_api-1.0.2 → bwt_api-1.0.3}/src/bwt_api/exception.py +0 -0
  42. {bwt_api-1.0.2 → bwt_api-1.0.3}/src/bwt_api/silk_api.py +0 -0
  43. {bwt_api-1.0.2 → bwt_api-1.0.3}/src/bwt_api/skeleton.py +0 -0
  44. {bwt_api-1.0.2 → bwt_api-1.0.3}/src/bwt_api.egg-info/dependency_links.txt +0 -0
  45. {bwt_api-1.0.2 → bwt_api-1.0.3}/src/bwt_api.egg-info/entry_points.txt +0 -0
  46. {bwt_api-1.0.2 → bwt_api-1.0.3}/src/bwt_api.egg-info/not-zip-safe +0 -0
  47. {bwt_api-1.0.2 → bwt_api-1.0.3}/src/bwt_api.egg-info/requires.txt +0 -0
  48. {bwt_api-1.0.2 → bwt_api-1.0.3}/src/bwt_api.egg-info/top_level.txt +0 -0
  49. {bwt_api-1.0.2 → bwt_api-1.0.3}/tests/conftest.py +0 -0
  50. {bwt_api-1.0.2 → bwt_api-1.0.3}/tests/test_distinguish.py +0 -0
  51. {bwt_api-1.0.2 → bwt_api-1.0.3}/tox.ini +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bwt_api
3
- Version: 1.0.2
3
+ Version: 1.0.3
4
4
  Summary: Python API to access the local BWT Perla API.
5
5
  Home-page: https://github.com/dkarv/bwt_api
6
6
  Author: dkarv
@@ -33,6 +33,56 @@ class SmartDosStatus(enum.IntEnum):
33
33
  def _missing_(cls, value):
34
34
  return None
35
35
 
36
+ @classmethod
37
+ def normal(cls) -> list["SmartDosStatus"]:
38
+ """Return SmartDosStatus values considered normal/OK."""
39
+ return [
40
+ cls.STANDBY,
41
+ cls.METERING_ACTIVE,
42
+ ]
43
+
44
+ @classmethod
45
+ def warning(cls) -> list["SmartDosStatus"]:
46
+ """Return SmartDosStatus values that indicate warnings."""
47
+ return [
48
+ cls.MINERAL_CONTAINER_LEVEL_LOW,
49
+ cls.SMART_MINERAL_RUNNING_LOW,
50
+ ]
51
+
52
+ @classmethod
53
+ def error(cls) -> list["SmartDosStatus"]:
54
+ """Return SmartDosStatus values that indicate errors."""
55
+ return [
56
+ cls.MINERAL_CONTAINER_EMPTY,
57
+ cls.PUMP_FAULT,
58
+ cls.PUMP_POWER_FAULT,
59
+ cls.SMART_MINERAL_RUN_OUT,
60
+ cls.AQA_VOLUME_ALARM,
61
+ cls.AQA_WATCH_ALARM,
62
+ cls.AQA_MAXFLOW_ALARM,
63
+ cls.PUMP_CONTROL_FAULT,
64
+ ]
65
+
66
+ @classmethod
67
+ def category(cls, value) -> BwtStatus | None:
68
+ """Return the `BwtStatus` category for a SmartDosStatus value.
69
+
70
+ Accepts either an int or a SmartDosStatus member. Returns `BwtStatus.OK`,
71
+ `BwtStatus.WARNING`, or `BwtStatus.ERROR`, or `None` for unknown values.
72
+ """
73
+ try:
74
+ status = cls(value)
75
+ except Exception:
76
+ return None
77
+
78
+ if status in cls.normal():
79
+ return BwtStatus.OK
80
+ if status in cls.warning():
81
+ return BwtStatus.WARNING
82
+ if status in cls.error():
83
+ return BwtStatus.ERROR
84
+ return None
85
+
36
86
 
37
87
  class SubstanceType(enum.IntEnum):
38
88
  L1_LE = 1
@@ -68,7 +118,7 @@ class CurrentResponse:
68
118
  holiday_mode: int # -1 or 0: inactive, 1: active, unix timestamp: start in future
69
119
  regeneration_last_1: datetime
70
120
  regeneration_last_2: datetime
71
- service_customer: datetime
121
+ service_customer: datetime
72
122
  service_technician: datetime
73
123
  out_of_service: int
74
124
  regeneration_count_1: int
@@ -81,7 +131,7 @@ class CurrentResponse:
81
131
  treated_day: int # treated water current day
82
132
  treated_month: int # treated water current month
83
133
  treated_year: int # treated water current year
84
- columns: int # number of columns: 2 for BWT Duo and 1 for BWT Perla One
134
+ columns: int # number of columns: 2 for BWT Duo and 1 for BWT Perla One
85
135
 
86
136
 
87
137
  @dataclass
@@ -91,11 +141,13 @@ class DailyResponse:
91
141
  # [1] = 00:30 - 00:59
92
142
  values: list[int]
93
143
 
144
+
94
145
  @dataclass
95
146
  class MonthlyResponse:
96
147
  # treated water in 1 day intervals
97
148
  values: list[int]
98
149
 
150
+
99
151
  @dataclass
100
152
  class YearlyResponse:
101
153
  # treated water in 1 month intervals
@@ -109,6 +161,15 @@ class WifiResponse:
109
161
  """UUID 0104: Wi-Fi Information"""
110
162
  ssid: str # Wi-Fi network name
111
163
  rssi: int # Wi-Fi signal strength in dBm
164
+ rssiAvg: str # Average Wi-Fi signal strength in dBm
165
+ rssiSig: str # Signal strength standard deviation in dBm
166
+ dhcp: bool # DHCP enabled
167
+ ip: str | None # IP address
168
+ sn: str | None # Serial number
169
+ sg: str | None # Subnet mask
170
+ pDns: str | None # Primary DNS
171
+ sDns: str | None # Secondary DNS
172
+ mac: str | None # MAC address
112
173
 
113
174
 
114
175
  @dataclass
@@ -122,6 +183,11 @@ class DeviceInfoResponse:
122
183
  dev_state: SmartDosStatus | None # Current device status
123
184
  active_states: list[SmartDosStatus | None] # List of active device statuses
124
185
  comm_date: str # Start-up date (ISO format)
186
+ device_id: str # Device ID
187
+ device_type: str # Device type
188
+ device_variant: str # Device variant
189
+ total_flow: int # Total treated water in ml
190
+ total_dosed: int # Total dosed substance in ml
125
191
 
126
192
 
127
193
  @dataclass
@@ -133,6 +199,12 @@ class ConfigurationResponse:
133
199
  aqa_watch_en: bool # AQA Watch active
134
200
  aqa_max_flow_en: bool # AQA MaxFlow active
135
201
  aqa_volume_val: float # AQA Volume value (l)
202
+ volume_per_stroke: float # Volume per stroke (ml)
203
+ pouch_empty_timeout: int # Pouch empty timeout (s)
204
+ pouch_not_empty_timeout: int # Pouch not empty timeout (s)
205
+ aqa_watch_val: int # AQA Watch value (s)
206
+ aqa_max_flow_val: float # AQA MaxFlow value (l/h)
207
+ rest_server_en: bool # REST server active
136
208
 
137
209
 
138
210
  @dataclass
@@ -156,15 +228,17 @@ class PouchInfoResponse:
156
228
  @dataclass
157
229
  class RemainingCapacityResponse:
158
230
  """UUID 0402: Remaining Capacity"""
159
- rem_capacity: Optional[float] # Remaining volume in ml
160
- rem_capacity_pct: Optional[float] # Remaining volume in %
161
- rem_capacity_days: Optional[int] # Remaining volume in days
231
+ rem_capacity: float # Remaining volume in ml
232
+ rem_capacity_pct: float # Remaining volume in %
233
+ rem_capacity_days: int # Remaining volume in days
234
+ unit: int # Unit (0 = ml)
162
235
 
163
236
 
164
237
  @dataclass
165
238
  class TreatedWaterResponse:
166
239
  """UUID 0503: Treated Water"""
167
240
  total_flow: int # Total treated water in ml
241
+ total_ticks: int # Total treated water ticks??
168
242
 
169
243
 
170
244
  @dataclass
@@ -1,5 +1,7 @@
1
1
  """The BWT Smart Dos API class."""
2
2
 
3
+ from collections.abc import Mapping
4
+
3
5
  import aiohttp
4
6
  import logging
5
7
  from typing import Any
@@ -61,7 +63,19 @@ class BwtSmartDosApi:
61
63
  """UUID 0104: Get Wi-Fi name and signal strength."""
62
64
  self._logger.debug("Fetching Wi-Fi info from %s", self._host)
63
65
  raw = await self._get_gatt("0104")
64
- return WifiResponse(ssid=raw["ssid"], rssi=raw["rssi"])
66
+ return WifiResponse(
67
+ ssid=raw["ssid"],
68
+ rssi=raw["rssi"],
69
+ rssiAvg=raw["rssiAvg"],
70
+ rssiSig=raw["rssiSig"],
71
+ dhcp=raw["dhcp"],
72
+ ip=raw["ip"],
73
+ sn=raw["sn"],
74
+ sg=raw["sg"],
75
+ pDns=raw["pDns"],
76
+ sDns=raw["sDns"],
77
+ mac=raw["mac"]
78
+ )
65
79
 
66
80
  async def get_device_info(self) -> DeviceInfoResponse:
67
81
  """UUID 0201: Get device information."""
@@ -71,11 +85,16 @@ class BwtSmartDosApi:
71
85
  fw_rev=raw["fwRev"],
72
86
  hw_rev=raw["hwRev"],
73
87
  product_code=raw["productCode"],
88
+ device_id=raw["iotDevId"],
89
+ device_type=raw["iotDevType"],
90
+ device_variant=raw["iotDevVariant"],
74
91
  uptime=raw["uptime"],
75
92
  operating_time=raw["operatingTime"],
76
93
  dev_state=SmartDosStatus(raw["devState"]),
77
94
  active_states=[SmartDosStatus(state) for state in raw["activeStates"]],
78
95
  comm_date=raw["commDate"],
96
+ total_flow=raw["lifeTimeFlow_ml"],
97
+ total_dosed=raw["lifeTimeDosed_ml"],
79
98
  )
80
99
 
81
100
  async def get_configuration(self) -> ConfigurationResponse:
@@ -85,10 +104,16 @@ class BwtSmartDosApi:
85
104
  return ConfigurationResponse(
86
105
  buzzer_en=raw["buzzerEn"],
87
106
  dosing_rate=raw["dosingRate"],
107
+ volume_per_stroke=raw["volumePerStroke"],
108
+ pouch_empty_timeout=raw["pouchEmptyTimeout"],
109
+ pouch_not_empty_timeout=raw["pouchNotEmptyTimeout"],
88
110
  aqa_volume_en=raw["aqaVolumeEn"],
89
111
  aqa_watch_en=raw["aqaWatchEn"],
90
112
  aqa_max_flow_en=raw["aqaMaxFlowEn"],
91
113
  aqa_volume_val=raw["aqaVolumeVal"],
114
+ aqa_watch_val=raw["aqaWatchVal"],
115
+ aqa_max_flow_val=raw["aqaMaxFlowVal"],
116
+ rest_server_en=raw["restServerEn"],
92
117
  )
93
118
 
94
119
  async def get_time_info(self) -> TimeResponse:
@@ -110,22 +135,24 @@ class BwtSmartDosApi:
110
135
  unit=raw["unit"],
111
136
  )
112
137
 
113
- async def get_remaining_capacity(self) -> RemainingCapacityResponse:
138
+ async def get_remaining_capacity(self) -> Mapping[int, RemainingCapacityResponse]:
114
139
  """UUID 0402: Get remaining capacity information."""
115
140
  self._logger.debug("Fetching remaining capacity from %s", self._host)
116
141
  raw = await self._get_gatt("0402")
117
- return RemainingCapacityResponse(
118
- rem_capacity=raw.get("remCapacity"),
119
- rem_capacity_pct=raw.get("remCapacityPct"),
120
- rem_capacity_days=raw.get("remCapacityDays"),
121
- )
142
+ return {int(k): RemainingCapacityResponse(
143
+ rem_capacity=v["remCapacity"],
144
+ rem_capacity_pct=v["remCapacityPct"],
145
+ rem_capacity_days=v["remCapacityDays"],
146
+ unit=v["unit"]) for k, v in raw.items()}
122
147
 
123
- async def get_treated_water(self) -> TreatedWaterResponse:
148
+ async def get_treated_water(self) -> Mapping[int, TreatedWaterResponse]:
124
149
  """UUID 0503: Get treated water information."""
125
150
  self._logger.debug("Fetching treated water from %s", self._host)
126
151
  raw = await self._get_gatt("0503")
127
- total_flow = raw["flow"]["1"]["totFlow"]
128
- return TreatedWaterResponse(total_flow=total_flow)
152
+ return {int(k): TreatedWaterResponse(
153
+ total_flow=v["totFlow"],
154
+ total_ticks=v["totTicks"],
155
+ ) for k, v in raw["flow"].items()}
129
156
 
130
157
  async def get_substance_dosage(self) -> SubstanceDosageResponse:
131
158
  """UUID 0505: Get substance dosage information."""
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bwt_api
3
- Version: 1.0.2
3
+ Version: 1.0.3
4
4
  Summary: Python API to access the local BWT Perla API.
5
5
  Home-page: https://github.com/dkarv/bwt_api
6
6
  Author: dkarv
@@ -1,8 +1,5 @@
1
1
  .coveragerc
2
2
  .gitignore
3
- AUTHORS.rst
4
- CHANGELOG.rst
5
- CONTRIBUTING.rst
6
3
  LICENSE.txt
7
4
  README.rst
8
5
  pyproject.toml
@@ -33,4 +30,17 @@ src/bwt_api.egg-info/scm_version.json
33
30
  src/bwt_api.egg-info/top_level.txt
34
31
  tests/conftest.py
35
32
  tests/test_api.py
36
- tests/test_distinguish.py
33
+ tests/test_distinguish.py
34
+ tests/data/perla/GetCurrentData.json
35
+ tests/data/perla/GetDailyData.json
36
+ tests/data/perla/GetMonthlyData.json
37
+ tests/data/perla/GetYearlyData.json
38
+ tests/data/silk/registers.json
39
+ tests/data/smartdos/gatt_0104.json
40
+ tests/data/smartdos/gatt_0201.json
41
+ tests/data/smartdos/gatt_0202.json
42
+ tests/data/smartdos/gatt_0208.json
43
+ tests/data/smartdos/gatt_0401.json
44
+ tests/data/smartdos/gatt_0402.json
45
+ tests/data/smartdos/gatt_0503.json
46
+ tests/data/smartdos/gatt_0505.json
@@ -1,32 +1,41 @@
1
1
  {
2
2
  "files": [
3
- ".gitignore",
4
- "AUTHORS.rst",
5
- "LICENSE.txt",
6
- "CHANGELOG.rst",
7
3
  "tox.ini",
8
- "pyproject.toml",
9
- ".coveragerc",
4
+ "LICENSE.txt",
5
+ "README.rst",
10
6
  "setup.py",
11
- "CONTRIBUTING.rst",
7
+ "pyproject.toml",
12
8
  "setup.cfg",
13
- "README.rst",
14
- "src/bwt_api/bwt_api",
15
- "src/bwt_api/api.py",
16
- "src/bwt_api/exception.py",
17
- "src/bwt_api/bwt.py",
18
- "src/bwt_api/data.py",
19
- "src/bwt_api/smart_dos_api.py",
20
- "src/bwt_api/bwt_api.py",
21
- "src/bwt_api/silk_api.py",
22
- "src/bwt_api/error.py",
23
- "src/bwt_api/skeleton.py",
24
- "src/bwt_api/__init__.py",
25
- ".devcontainer/devcontainer.json",
9
+ ".gitignore",
10
+ ".coveragerc",
26
11
  ".github/workflows/python-publish.yml",
27
12
  ".github/workflows/python-package.yml",
13
+ ".devcontainer/devcontainer.json",
14
+ "tests/test_api.py",
28
15
  "tests/test_distinguish.py",
29
16
  "tests/conftest.py",
30
- "tests/test_api.py"
17
+ "tests/data/silk/registers.json",
18
+ "tests/data/smartdos/gatt_0202.json",
19
+ "tests/data/smartdos/gatt_0208.json",
20
+ "tests/data/smartdos/gatt_0401.json",
21
+ "tests/data/smartdos/gatt_0104.json",
22
+ "tests/data/smartdos/gatt_0503.json",
23
+ "tests/data/smartdos/gatt_0505.json",
24
+ "tests/data/smartdos/gatt_0402.json",
25
+ "tests/data/smartdos/gatt_0201.json",
26
+ "tests/data/perla/GetMonthlyData.json",
27
+ "tests/data/perla/GetDailyData.json",
28
+ "tests/data/perla/GetCurrentData.json",
29
+ "tests/data/perla/GetYearlyData.json",
30
+ "src/bwt_api/data.py",
31
+ "src/bwt_api/smart_dos_api.py",
32
+ "src/bwt_api/exception.py",
33
+ "src/bwt_api/__init__.py",
34
+ "src/bwt_api/bwt.py",
35
+ "src/bwt_api/api.py",
36
+ "src/bwt_api/error.py",
37
+ "src/bwt_api/bwt_api.py",
38
+ "src/bwt_api/skeleton.py",
39
+ "src/bwt_api/silk_api.py"
31
40
  ]
32
41
  }
@@ -0,0 +1,8 @@
1
+ {
2
+ "tag": "1.0.3",
3
+ "distance": 0,
4
+ "node": "g59008971f6a96776cdf5cac562c45f27f148eec7",
5
+ "dirty": false,
6
+ "branch": "HEAD",
7
+ "node_date": "2026-08-04"
8
+ }
@@ -0,0 +1,33 @@
1
+ {
2
+ "ActiveErrorIDs": "",
3
+ "BlendedWaterSinceSetup_l": 123456,
4
+ "CapacityColumn1_ml_dH": 500000,
5
+ "CapacityColumn2_ml_dH": -1,
6
+ "CurrentFlowrate_l_h": 100,
7
+ "DosingSinceSetup_ml": 2500,
8
+ "FirmwareVersion": "1.2.3",
9
+ "HardnessIN_CaCO3": 200,
10
+ "HardnessIN_dH": 11,
11
+ "HardnessIN_fH": 19,
12
+ "HardnessIN_mmol_l": 2.0,
13
+ "HardnessOUT_CaCO3": 20,
14
+ "HardnessOUT_dH": 1,
15
+ "HardnessOUT_fH": 2,
16
+ "HardnessOUT_mmol_l": 0.2,
17
+ "HolidayModeStartTime": "",
18
+ "LastRegenerationColumn1": "2023-07-01 12:00:00",
19
+ "LastRegenerationColumn2": "2023-07-02 12:00:00",
20
+ "LastServiceCustomer": "2024-01-01 09:00:00",
21
+ "LastServiceTechnican": "2024-02-01 10:00:00",
22
+ "OutOfService": 0,
23
+ "RegenerationCounterColumn1": 5,
24
+ "RegenerationCounterColumn2": 0,
25
+ "RegenerationCountSinceSetup": 10,
26
+ "RegenerativLevel": 80,
27
+ "RegenerativRemainingDays": 100,
28
+ "RegenerativSinceSetup_g": 12345,
29
+ "ShowError": 0,
30
+ "WaterTreatedCurrentDay_l": 1234,
31
+ "WaterTreatedCurrentMonth_l": 5678,
32
+ "WaterTreatedCurrentYear_l": 23456
33
+ }
@@ -0,0 +1,50 @@
1
+ {
2
+ "0000_0029_l": 0,
3
+ "0030_0059_l": 1,
4
+ "0100_0129_l": 2,
5
+ "0130_0159_l": 3,
6
+ "0200_0229_l": 4,
7
+ "0230_0259_l": 5,
8
+ "0300_0329_l": 6,
9
+ "0330_0359_l": 7,
10
+ "0400_0429_l": 8,
11
+ "0430_0459_l": 9,
12
+ "0500_0529_l": 10,
13
+ "0530_0559_l": 11,
14
+ "0600_0629_l": 12,
15
+ "0630_0659_l": 13,
16
+ "0700_0729_l": 14,
17
+ "0730_0759_l": 15,
18
+ "0800_0829_l": 16,
19
+ "0830_0859_l": 17,
20
+ "0900_0929_l": 18,
21
+ "0930_0959_l": 19,
22
+ "1000_1029_l": 20,
23
+ "1030_1059_l": 21,
24
+ "1100_1129_l": 22,
25
+ "1130_1159_l": 23,
26
+ "1200_1229_l": 24,
27
+ "1230_1259_l": 25,
28
+ "1300_1329_l": 26,
29
+ "1330_1359_l": 27,
30
+ "1400_1429_l": 28,
31
+ "1430_1459_l": 29,
32
+ "1500_1529_l": 30,
33
+ "1530_1559_l": 31,
34
+ "1600_1629_l": 32,
35
+ "1630_1659_l": 33,
36
+ "1700_1729_l": 34,
37
+ "1730_1759_l": 35,
38
+ "1800_1829_l": 36,
39
+ "1830_1859_l": 37,
40
+ "1900_1929_l": 38,
41
+ "1930_1959_l": 39,
42
+ "2000_2029_l": 40,
43
+ "2030_2059_l": 41,
44
+ "2100_2129_l": 42,
45
+ "2130_2159_l": 43,
46
+ "2200_2229_l": 44,
47
+ "2230_2259_l": 45,
48
+ "2300_2329_l": 46,
49
+ "2330_2359_l": 47
50
+ }
@@ -0,0 +1,33 @@
1
+ {
2
+ "Day01_l": 1,
3
+ "Day02_l": 2,
4
+ "Day03_l": 3,
5
+ "Day04_l": 4,
6
+ "Day05_l": 5,
7
+ "Day06_l": 6,
8
+ "Day07_l": 7,
9
+ "Day08_l": 8,
10
+ "Day09_l": 9,
11
+ "Day10_l": 10,
12
+ "Day11_l": 11,
13
+ "Day12_l": 12,
14
+ "Day13_l": 13,
15
+ "Day14_l": 14,
16
+ "Day15_l": 15,
17
+ "Day16_l": 16,
18
+ "Day17_l": 17,
19
+ "Day18_l": 18,
20
+ "Day19_l": 19,
21
+ "Day20_l": 20,
22
+ "Day21_l": 21,
23
+ "Day22_l": 22,
24
+ "Day23_l": 23,
25
+ "Day24_l": 24,
26
+ "Day25_l": 25,
27
+ "Day26_l": 26,
28
+ "Day27_l": 27,
29
+ "Day28_l": 28,
30
+ "Day29_l": 29,
31
+ "Day30_l": 30,
32
+ "Day31_l": 31
33
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "Month01_l": 100,
3
+ "Month02_l": 110,
4
+ "Month03_l": 120,
5
+ "Month04_l": 130,
6
+ "Month05_l": 140,
7
+ "Month06_l": 150,
8
+ "Month07_l": 160,
9
+ "Month08_l": 170,
10
+ "Month09_l": 180,
11
+ "Month10_l": 190,
12
+ "Month11_l": 200,
13
+ "Month12_l": 210
14
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "params": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
3
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "ssid": "Graf WLAN",
3
+ "rssi": null,
4
+ "rssiAvg": "-54.00",
5
+ "rssiSig": "0.00",
6
+ "dhcp": true,
7
+ "ip": null,
8
+ "sn": null,
9
+ "sg": null,
10
+ "pDns": null,
11
+ "sDns": null,
12
+ "mac": "AA:BB:CC:DD:EE:FF"
13
+ }
@@ -0,0 +1,17 @@
1
+ {
2
+ "fwRev": "1.2.0",
3
+ "hwRev": "2.4.0(A)",
4
+ "productCode": "3HZR-1R37",
5
+ "iotDevId": "5b29b9e9-24c5-4eea-a599-927922a89f5a",
6
+ "iotDevType": "bwt_bewados",
7
+ "iotDevVariant": "dev",
8
+ "uptime": 6510811,
9
+ "operatingTime": 40773211,
10
+ "devState": 2001,
11
+ "activeStates": [
12
+ 2001
13
+ ],
14
+ "commDate": "2026-01-04T10:01:05.891Z",
15
+ "lifeTimeFlow_ml": 112507432,
16
+ "lifeTimeDosed_ml": 2746.1572265625
17
+ }
@@ -0,0 +1,24 @@
1
+ {
2
+ "buzzerEn": false,
3
+ "dosingRate": 10,
4
+ "volumePerStroke": 0.053784001618623734,
5
+ "pouchEmptyTimeout": 7200,
6
+ "pouchNotEmptyTimeout": 20,
7
+ "aqaVolumeEn": false,
8
+ "aqaWatchEn": false,
9
+ "aqaMaxFlowEn": false,
10
+ "aqaVolumeVal": 9999,
11
+ "aqaWatchVal": 0,
12
+ "aqaMaxFlowVal": 7,
13
+ "restServerEn": true,
14
+ "defaults": {
15
+ "consumable": {
16
+ "totCap": 1500
17
+ },
18
+ "aqaAlert": {
19
+ "aqaVolumeVal": 9999,
20
+ "aqaWatchVal": 60,
21
+ "aqaMaxFlowVal": 7
22
+ }
23
+ }
24
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "time": "2026-07-14 12:00:00",
3
+ "timezone": "UTC"
4
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "totCap": 1000,
3
+ "expDate": "2027-01-01",
4
+ "orderNr": "ORD123",
5
+ "batchNr": "BATCH1",
6
+ "id": 1,
7
+ "unit": "ml"
8
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "1": {
3
+ "remCapacity": 1451.1103515625,
4
+ "remCapacityPct": 97,
5
+ "remCapacityDays": 354,
6
+ "unit": 0
7
+ }
8
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "flow": {
3
+ "1": {
4
+ "totFlow": 112507432,
5
+ "totTicks": 2587671
6
+ },
7
+ "2": {
8
+ "totFlow": 20244130,
9
+ "totTicks": 465615
10
+ }
11
+ }
12
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "dosedMineral": 2746.1572265625
3
+ }
@@ -1,6 +1,7 @@
1
1
  from datetime import datetime
2
2
  from zoneinfo import ZoneInfo
3
3
  from unittest.mock import Mock
4
+ from pathlib import Path
4
5
  import pytest
5
6
  from aiohttp.client_reqrep import ClientResponse
6
7
 
@@ -101,6 +102,11 @@ current_json_empty_errors = """
101
102
  }
102
103
  """
103
104
 
105
+
106
+ def load_json_str(name: str) -> str:
107
+ """Load a JSON file from tests/data/smartdos and return its text."""
108
+ return (Path(__file__).parent / "data" / "smartdos" / name).read_text()
109
+
104
110
  current_json_perla_one = """
105
111
  {
106
112
  "ActiveErrorIDs" : "",
@@ -300,17 +306,33 @@ def test_unknown_error_no_mutation():
300
306
  # Different unknown codes produce different instances
301
307
  assert err1 is not err2
302
308
 
309
+ async def test_smartdos_get_wifi_info():
310
+ with aioresponses() as mocked:
311
+ mocked.get(
312
+ "http://host:80/api/v1/gatt/0104",
313
+ status=200,
314
+ body=load_json_str("gatt_0104.json"),
315
+ headers={"Content-Type": "application/json"},
316
+ )
317
+ async with BwtSmartDosApi("host") as api:
318
+ result = await api.get_wifi_info()
319
+ assert result.ssid == "Graf WLAN"
320
+ assert result.rssiAvg == "-54.00"
321
+ assert result.mac == "AA:BB:CC:DD:EE:FF"
322
+
323
+
303
324
  async def test_smartdos_get_gatt_0201():
304
325
  with aioresponses() as mocked:
305
326
  mocked.get(
306
327
  "http://host:80/api/v1/gatt/0201",
307
328
  status=200,
308
- body='{"characteristic":"0201","value":[1,2,3]}',
329
+ body=load_json_str("gatt_0201.json"),
309
330
  headers={"Content-Type": "application/json"},
310
331
  )
311
332
  async with BwtSmartDosApi("host") as api:
312
333
  result = await api.get_gatt_0201()
313
- assert result == {"characteristic": "0201", "value": [1, 2, 3]}
334
+ assert result["fwRev"] == "1.2.0"
335
+ assert result["productCode"] == "3HZR-1R37"
314
336
 
315
337
 
316
338
  async def test_smartdos_unknown_response():
@@ -321,12 +343,41 @@ async def test_smartdos_unknown_response():
321
343
  await api.get_gatt_0201()
322
344
 
323
345
 
346
+ async def test_smartdos_get_configuration():
347
+ with aioresponses() as mocked:
348
+ mocked.get(
349
+ "http://host:80/api/v1/gatt/0202",
350
+ status=200,
351
+ body=load_json_str("gatt_0202.json"),
352
+ headers={"Content-Type": "application/json"},
353
+ )
354
+ async with BwtSmartDosApi("host") as api:
355
+ result = await api.get_configuration()
356
+ assert result.buzzer_en is False
357
+ assert result.dosing_rate == 10
358
+ assert result.rest_server_en is True
359
+
360
+
361
+ async def test_smartdos_get_time_info():
362
+ with aioresponses() as mocked:
363
+ mocked.get(
364
+ "http://host:80/api/v1/gatt/0208",
365
+ status=200,
366
+ body=load_json_str("gatt_0208.json"),
367
+ headers={"Content-Type": "application/json"},
368
+ )
369
+ async with BwtSmartDosApi("host") as api:
370
+ result = await api.get_time_info()
371
+ assert result.time == "2026-07-14 12:00:00"
372
+ assert result.timezone == "UTC"
373
+
374
+
324
375
  async def test_smartdos_device_info_parses_status_values():
325
376
  with aioresponses() as mocked:
326
377
  mocked.get(
327
378
  "http://host:80/api/v1/gatt/0201",
328
379
  status=200,
329
- body='{"fwRev":"1.1.0+4","hwRev":"2.4.0(B)","productCode":"8R19-CX2A","uptime":4889,"operatingTime":706889,"devState":2001,"activeStates":[2001],"commDate":"2024-12-04T13:45:38.833Z"}',
380
+ body='{"fwRev":"1.2.0","hwRev":"2.4.0(A)","productCode":"3HZR-1R37","iotDevId":"5b29b9e9-24c5-4eea-a599-927922a89f5a","iotDevType":"bwt_bewados","iotDevVariant":"dev","uptime":6510811,"operatingTime":40773211,"devState":2001,"activeStates":[2001],"commDate":"2026-01-04T10:01:05.891Z","lifeTimeFlow_ml":112507432,"lifeTimeDosed_ml":2746.1572265625}',
330
381
  headers={"Content-Type": "application/json"},
331
382
  )
332
383
  async with BwtSmartDosApi("host") as api:
@@ -335,17 +386,62 @@ async def test_smartdos_device_info_parses_status_values():
335
386
  assert result.active_states == [SmartDosStatus.STANDBY]
336
387
 
337
388
 
389
+ async def test_smartdos_get_remaining_capacity():
390
+ with aioresponses() as mocked:
391
+ mocked.get(
392
+ "http://host:80/api/v1/gatt/0402",
393
+ status=200,
394
+ body=load_json_str("gatt_0402.json"),
395
+ headers={"Content-Type": "application/json"},
396
+ )
397
+ async with BwtSmartDosApi("host") as api:
398
+ result = await api.get_remaining_capacity()
399
+ assert 1 in result
400
+ assert result[1].rem_capacity == 1451.1103515625
401
+ assert result[1].rem_capacity_pct == 97
402
+
403
+
404
+ async def test_smartdos_get_treated_water():
405
+ with aioresponses() as mocked:
406
+ mocked.get(
407
+ "http://host:80/api/v1/gatt/0503",
408
+ status=200,
409
+ body=load_json_str("gatt_0503.json"),
410
+ headers={"Content-Type": "application/json"},
411
+ )
412
+ async with BwtSmartDosApi("host") as api:
413
+ result = await api.get_treated_water()
414
+ assert 1 in result
415
+ assert result[1].total_flow == 112507432
416
+ assert 2 in result
417
+ assert result[2].total_flow == 20244130
418
+
419
+
420
+ async def test_smartdos_get_substance_dosage():
421
+ with aioresponses() as mocked:
422
+ mocked.get(
423
+ "http://host:80/api/v1/gatt/0505",
424
+ status=200,
425
+ body=load_json_str("gatt_0505.json"),
426
+ headers={"Content-Type": "application/json"},
427
+ )
428
+ async with BwtSmartDosApi("host") as api:
429
+ result = await api.get_substance_dosage()
430
+ assert result.dosed_mineral == 2746.1572265625
431
+
432
+
338
433
  async def test_smartdos_pouch_info_parses_substance_type():
339
434
  with aioresponses() as mocked:
340
435
  mocked.get(
341
436
  "http://host:80/api/v1/gatt/0401",
342
437
  status=200,
343
- body='{"totCap":10000,"expDate":"05.12.2025","orderNr":125123456,"batchNr":12345,"id":2,"unit":0}',
438
+ body=load_json_str("gatt_0401.json"),
344
439
  headers={"Content-Type": "application/json"},
345
440
  )
346
441
  async with BwtSmartDosApi("host") as api:
347
442
  result = await api.get_pouch_info()
348
- assert result.substance_type == SubstanceType.L2_L3
443
+ assert result.substance_type == SubstanceType.L1_LE
444
+ assert result.tot_cap == 1000
349
445
 
350
446
  def test_treated_to_blended():
351
447
  assert treated_to_blended(0, 21, 4) == 0
bwt_api-1.0.2/AUTHORS.rst DELETED
@@ -1,5 +0,0 @@
1
- ============
2
- Contributors
3
- ============
4
-
5
- * dkarv <punzet.david@gmail.com>
@@ -1,8 +0,0 @@
1
- =========
2
- Changelog
3
- =========
4
-
5
- Version 0.2.1
6
- =============
7
-
8
- - Initial release
@@ -1,278 +0,0 @@
1
- ============
2
- Contributing
3
- ============
4
-
5
- Welcome to ``bwt_api`` contributor's guide.
6
-
7
- This document focuses on getting any potential contributor familiarized
8
- with the development processes, but `other kinds of contributions`_ are also
9
- appreciated.
10
-
11
- If you are new to using git_ or have never collaborated in a project previously,
12
- please have a look at `contribution-guide.org`_. Other resources are also
13
- listed in the excellent `guide created by FreeCodeCamp`_ [#contrib1]_.
14
-
15
- Please notice, all users and contributors are expected to be **open,
16
- considerate, reasonable, and respectful**. When in doubt, `Python Software
17
- Foundation's Code of Conduct`_ is a good reference in terms of behavior
18
- guidelines.
19
-
20
-
21
- Issue Reports
22
- =============
23
-
24
- If you experience bugs or general issues with ``bwt_api``, please have a look
25
- on the `issue tracker`_. If you don't see anything useful there, please feel
26
- free to fire an issue report.
27
-
28
- .. tip::
29
- Please don't forget to include the closed issues in your search.
30
- Sometimes a solution was already reported, and the problem is considered
31
- **solved**.
32
-
33
- New issue reports should include information about your programming environment
34
- (e.g., operating system, Python version) and steps to reproduce the problem.
35
- Please try also to simplify the reproduction steps to a very minimal example
36
- that still illustrates the problem you are facing. By removing other factors,
37
- you help us to identify the root cause of the issue.
38
-
39
-
40
- Documentation Improvements
41
- ==========================
42
-
43
- You can help improve ``bwt_api`` docs by making them more readable and coherent, or
44
- by adding missing information and correcting mistakes.
45
-
46
- .. tip::
47
- Please notice that the `GitHub web interface`_ provides a quick way of
48
- propose changes in ``bwt_api``'s files. While this mechanism can
49
- be tricky for normal code contributions, it works perfectly fine for
50
- contributing to the docs, and can be quite handy.
51
-
52
- If you are interested in trying this method out, please navigate to
53
- the ``docs`` folder in the source repository_, find which file you
54
- would like to propose changes and click in the little pencil icon at the
55
- top, to open `GitHub's code editor`_. Once you finish editing the file,
56
- please write a message in the form at the bottom of the page describing
57
- which changes have you made and what are the motivations behind them and
58
- submit your proposal.
59
-
60
-
61
- Code Contributions
62
- ==================
63
-
64
- Submit an issue
65
- ---------------
66
-
67
- Before you work on any non-trivial code contribution it's best to first create
68
- a report in the `issue tracker`_ to start a discussion on the subject.
69
- This often provides additional considerations and avoids unnecessary work.
70
-
71
- Create an environment
72
- ---------------------
73
-
74
- Before you start coding, we recommend creating an isolated `virtual
75
- environment`_ to avoid any problems with your installed Python packages.
76
- This can easily be done via either |virtualenv|_::
77
-
78
- virtualenv <PATH TO VENV>
79
- source <PATH TO VENV>/bin/activate
80
-
81
- or Miniconda_::
82
-
83
- conda create -n bwt_api python=3 six virtualenv pytest pytest-cov
84
- conda activate bwt_api
85
-
86
- Clone the repository
87
- --------------------
88
-
89
- #. Create an user account on |the repository service| if you do not already have one.
90
- #. Fork the project repository_: click on the *Fork* button near the top of the
91
- page. This creates a copy of the code under your account on |the repository service|.
92
- #. Clone this copy to your local disk::
93
-
94
- git clone git@github.com:YourLogin/bwt_api.git
95
- cd bwt_api
96
-
97
- #. You should run::
98
-
99
- pip install -U pip setuptools -e .
100
-
101
- to be able to import the package under development in the Python REPL.
102
-
103
-
104
- Implement your changes
105
- ----------------------
106
-
107
- #. Create a branch to hold your changes::
108
-
109
- git checkout -b my-feature
110
-
111
- and start making changes. Never work on the main branch!
112
-
113
- #. Start your work on this branch. Don't forget to add docstrings_ to new
114
- functions, modules and classes, especially if they are part of public APIs.
115
-
116
- #. Add yourself to the list of contributors in ``AUTHORS.rst``.
117
-
118
- #. When you’re done editing, do::
119
-
120
- git add <MODIFIED FILES>
121
- git commit
122
-
123
- to record your changes in git_.
124
-
125
- .. important:: Don't forget to add unit tests and documentation in case your
126
- contribution adds an additional feature and is not just a bugfix.
127
-
128
- Moreover, writing a `descriptive commit message`_ is highly recommended.
129
- In case of doubt, you can check the commit history with::
130
-
131
- git log --graph --decorate --pretty=oneline --abbrev-commit --all
132
-
133
- to look for recurring communication patterns.
134
-
135
- #. Please check that your changes don't break any unit tests with::
136
-
137
- tox
138
-
139
- (after having installed |tox|_ with ``pip install tox`` or ``pipx``).
140
-
141
- You can also use |tox|_ to run several other pre-configured tasks in the
142
- repository. Try ``tox -av`` to see a list of the available checks.
143
-
144
- Submit your contribution
145
- ------------------------
146
-
147
- #. If everything works fine, push your local branch to |the repository service| with::
148
-
149
- git push -u origin my-feature
150
-
151
- #. Go to the web page of your fork and click |contribute button|
152
- to send your changes for review.
153
-
154
- Find more detailed information in `creating a PR`_. You might also want to open
155
- the PR as a draft first and mark it as ready for review after the feedbacks
156
- from the continuous integration (CI) system or any required fixes.
157
-
158
-
159
- Troubleshooting
160
- ---------------
161
-
162
- The following tips can be used when facing problems to build or test the
163
- package:
164
-
165
- #. Make sure to fetch all the tags from the upstream repository_.
166
- The command ``git describe --abbrev=0 --tags`` should return the version you
167
- are expecting. If you are trying to run CI scripts in a fork repository,
168
- make sure to push all the tags.
169
- You can also try to remove all the egg files or the complete egg folder, i.e.,
170
- ``.eggs``, as well as the ``*.egg-info`` folders in the ``src`` folder or
171
- potentially in the root of your project.
172
-
173
- #. Sometimes |tox|_ misses out when new dependencies are added, especially to
174
- ``setup.cfg`` and ``docs/requirements.txt``. If you find any problems with
175
- missing dependencies when running a command with |tox|_, try to recreate the
176
- ``tox`` environment using the ``-r`` flag. For example, instead of::
177
-
178
- tox -e docs
179
-
180
- Try running::
181
-
182
- tox -r -e docs
183
-
184
- #. Make sure to have a reliable |tox|_ installation that uses the correct
185
- Python version (e.g., 3.7+). When in doubt you can run::
186
-
187
- tox --version
188
- # OR
189
- which tox
190
-
191
- If you have trouble and are seeing weird errors upon running |tox|_, you can
192
- also try to create a dedicated `virtual environment`_ with a |tox|_ binary
193
- freshly installed. For example::
194
-
195
- virtualenv .venv
196
- source .venv/bin/activate
197
- .venv/bin/pip install tox
198
- .venv/bin/tox -e all
199
-
200
- #. `Pytest can drop you`_ in an interactive session in the case an error occurs.
201
- In order to do that you need to pass a ``--pdb`` option (for example by
202
- running ``tox -- -k <NAME OF THE FALLING TEST> --pdb``).
203
- You can also setup breakpoints manually instead of using the ``--pdb`` option.
204
-
205
-
206
- Maintainer tasks
207
- ================
208
-
209
- Releases
210
- --------
211
-
212
- If you are part of the group of maintainers and have correct user permissions
213
- on PyPI_, the following steps can be used to release a new version for
214
- ``bwt_api``:
215
-
216
- #. Make sure all unit tests are successful.
217
- #. Tag the current commit on the main branch with a release tag, e.g., ``v1.2.3``.
218
- #. Push the new tag to the upstream repository_, e.g., ``git push upstream v1.2.3``
219
- #. Clean up the ``dist`` and ``build`` folders with ``tox -e clean``
220
- (or ``rm -rf dist build``)
221
- to avoid confusion with old builds and Sphinx docs.
222
- #. Run ``tox -e build`` and check that the files in ``dist`` have
223
- the correct version (no ``.dirty`` or git_ hash) according to the git_ tag.
224
- Also check the sizes of the distributions, if they are too big (e.g., >
225
- 500KB), unwanted clutter may have been accidentally included.
226
- #. Run ``tox -e publish -- --repository pypi`` and check that everything was
227
- uploaded to PyPI_ correctly.
228
-
229
-
230
-
231
- .. [#contrib1] Even though, these resources focus on open source projects and
232
- communities, the general ideas behind collaborating with other developers
233
- to collectively create software are general and can be applied to all sorts
234
- of environments, including private companies and proprietary code bases.
235
-
236
-
237
- .. <-- start -->
238
-
239
- .. |the repository service| replace:: GitHub
240
- .. |contribute button| replace:: "Create pull request"
241
-
242
- .. _repository: https://github.com/dkarv/bwt_api
243
- .. _issue tracker: https://github.com/dkarv/bwt_api/issues
244
- .. <-- end -->
245
-
246
-
247
- .. |virtualenv| replace:: ``virtualenv``
248
- .. |pre-commit| replace:: ``pre-commit``
249
- .. |tox| replace:: ``tox``
250
-
251
-
252
- .. _black: https://pypi.org/project/black/
253
- .. _CommonMark: https://commonmark.org/
254
- .. _contribution-guide.org: https://www.contribution-guide.org/
255
- .. _creating a PR: https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request
256
- .. _descriptive commit message: https://chris.beams.io/posts/git-commit
257
- .. _docstrings: https://www.sphinx-doc.org/en/master/usage/extensions/napoleon.html
258
- .. _first-contributions tutorial: https://github.com/firstcontributions/first-contributions
259
- .. _flake8: https://flake8.pycqa.org/en/stable/
260
- .. _git: https://git-scm.com
261
- .. _GitHub's fork and pull request workflow: https://guides.github.com/activities/forking/
262
- .. _guide created by FreeCodeCamp: https://github.com/FreeCodeCamp/how-to-contribute-to-open-source
263
- .. _Miniconda: https://docs.conda.io/en/latest/miniconda.html
264
- .. _MyST: https://myst-parser.readthedocs.io/en/latest/syntax/syntax.html
265
- .. _other kinds of contributions: https://opensource.guide/how-to-contribute
266
- .. _pre-commit: https://pre-commit.com/
267
- .. _PyPI: https://pypi.org/
268
- .. _PyScaffold's contributor's guide: https://pyscaffold.org/en/stable/contributing.html
269
- .. _Pytest can drop you: https://docs.pytest.org/en/stable/how-to/failures.html#using-python-library-pdb-with-pytest
270
- .. _Python Software Foundation's Code of Conduct: https://www.python.org/psf/conduct/
271
- .. _reStructuredText: https://www.sphinx-doc.org/en/master/usage/restructuredtext/
272
- .. _Sphinx: https://www.sphinx-doc.org/en/master/
273
- .. _tox: https://tox.wiki/en/stable/
274
- .. _virtual environment: https://realpython.com/python-virtual-environments-a-primer/
275
- .. _virtualenv: https://virtualenv.pypa.io/en/stable/
276
-
277
- .. _GitHub web interface: https://docs.github.com/en/repositories/working-with-files/managing-files/editing-files
278
- .. _GitHub's code editor: https://docs.github.com/en/repositories/working-with-files/managing-files/editing-files
@@ -1,8 +0,0 @@
1
- {
2
- "tag": "1.0.2",
3
- "distance": 0,
4
- "node": "g5bd1108162a356c6ba843801c305aa396bb2b6f4",
5
- "dirty": false,
6
- "branch": "HEAD",
7
- "node_date": "2026-07-18"
8
- }
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
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes