bwt-api 1.0.0__tar.gz → 1.0.2__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 (38) hide show
  1. {bwt_api-1.0.0 → bwt_api-1.0.2}/PKG-INFO +1 -1
  2. {bwt_api-1.0.0 → bwt_api-1.0.2}/src/bwt_api/bwt.py +8 -7
  3. {bwt_api-1.0.0 → bwt_api-1.0.2}/src/bwt_api/bwt_api.py +14 -13
  4. {bwt_api-1.0.0 → bwt_api-1.0.2}/src/bwt_api/data.py +4 -3
  5. {bwt_api-1.0.0 → bwt_api-1.0.2}/src/bwt_api/silk_api.py +8 -7
  6. {bwt_api-1.0.0 → bwt_api-1.0.2}/src/bwt_api/skeleton.py +0 -2
  7. {bwt_api-1.0.0 → bwt_api-1.0.2}/src/bwt_api/smart_dos_api.py +19 -18
  8. {bwt_api-1.0.0 → bwt_api-1.0.2}/src/bwt_api.egg-info/PKG-INFO +1 -1
  9. {bwt_api-1.0.0 → bwt_api-1.0.2}/src/bwt_api.egg-info/scm_file_list.json +19 -19
  10. bwt_api-1.0.2/src/bwt_api.egg-info/scm_version.json +8 -0
  11. bwt_api-1.0.0/src/bwt_api.egg-info/scm_version.json +0 -8
  12. {bwt_api-1.0.0 → bwt_api-1.0.2}/.coveragerc +0 -0
  13. {bwt_api-1.0.0 → bwt_api-1.0.2}/.devcontainer/devcontainer.json +0 -0
  14. {bwt_api-1.0.0 → bwt_api-1.0.2}/.github/workflows/python-package.yml +0 -0
  15. {bwt_api-1.0.0 → bwt_api-1.0.2}/.github/workflows/python-publish.yml +0 -0
  16. {bwt_api-1.0.0 → bwt_api-1.0.2}/.gitignore +0 -0
  17. {bwt_api-1.0.0 → bwt_api-1.0.2}/AUTHORS.rst +0 -0
  18. {bwt_api-1.0.0 → bwt_api-1.0.2}/CHANGELOG.rst +0 -0
  19. {bwt_api-1.0.0 → bwt_api-1.0.2}/CONTRIBUTING.rst +0 -0
  20. {bwt_api-1.0.0 → bwt_api-1.0.2}/LICENSE.txt +0 -0
  21. {bwt_api-1.0.0 → bwt_api-1.0.2}/README.rst +0 -0
  22. {bwt_api-1.0.0 → bwt_api-1.0.2}/pyproject.toml +0 -0
  23. {bwt_api-1.0.0 → bwt_api-1.0.2}/setup.cfg +0 -0
  24. {bwt_api-1.0.0 → bwt_api-1.0.2}/setup.py +0 -0
  25. {bwt_api-1.0.0 → bwt_api-1.0.2}/src/bwt_api/__init__.py +0 -0
  26. {bwt_api-1.0.0 → bwt_api-1.0.2}/src/bwt_api/api.py +0 -0
  27. {bwt_api-1.0.0 → bwt_api-1.0.2}/src/bwt_api/error.py +0 -0
  28. {bwt_api-1.0.0 → bwt_api-1.0.2}/src/bwt_api/exception.py +0 -0
  29. {bwt_api-1.0.0 → bwt_api-1.0.2}/src/bwt_api.egg-info/SOURCES.txt +0 -0
  30. {bwt_api-1.0.0 → bwt_api-1.0.2}/src/bwt_api.egg-info/dependency_links.txt +0 -0
  31. {bwt_api-1.0.0 → bwt_api-1.0.2}/src/bwt_api.egg-info/entry_points.txt +0 -0
  32. {bwt_api-1.0.0 → bwt_api-1.0.2}/src/bwt_api.egg-info/not-zip-safe +0 -0
  33. {bwt_api-1.0.0 → bwt_api-1.0.2}/src/bwt_api.egg-info/requires.txt +0 -0
  34. {bwt_api-1.0.0 → bwt_api-1.0.2}/src/bwt_api.egg-info/top_level.txt +0 -0
  35. {bwt_api-1.0.0 → bwt_api-1.0.2}/tests/conftest.py +0 -0
  36. {bwt_api-1.0.0 → bwt_api-1.0.2}/tests/test_api.py +0 -0
  37. {bwt_api-1.0.0 → bwt_api-1.0.2}/tests/test_distinguish.py +0 -0
  38. {bwt_api-1.0.0 → bwt_api-1.0.2}/tox.ini +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bwt_api
3
- Version: 1.0.0
3
+ Version: 1.0.2
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
@@ -7,18 +7,16 @@ import logging
7
7
 
8
8
  from bwt_api.exception import ConnectException
9
9
 
10
- _logger = logging.getLogger(__name__)
11
-
12
10
  class BwtModel(Enum):
13
11
  """Enum for BWT models."""
14
12
  PERLA_LOCAL_API = 1
15
13
  PERLA_SILK = 2
16
14
  SMART_DOS = 3
17
15
 
18
- async def determine_bwt_model(host: str) -> BwtModel:
16
+ async def determine_bwt_model(host: str, logger: logging.Logger = logging.getLogger(__name__)) -> BwtModel:
19
17
  """Determine the BWT model based on the api response."""
20
18
 
21
- _logger.debug(f"Determining BWT model for host {host}")
19
+ logger.info("Determining BWT model for host %s", host)
22
20
  timeout = aiohttp.ClientTimeout(total=3)
23
21
 
24
22
  # Recent BWT Perla models with local API
@@ -26,8 +24,9 @@ async def determine_bwt_model(host: str) -> BwtModel:
26
24
  async with aiohttp.ClientSession() as session:
27
25
  async with session.get(f"http://{host}:8080/api", timeout=timeout) as response:
28
26
  res = await response.text()
29
- _logger.debug(f"Response from {host}:8080/api: {response.status} - {res}")
27
+ logger.debug("Response from %s:8080/api: %s - %s", host, response.status, res)
30
28
  if response.status == 404 and res == "Not Found":
29
+ logger.info("Detected BWT Perla model with local API at %s", host)
31
30
  return BwtModel.PERLA_LOCAL_API
32
31
  except Exception:
33
32
  pass
@@ -37,8 +36,9 @@ async def determine_bwt_model(host: str) -> BwtModel:
37
36
  async with aiohttp.ClientSession() as session:
38
37
  async with session.get(f"http://{host}:80/silk/registers", timeout=timeout) as response:
39
38
  res = await response.text()
40
- _logger.debug(f"Response from {host}:80/silk/registers: {response.status} - {res}")
39
+ logger.debug("Response from %s:80/silk/registers: %s - %s", host, response.status, res)
41
40
  if response.status == 200 and res.startswith("""{"params":["""):
41
+ logger.info("Detected BWT Perla model with Silk API at %s", host)
42
42
  return BwtModel.PERLA_SILK
43
43
  except Exception:
44
44
  pass
@@ -48,8 +48,9 @@ async def determine_bwt_model(host: str) -> BwtModel:
48
48
  async with aiohttp.ClientSession() as session:
49
49
  async with session.get(f"http://{host}:80/api/v1/gatt/0201", timeout=timeout) as response:
50
50
  res = await response.text()
51
- _logger.debug(f"Response from {host}:80/api/v1/gatt/0201: {response.status} - {res}")
51
+ logger.debug("Response from %s:80/api/v1/gatt/0201: %s - %s", host, response.status, res)
52
52
  if response.status == 200 and res.startswith("""{"""):
53
+ logger.info("Detected BWT Smart Dos model at %s", host)
53
54
  return BwtModel.SMART_DOS
54
55
  except Exception:
55
56
  pass
@@ -9,8 +9,6 @@ from bwt_api.error import BwtError
9
9
  from bwt_api.exception import ApiException, ConnectException, WrongCodeException
10
10
  from bwt_api.data import CurrentResponse, DailyResponse, MonthlyResponse, YearlyResponse, Hardness, BwtStatus
11
11
 
12
- _logger = logging.getLogger(__name__)
13
-
14
12
 
15
13
  class BwtApi:
16
14
  """BWT Api."""
@@ -18,12 +16,13 @@ class BwtApi:
18
16
  _host: str
19
17
  _headers: dict[str, str]
20
18
 
21
- def __init__(self, host, code):
19
+ def __init__(self, host, code, logger: logging.Logger = logging.getLogger(__name__)):
22
20
  self._host = host
23
21
  auth = f"user:{code}"
24
22
  base64_auth = base64.b64encode(auth.encode("ascii")).decode("ascii")
25
23
  self._headers = {"Authorization": f"Basic {base64_auth}"}
26
24
  self._session = aiohttp.ClientSession(headers=self._headers)
25
+ self._logger = logger
27
26
 
28
27
  async def __aenter__(self):
29
28
  return self
@@ -38,20 +37,22 @@ class BwtApi:
38
37
  """Internal method to fetch json from the endpoint and handle general errors."""
39
38
  try:
40
39
  async with self._session.get(f"http://{self._host}:8080/api/{endpoint}") as response:
41
- _logger.debug(
42
- f"Response status: {response.status}, content-type: {response.headers['content-type']}"
40
+ self._logger.debug(
41
+ "Response status: %s, content-type: %s",
42
+ response.status,
43
+ response.headers['content-type']
43
44
  )
44
45
  if response.status == 200:
45
46
  json = await response.json(content_type=None)
46
- _logger.debug(f"Raw response: {json}")
47
+ self._logger.debug("Raw response: %s", json)
47
48
  return json
48
49
  else:
49
50
  text = await response.text()
50
51
  if response.status == 404 and text == "":
51
- _logger.warning(f"Assuming wrong code: {response}")
52
+ self._logger.warning("Assuming wrong code: %s", response)
52
53
  raise WrongCodeException
53
54
  else:
54
- _logger.warning(f"Unknown response with status {response.status}: {text}")
55
+ self._logger.warning("Unknown response with status %s: %s", response.status, text)
55
56
  raise ApiException(f"Unknown response: {text}")
56
57
  except (aiohttp.ClientConnectorError, TimeoutError) as e:
57
58
  raise ConnectException from e
@@ -64,11 +65,11 @@ class BwtApi:
64
65
 
65
66
  async def get_current_data(self) -> CurrentResponse:
66
67
  """Get the current state of the BWT."""
67
- _logger.debug(f"Fetching current data from {self._host}")
68
+ self._logger.debug("Fetching current data from %s", self._host)
68
69
  raw = await self.__get_data("GetCurrentData")
69
70
  errors = [BwtError(int(error)) for error in raw["ActiveErrorIDs"].split(",") if error]
70
71
  if BwtError.UNKNOWN in errors:
71
- _logger.warning(f"Unknown error in current data response {raw['ActiveErrorIDs']}")
72
+ self._logger.warning("Unknown error in current data response %s", raw['ActiveErrorIDs'])
72
73
  perla_one = raw["CapacityColumn2_ml_dH"] == -1
73
74
 
74
75
  in_hardness = Hardness(
@@ -114,7 +115,7 @@ class BwtApi:
114
115
 
115
116
  async def get_daily_data(self) -> DailyResponse:
116
117
  """Get treated water of the current day."""
117
- _logger.debug(f"Fetching daily data from {self._host}")
118
+ self._logger.debug("Fetching daily data from %s", self._host)
118
119
  raw = await self.__get_data("GetDailyData")
119
120
  keys = [
120
121
  f"{min // 60:02}{min % 60:02}_{min // 60:02}{min % 60 + 29:02}_l"
@@ -124,14 +125,14 @@ class BwtApi:
124
125
 
125
126
  async def get_monthly_data(self) -> MonthlyResponse:
126
127
  """Get treated water of the current month."""
127
- _logger.debug(f"Fetching monthly data from {self._host}")
128
+ self._logger.debug("Fetching monthly data from %s", self._host)
128
129
  raw = await self.__get_data("GetMonthlyData")
129
130
  keys = [f"Day{day:02}_l" for day in range(1, 32)]
130
131
  return MonthlyResponse(list(map(lambda k: raw[k], keys)))
131
132
 
132
133
  async def get_yearly_data(self) -> YearlyResponse:
133
134
  """Get treated water of the current year."""
134
- _logger.debug(f"Fetching yearly data from {self._host}")
135
+ self._logger.debug("Fetching yearly data from %s", self._host)
135
136
  raw = await self.__get_data("GetYearlyData")
136
137
  keys = [f"Month{month:02}_l" for month in range(1, 13)]
137
138
  return YearlyResponse(list(map(lambda k: raw[k], keys)))
@@ -5,6 +5,7 @@ import enum
5
5
 
6
6
  from dataclasses import dataclass
7
7
  from datetime import datetime
8
+ from typing import Optional
8
9
  from bwt_api.error import BwtError
9
10
 
10
11
 
@@ -155,9 +156,9 @@ class PouchInfoResponse:
155
156
  @dataclass
156
157
  class RemainingCapacityResponse:
157
158
  """UUID 0402: Remaining Capacity"""
158
- rem_capacity: float # Remaining volume in ml
159
- rem_capacity_pct: float # Remaining volume in %
160
- rem_capacity_days: int # Remaining volume in days
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
161
162
 
162
163
 
163
164
  @dataclass
@@ -5,17 +5,16 @@ import logging
5
5
 
6
6
  from bwt_api.exception import ApiException, ConnectException
7
7
 
8
- _logger = logging.getLogger(__name__)
9
-
10
8
 
11
9
  class BwtSilkApi:
12
10
  """BWT Silk Api."""
13
11
  _session: aiohttp.ClientSession
14
12
  _host: str
15
13
 
16
- def __init__(self, host):
14
+ def __init__(self, host, logger: logging.Logger = logging.getLogger(__name__)):
17
15
  self._host = host
18
16
  self._session = aiohttp.ClientSession()
17
+ self._logger = logger
19
18
 
20
19
  async def __aenter__(self):
21
20
  return self
@@ -30,16 +29,18 @@ class BwtSilkApi:
30
29
  """Internal method to fetch json from the endpoint and handle general errors."""
31
30
  try:
32
31
  async with self._session.get(f"http://{self._host}:80/silk/registers") as response:
33
- _logger.debug(
34
- f"Response status: {response.status}, content-type: {response.headers['content-type']}"
32
+ self._logger.debug(
33
+ "Response status: %s, content-type: %s",
34
+ response.status,
35
+ response.headers['content-type']
35
36
  )
36
37
  if response.status == 200:
37
38
  json = await response.json(content_type=None)
38
- _logger.debug(f"Raw response: {json}")
39
+ self._logger.debug("Raw response: %s", json)
39
40
  return json["params"]
40
41
  else:
41
42
  text = await response.text()
42
- _logger.warning(f"Unknown response with status {response.status}: {text}")
43
+ self._logger.warning("Unknown response with status %s: %s", response.status, text)
43
44
  raise ApiException(f"Unknown response: {text}")
44
45
  except aiohttp.ClientConnectorError as e:
45
46
  raise ConnectException from e
@@ -12,8 +12,6 @@ __author__ = "dkarv"
12
12
  __copyright__ = "dkarv"
13
13
  __license__ = "MIT"
14
14
 
15
- _logger = logging.getLogger(__name__)
16
-
17
15
 
18
16
  def parse_args(args):
19
17
  """Parse command line parameters
@@ -18,17 +18,16 @@ from bwt_api.data import (
18
18
  )
19
19
  from bwt_api.exception import ApiException, ConnectException
20
20
 
21
- _logger = logging.getLogger(__name__)
22
-
23
21
 
24
22
  class BwtSmartDosApi:
25
23
  """BWT Smart Dos Api."""
26
24
  _session: aiohttp.ClientSession
27
25
  _host: str
28
26
 
29
- def __init__(self, host: str):
27
+ def __init__(self, host: str, logger: logging.Logger = logging.getLogger(__name__)):
30
28
  self._host = host
31
29
  self._session = aiohttp.ClientSession()
30
+ self._logger = logger
32
31
 
33
32
  async def __aenter__(self):
34
33
  return self
@@ -43,28 +42,30 @@ class BwtSmartDosApi:
43
42
  """Internal method to fetch GATT characteristic JSON."""
44
43
  try:
45
44
  async with self._session.get(f"http://{self._host}:80/api/v1/gatt/{uuid}") as response:
46
- _logger.debug(
47
- f"Response status: {response.status}, content-type: {response.headers['content-type']}"
45
+ self._logger.debug(
46
+ "Response status: %s, content-type: %s",
47
+ response.status,
48
+ response.headers['content-type']
48
49
  )
49
50
  if response.status == 200:
50
51
  json = await response.json(content_type=None)
51
- _logger.debug(f"Raw response for UUID {uuid}: {json}")
52
+ self._logger.debug("Raw response for UUID %s: %s", uuid, json)
52
53
  return json
53
54
  text = await response.text()
54
- _logger.warning(f"Unknown response with status {response.status}: {text}")
55
+ self._logger.warning("Unknown response with status %s: %s", response.status, text)
55
56
  raise ApiException(f"Unknown response: {text}")
56
57
  except aiohttp.ClientConnectorError as e:
57
58
  raise ConnectException from e
58
59
 
59
60
  async def get_wifi_info(self) -> WifiResponse:
60
61
  """UUID 0104: Get Wi-Fi name and signal strength."""
61
- _logger.debug(f"Fetching Wi-Fi info from {self._host}")
62
+ self._logger.debug("Fetching Wi-Fi info from %s", self._host)
62
63
  raw = await self._get_gatt("0104")
63
64
  return WifiResponse(ssid=raw["ssid"], rssi=raw["rssi"])
64
65
 
65
66
  async def get_device_info(self) -> DeviceInfoResponse:
66
67
  """UUID 0201: Get device information."""
67
- _logger.debug(f"Fetching device info from {self._host}")
68
+ self._logger.debug("Fetching device info from %s", self._host)
68
69
  raw = await self._get_gatt("0201")
69
70
  return DeviceInfoResponse(
70
71
  fw_rev=raw["fwRev"],
@@ -79,7 +80,7 @@ class BwtSmartDosApi:
79
80
 
80
81
  async def get_configuration(self) -> ConfigurationResponse:
81
82
  """UUID 0202: Get device configuration."""
82
- _logger.debug(f"Fetching configuration from {self._host}")
83
+ self._logger.debug("Fetching configuration from %s", self._host)
83
84
  raw = await self._get_gatt("0202")
84
85
  return ConfigurationResponse(
85
86
  buzzer_en=raw["buzzerEn"],
@@ -92,13 +93,13 @@ class BwtSmartDosApi:
92
93
 
93
94
  async def get_time_info(self) -> TimeResponse:
94
95
  """UUID 0208: Get time and timezone information."""
95
- _logger.debug(f"Fetching time info from {self._host}")
96
+ self._logger.debug("Fetching time info from %s", self._host)
96
97
  raw = await self._get_gatt("0208")
97
98
  return TimeResponse(time=raw["time"], timezone=raw["timezone"])
98
99
 
99
100
  async def get_pouch_info(self) -> PouchInfoResponse:
100
101
  """UUID 0401: Get pouch/container information."""
101
- _logger.debug(f"Fetching pouch info from {self._host}")
102
+ self._logger.debug("Fetching pouch info from %s", self._host)
102
103
  raw = await self._get_gatt("0401")
103
104
  return PouchInfoResponse(
104
105
  tot_cap=raw["totCap"],
@@ -111,24 +112,24 @@ class BwtSmartDosApi:
111
112
 
112
113
  async def get_remaining_capacity(self) -> RemainingCapacityResponse:
113
114
  """UUID 0402: Get remaining capacity information."""
114
- _logger.debug(f"Fetching remaining capacity from {self._host}")
115
+ self._logger.debug("Fetching remaining capacity from %s", self._host)
115
116
  raw = await self._get_gatt("0402")
116
117
  return RemainingCapacityResponse(
117
- rem_capacity=raw["remCapacity"],
118
- rem_capacity_pct=raw["remCapacityPct"],
119
- rem_capacity_days=raw["remCapacityDays"],
118
+ rem_capacity=raw.get("remCapacity"),
119
+ rem_capacity_pct=raw.get("remCapacityPct"),
120
+ rem_capacity_days=raw.get("remCapacityDays"),
120
121
  )
121
122
 
122
123
  async def get_treated_water(self) -> TreatedWaterResponse:
123
124
  """UUID 0503: Get treated water information."""
124
- _logger.debug(f"Fetching treated water from {self._host}")
125
+ self._logger.debug("Fetching treated water from %s", self._host)
125
126
  raw = await self._get_gatt("0503")
126
127
  total_flow = raw["flow"]["1"]["totFlow"]
127
128
  return TreatedWaterResponse(total_flow=total_flow)
128
129
 
129
130
  async def get_substance_dosage(self) -> SubstanceDosageResponse:
130
131
  """UUID 0505: Get substance dosage information."""
131
- _logger.debug(f"Fetching substance dosage from {self._host}")
132
+ self._logger.debug("Fetching substance dosage from %s", self._host)
132
133
  raw = await self._get_gatt("0505")
133
134
  return SubstanceDosageResponse(dosed_mineral=raw["dosedMineral"])
134
135
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bwt_api
3
- Version: 1.0.0
3
+ Version: 1.0.2
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,32 +1,32 @@
1
1
  {
2
2
  "files": [
3
- ".coveragerc",
4
- "setup.cfg",
5
- "tox.ini",
3
+ ".gitignore",
4
+ "AUTHORS.rst",
6
5
  "LICENSE.txt",
7
- "setup.py",
8
- "pyproject.toml",
9
6
  "CHANGELOG.rst",
7
+ "tox.ini",
8
+ "pyproject.toml",
9
+ ".coveragerc",
10
+ "setup.py",
10
11
  "CONTRIBUTING.rst",
12
+ "setup.cfg",
11
13
  "README.rst",
12
- ".gitignore",
13
- "AUTHORS.rst",
14
- ".devcontainer/devcontainer.json",
15
- "src/bwt_api/data.py",
16
- "src/bwt_api/error.py",
17
- "src/bwt_api/__init__.py",
18
- "src/bwt_api/silk_api.py",
19
- "src/bwt_api/exception.py",
14
+ "src/bwt_api/bwt_api",
20
15
  "src/bwt_api/api.py",
21
- "src/bwt_api/bwt_api.py",
16
+ "src/bwt_api/exception.py",
22
17
  "src/bwt_api/bwt.py",
18
+ "src/bwt_api/data.py",
23
19
  "src/bwt_api/smart_dos_api.py",
24
- "src/bwt_api/bwt_api",
20
+ "src/bwt_api/bwt_api.py",
21
+ "src/bwt_api/silk_api.py",
22
+ "src/bwt_api/error.py",
25
23
  "src/bwt_api/skeleton.py",
26
- "tests/test_api.py",
27
- "tests/conftest.py",
28
- "tests/test_distinguish.py",
24
+ "src/bwt_api/__init__.py",
25
+ ".devcontainer/devcontainer.json",
29
26
  ".github/workflows/python-publish.yml",
30
- ".github/workflows/python-package.yml"
27
+ ".github/workflows/python-package.yml",
28
+ "tests/test_distinguish.py",
29
+ "tests/conftest.py",
30
+ "tests/test_api.py"
31
31
  ]
32
32
  }
@@ -0,0 +1,8 @@
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
+ }
@@ -1,8 +0,0 @@
1
- {
2
- "tag": "1.0.0",
3
- "distance": 0,
4
- "node": "g5492b4bbc37a2bb17fa43aa4c44738964f42a13f",
5
- "dirty": false,
6
- "branch": "HEAD",
7
- "node_date": "2026-07-14"
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