sweatstack 0.84.0__tar.gz → 0.86.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. {sweatstack-0.84.0 → sweatstack-0.86.0}/CHANGELOG.md +18 -0
  2. {sweatstack-0.84.0 → sweatstack-0.86.0}/PKG-INFO +1 -1
  3. {sweatstack-0.84.0 → sweatstack-0.86.0}/pyproject.toml +1 -1
  4. {sweatstack-0.84.0 → sweatstack-0.86.0}/src/sweatstack/client.py +52 -27
  5. {sweatstack-0.84.0 → sweatstack-0.86.0}/uv.lock +1 -1
  6. {sweatstack-0.84.0 → sweatstack-0.86.0}/.claude/settings.local.json +0 -0
  7. {sweatstack-0.84.0 → sweatstack-0.86.0}/.claude/skills/sweatstack-python/SKILL.md +0 -0
  8. {sweatstack-0.84.0 → sweatstack-0.86.0}/.claude/skills/sweatstack-python/client.md +0 -0
  9. {sweatstack-0.84.0 → sweatstack-0.86.0}/.claude/skills/sweatstack-python/data-models.md +0 -0
  10. {sweatstack-0.84.0 → sweatstack-0.86.0}/.claude/skills/sweatstack-python/fastapi.md +0 -0
  11. {sweatstack-0.84.0 → sweatstack-0.86.0}/.claude/skills/sweatstack-python/streamlit.md +0 -0
  12. {sweatstack-0.84.0 → sweatstack-0.86.0}/.gitignore +0 -0
  13. {sweatstack-0.84.0 → sweatstack-0.86.0}/.python-version +0 -0
  14. {sweatstack-0.84.0 → sweatstack-0.86.0}/AGENTS.md +0 -0
  15. {sweatstack-0.84.0 → sweatstack-0.86.0}/CONTRIBUTING.md +0 -0
  16. {sweatstack-0.84.0 → sweatstack-0.86.0}/DEVELOPMENT.md +0 -0
  17. {sweatstack-0.84.0 → sweatstack-0.86.0}/LICENSE +0 -0
  18. {sweatstack-0.84.0 → sweatstack-0.86.0}/Makefile +0 -0
  19. {sweatstack-0.84.0 → sweatstack-0.86.0}/README.md +0 -0
  20. {sweatstack-0.84.0 → sweatstack-0.86.0}/docs/conf.py +0 -0
  21. {sweatstack-0.84.0 → sweatstack-0.86.0}/docs/everything.rst +0 -0
  22. {sweatstack-0.84.0 → sweatstack-0.86.0}/docs/index.rst +0 -0
  23. {sweatstack-0.84.0 → sweatstack-0.86.0}/examples/fastapi_webhooks_example.py +0 -0
  24. {sweatstack-0.84.0 → sweatstack-0.86.0}/examples/send_webhook.py +0 -0
  25. {sweatstack-0.84.0 → sweatstack-0.86.0}/plans/001a_tests.md +0 -0
  26. {sweatstack-0.84.0 → sweatstack-0.86.0}/plans/001b_metadata.md +0 -0
  27. {sweatstack-0.84.0 → sweatstack-0.86.0}/plans/001c_dailies.md +0 -0
  28. {sweatstack-0.84.0 → sweatstack-0.86.0}/plans/002_TYPED_EXCEPTIONS.md +0 -0
  29. {sweatstack-0.84.0 → sweatstack-0.86.0}/plans/003_trace_test_linking.md +0 -0
  30. {sweatstack-0.84.0 → sweatstack-0.86.0}/plans/004_codebase_hygiene.md +0 -0
  31. {sweatstack-0.84.0 → sweatstack-0.86.0}/plans/005_ost_sport_bridge.md +0 -0
  32. {sweatstack-0.84.0 → sweatstack-0.86.0}/src/sweatstack/Sweat Stack examples/Getting started.ipynb +0 -0
  33. {sweatstack-0.84.0 → sweatstack-0.86.0}/src/sweatstack/__init__.py +0 -0
  34. {sweatstack-0.84.0 → sweatstack-0.86.0}/src/sweatstack/cli.py +0 -0
  35. {sweatstack-0.84.0 → sweatstack-0.86.0}/src/sweatstack/constants.py +0 -0
  36. {sweatstack-0.84.0 → sweatstack-0.86.0}/src/sweatstack/exceptions.py +0 -0
  37. {sweatstack-0.84.0 → sweatstack-0.86.0}/src/sweatstack/fastapi/__init__.py +0 -0
  38. {sweatstack-0.84.0 → sweatstack-0.86.0}/src/sweatstack/fastapi/access_token_cache.py +0 -0
  39. {sweatstack-0.84.0 → sweatstack-0.86.0}/src/sweatstack/fastapi/config.py +0 -0
  40. {sweatstack-0.84.0 → sweatstack-0.86.0}/src/sweatstack/fastapi/dependencies.py +0 -0
  41. {sweatstack-0.84.0 → sweatstack-0.86.0}/src/sweatstack/fastapi/models.py +0 -0
  42. {sweatstack-0.84.0 → sweatstack-0.86.0}/src/sweatstack/fastapi/routes.py +0 -0
  43. {sweatstack-0.84.0 → sweatstack-0.86.0}/src/sweatstack/fastapi/session.py +0 -0
  44. {sweatstack-0.84.0 → sweatstack-0.86.0}/src/sweatstack/fastapi/token_stores.py +0 -0
  45. {sweatstack-0.84.0 → sweatstack-0.86.0}/src/sweatstack/fastapi/webhooks.py +0 -0
  46. {sweatstack-0.84.0 → sweatstack-0.86.0}/src/sweatstack/ipython_init.py +0 -0
  47. {sweatstack-0.84.0 → sweatstack-0.86.0}/src/sweatstack/jupyterlab_oauth2_startup.py +0 -0
  48. {sweatstack-0.84.0 → sweatstack-0.86.0}/src/sweatstack/openapi_schemas.py +0 -0
  49. {sweatstack-0.84.0 → sweatstack-0.86.0}/src/sweatstack/py.typed +0 -0
  50. {sweatstack-0.84.0 → sweatstack-0.86.0}/src/sweatstack/schemas.py +0 -0
  51. {sweatstack-0.84.0 → sweatstack-0.86.0}/src/sweatstack/streamlit.py +0 -0
  52. {sweatstack-0.84.0 → sweatstack-0.86.0}/src/sweatstack/sweatshell.py +0 -0
  53. {sweatstack-0.84.0 → sweatstack-0.86.0}/src/sweatstack/utils.py +0 -0
  54. {sweatstack-0.84.0 → sweatstack-0.86.0}/tests/__init__.py +0 -0
  55. {sweatstack-0.84.0 → sweatstack-0.86.0}/tests/test_access_token_cache.py +0 -0
  56. {sweatstack-0.84.0 → sweatstack-0.86.0}/tests/test_dailies.py +0 -0
  57. {sweatstack-0.84.0 → sweatstack-0.86.0}/tests/test_dtype_conversion.py +0 -0
  58. {sweatstack-0.84.0 → sweatstack-0.86.0}/tests/test_exceptions.py +0 -0
  59. {sweatstack-0.84.0 → sweatstack-0.86.0}/tests/test_longitudinal_mean_max_after.py +0 -0
  60. {sweatstack-0.84.0 → sweatstack-0.86.0}/tests/test_metadata.py +0 -0
  61. {sweatstack-0.84.0 → sweatstack-0.86.0}/tests/test_public_surface.py +0 -0
  62. {sweatstack-0.84.0 → sweatstack-0.86.0}/tests/test_sport_ost.py +0 -0
  63. {sweatstack-0.84.0 → sweatstack-0.86.0}/tests/test_teams.py +0 -0
  64. {sweatstack-0.84.0 → sweatstack-0.86.0}/tests/test_tests.py +0 -0
  65. {sweatstack-0.84.0 → sweatstack-0.86.0}/tests/test_trace_test_linking.py +0 -0
  66. {sweatstack-0.84.0 → sweatstack-0.86.0}/tests/test_webhooks.py +0 -0
@@ -6,6 +6,24 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
8
 
9
+ ## [0.86.0] - 2026-06-18
10
+
11
+ ### Changed
12
+
13
+ - **Breaking:** renamed the adaptive-sampling parameters to NLEC (near-lossless effort codec). `adaptive_sampling_on` is now `nlec_on` (on `get_activity_data`, `get_latest_activity_data`, `get_longitudinal_data`) and `adaptive_sampling` is now `nlec` (on `get_activity_mean_max`, `get_latest_activity_mean_max`). No backwards-compatible aliases; requires the SweatStack server 0.107.0 or later.
14
+
15
+
16
+ ## [0.85.0] - 2026-06-17
17
+
18
+ ### Changed
19
+
20
+ - `get_longitudinal_mean_max(by=...)` now defaults to `None`: the `by` parameter is omitted from the request so the server picks the orientation. For `after` (fatigue) with `metric="power"` the server now defaults to `by="duration"`; every other case stays `by="intensity"`. The returned frame is indexed on whichever orientation the server used. Passing `by` explicitly still works.
21
+
22
+ ### Deprecated
23
+
24
+ - `by="intensity"` for the `after` (fatigue) case is deprecated and now raises a `DeprecationWarning`; `by="duration"` is the default and only supported orientation going forward. Pass `by="duration"` or leave `by` unset. (`by="intensity"` remains the only orientation for `metric="speed"`.)
25
+
26
+
9
27
  ## [0.84.0] - 2026-06-17
10
28
 
11
29
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sweatstack
3
- Version: 0.84.0
3
+ Version: 0.86.0
4
4
  Summary: The official Python client for SweatStack
5
5
  Project-URL: Homepage, https://sweatstack.no
6
6
  Project-URL: Documentation, https://docs.sweatstack.no/getting-started/
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sweatstack"
3
- version = "0.84.0"
3
+ version = "0.86.0"
4
4
  description = "The official Python client for SweatStack"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -1219,18 +1219,18 @@ class Client(_OAuth2Mixin, _DelegationMixin, _TokenStorageMixin, _LocalCacheMixi
1219
1219
  def get_activity_data(
1220
1220
  self,
1221
1221
  activity_id: str,
1222
- adaptive_sampling_on: Literal["power", "speed"] | None = None,
1222
+ nlec_on: Literal["power", "speed"] | None = None,
1223
1223
  metrics: list[Metric | str] | None = None,
1224
1224
  ) -> pd.DataFrame:
1225
1225
  """Gets the raw data for a specific activity.
1226
1226
 
1227
- This method retrieves the time-series data for a given activity, with optional
1228
- adaptive sampling to reduce data points for visualization.
1227
+ This method retrieves the time-series data for a given activity, with optional NLEC
1228
+ (near-lossless effort codec) downsampling to reduce data points for visualization.
1229
1229
 
1230
1230
  Args:
1231
1231
  activity_id: The unique identifier of the activity.
1232
- adaptive_sampling_on: Optional parameter to apply adaptive sampling on
1233
- either "power" or "speed" data. If None, no adaptive sampling is applied.
1232
+ nlec_on: Downsample with NLEC (near-lossless effort codec), keyed on
1233
+ either "power" or "speed" data. If None, no NLEC is applied.
1234
1234
  metrics: Optional list of metrics to include in the results. Can be a list of Metric enums or strings.
1235
1235
 
1236
1236
  Returns:
@@ -1241,8 +1241,8 @@ class Client(_OAuth2Mixin, _DelegationMixin, _TokenStorageMixin, _LocalCacheMixi
1241
1241
  SweatStackAPIError: If the API request fails for any other reason.
1242
1242
  """
1243
1243
  params = {}
1244
- if adaptive_sampling_on is not None:
1245
- params["adaptive_sampling_on"] = adaptive_sampling_on
1244
+ if nlec_on is not None:
1245
+ params["nlec_on"] = nlec_on
1246
1246
  if metrics is not None:
1247
1247
  params["metrics"] = self._enums_to_strings(metrics)
1248
1248
 
@@ -1260,7 +1260,7 @@ class Client(_OAuth2Mixin, _DelegationMixin, _TokenStorageMixin, _LocalCacheMixi
1260
1260
  self,
1261
1261
  activity_id: str,
1262
1262
  metric: Literal[Metric.power, Metric.speed] | Literal["power", "speed"],
1263
- adaptive_sampling: bool = False,
1263
+ nlec: bool = False,
1264
1264
  ) -> pd.DataFrame:
1265
1265
  """Gets the mean-max data for a specific activity.
1266
1266
 
@@ -1270,7 +1270,7 @@ class Client(_OAuth2Mixin, _DelegationMixin, _TokenStorageMixin, _LocalCacheMixi
1270
1270
  Args:
1271
1271
  activity_id: The unique identifier of the activity.
1272
1272
  metric: The metric to calculate mean-max values for, either "power" or "speed".
1273
- adaptive_sampling: Whether to apply adaptive sampling to reduce data points
1273
+ nlec: Downsample with NLEC (near-lossless effort codec) to reduce data points
1274
1274
  for visualization. Defaults to False.
1275
1275
 
1276
1276
  Returns:
@@ -1286,7 +1286,7 @@ class Client(_OAuth2Mixin, _DelegationMixin, _TokenStorageMixin, _LocalCacheMixi
1286
1286
  url=f"/api/v1/activities/{activity_id}/mean-max",
1287
1287
  params={
1288
1288
  "metric": metric,
1289
- "adaptive_sampling": adaptive_sampling,
1289
+ "nlec": nlec,
1290
1290
  },
1291
1291
  )
1292
1292
  self._raise_for_status(response)
@@ -1332,7 +1332,7 @@ class Client(_OAuth2Mixin, _DelegationMixin, _TokenStorageMixin, _LocalCacheMixi
1332
1332
  def get_latest_activity_data(
1333
1333
  self,
1334
1334
  sport: Sport | str | None = None,
1335
- adaptive_sampling_on: Literal["power", "speed"] | None = None,
1335
+ nlec_on: Literal["power", "speed"] | None = None,
1336
1336
  metrics: list[Metric | str] | None = None,
1337
1337
  ) -> pd.DataFrame:
1338
1338
  """Gets the data for the latest activity of a specific sport.
@@ -1342,7 +1342,7 @@ class Client(_OAuth2Mixin, _DelegationMixin, _TokenStorageMixin, _LocalCacheMixi
1342
1342
 
1343
1343
  Args:
1344
1344
  sport: Optional sport to filter by. Can be a Sport enum or string.
1345
- adaptive_sampling_on: Optional metric to apply adaptive sampling for visualization.
1345
+ nlec_on: Metric to downsample on with NLEC (near-lossless effort codec); omit to disable.
1346
1346
  Can be either "power" or "speed". Defaults to None.
1347
1347
  metrics: Optional list of metrics to include in the results. Can be a list of Metric enums or strings.
1348
1348
 
@@ -1353,13 +1353,13 @@ class Client(_OAuth2Mixin, _DelegationMixin, _TokenStorageMixin, _LocalCacheMixi
1353
1353
  SweatStackAPIError: If the API request fails.
1354
1354
  """
1355
1355
  activity = self.get_latest_activity(sport=sport)
1356
- return self.get_activity_data(activity.id, adaptive_sampling_on, metrics=metrics)
1356
+ return self.get_activity_data(activity.id, nlec_on, metrics=metrics)
1357
1357
 
1358
1358
  def get_latest_activity_mean_max(
1359
1359
  self,
1360
1360
  metric: Literal[Metric.power, Metric.speed] | Literal["power", "speed"],
1361
1361
  sport: Sport | str | None = None,
1362
- adaptive_sampling: bool = False,
1362
+ nlec: bool = False,
1363
1363
  ) -> pd.DataFrame:
1364
1364
  """Gets the mean-max curve for the latest activity of a specific sport.
1365
1365
 
@@ -1369,7 +1369,7 @@ class Client(_OAuth2Mixin, _DelegationMixin, _TokenStorageMixin, _LocalCacheMixi
1369
1369
  Args:
1370
1370
  metric: The metric to calculate the mean-max curve for. Can be either "power" or "speed".
1371
1371
  sport: Optional sport to filter by. Can be a Sport enum or string.
1372
- adaptive_sampling: Whether to apply adaptive sampling to the mean-max curve data.
1372
+ nlec: Downsample the mean-max curve with NLEC (near-lossless effort codec).
1373
1373
  Defaults to False.
1374
1374
 
1375
1375
  Returns:
@@ -1379,7 +1379,7 @@ class Client(_OAuth2Mixin, _DelegationMixin, _TokenStorageMixin, _LocalCacheMixi
1379
1379
  SweatStackAPIError: If the API request fails.
1380
1380
  """
1381
1381
  activity = self.get_latest_activity(sport=sport)
1382
- return self.get_activity_mean_max(activity.id, metric, adaptive_sampling)
1382
+ return self.get_activity_mean_max(activity.id, metric, nlec)
1383
1383
 
1384
1384
  def get_longitudinal_data(
1385
1385
  self,
@@ -1389,7 +1389,7 @@ class Client(_OAuth2Mixin, _DelegationMixin, _TokenStorageMixin, _LocalCacheMixi
1389
1389
  start: date | str,
1390
1390
  end: date | str | None = None,
1391
1391
  metrics: list[Metric | str] | None = None,
1392
- adaptive_sampling_on: Literal[Metric.power, Metric.speed] | Literal["power", "speed"] | None = None,
1392
+ nlec_on: Literal[Metric.power, Metric.speed] | Literal["power", "speed"] | None = None,
1393
1393
  ) -> pd.DataFrame:
1394
1394
  """Gets longitudinal data for activities within a specified date range.
1395
1395
 
@@ -1402,7 +1402,7 @@ class Client(_OAuth2Mixin, _DelegationMixin, _TokenStorageMixin, _LocalCacheMixi
1402
1402
  start: The start date for the data range. Can be a date object or string in ISO format.
1403
1403
  end: Optional end date for the data range. Can be a date object or string in ISO format.
1404
1404
  metrics: Optional list of metrics to include in the results. Can be a list of Metric enums or strings.
1405
- adaptive_sampling_on: Optional metric to apply adaptive sampling for visualization.
1405
+ nlec_on: Metric to downsample on with NLEC (near-lossless effort codec); omit to disable.
1406
1406
  Can be either "power" or "speed". Defaults to None.
1407
1407
 
1408
1408
  Returns:
@@ -1431,8 +1431,8 @@ class Client(_OAuth2Mixin, _DelegationMixin, _TokenStorageMixin, _LocalCacheMixi
1431
1431
  params["end"] = end
1432
1432
  if metrics is not None:
1433
1433
  params["metrics"] = self._enums_to_strings(metrics)
1434
- if adaptive_sampling_on is not None:
1435
- params["adaptive_sampling_on"] = self._enums_to_strings([adaptive_sampling_on])[0]
1434
+ if nlec_on is not None:
1435
+ params["nlec_on"] = self._enums_to_strings([nlec_on])[0]
1436
1436
 
1437
1437
  if self._cache_enabled():
1438
1438
  cache_key = self._generate_cache_key("longitudinal_data", **params)
@@ -1465,7 +1465,7 @@ class Client(_OAuth2Mixin, _DelegationMixin, _TokenStorageMixin, _LocalCacheMixi
1465
1465
  date: date | str | None = None,
1466
1466
  window_days: int | None = None,
1467
1467
  after: list[float] | float | None = None,
1468
- by: Literal["intensity", "duration"] = "intensity",
1468
+ by: Literal["intensity", "duration"] | None = None,
1469
1469
  ) -> pd.DataFrame:
1470
1470
  """Gets the mean-max curve for one or more sports and a metric.
1471
1471
 
@@ -1483,9 +1483,14 @@ class Client(_OAuth2Mixin, _DelegationMixin, _TokenStorageMixin, _LocalCacheMixi
1483
1483
  enveloped across rides. The returned DataFrame then has an ``after``
1484
1484
  column (one curve per value). Max 5 values; the date range is capped at
1485
1485
  1 year when ``after`` is used.
1486
- by: Axis to index the curve on. ``"intensity"`` (default) indexes by the metric
1487
- value (unchanged). ``"duration"`` indexes by duration via the fast segment
1488
- kernel; currently requires ``after`` and ``metric="power"``.
1486
+ by: Axis to index the curve on, only meaningful with ``after``. ``"duration"``
1487
+ indexes by duration via the fast segment kernel (requires ``metric="power"``);
1488
+ ``"intensity"`` indexes by the metric value and is **deprecated**. For the
1489
+ ``after`` case ``"duration"`` will become the only supported orientation, and
1490
+ more generally ``"duration"`` is set to become the default and only option, so
1491
+ we recommend passing ``by="duration"`` explicitly. When left as ``None``
1492
+ (default), the server picks: ``after`` with ``metric="power"`` resolves to
1493
+ ``"duration"``, every other case to ``"intensity"``.
1489
1494
 
1490
1495
  Returns:
1491
1496
  pd.DataFrame: A pandas DataFrame containing the mean-max curve data, indexed by
@@ -1509,11 +1514,31 @@ class Client(_OAuth2Mixin, _DelegationMixin, _TokenStorageMixin, _LocalCacheMixi
1509
1514
  raise ValueError("'sports' is required.")
1510
1515
  metric = self._enums_to_strings([metric])[0]
1511
1516
 
1517
+ # For the fatigue ('after') case, by='duration' is the default and the only
1518
+ # orientation going forward; by='intensity' is deprecated there. (For metric='speed'
1519
+ # it is still the only option, so no warning.)
1520
+ if by == "intensity" and after is not None and metric == "power":
1521
+ warnings.warn(
1522
+ "by='intensity' is deprecated for 'after' (fatigue) mean-max; by='duration' "
1523
+ "is the default and only supported orientation going forward. Pass "
1524
+ "by='duration' or leave 'by' unset.",
1525
+ DeprecationWarning,
1526
+ stacklevel=2,
1527
+ )
1528
+
1529
+ # Mirror the server's default resolution so the returned frame is indexed on the
1530
+ # orientation the server actually used. ``by=None`` omits the param (the server
1531
+ # decides and the deprecated value-indexed path is not forced).
1532
+ effective_by = by if by is not None else (
1533
+ "duration" if (after is not None and metric == "power") else "intensity"
1534
+ )
1535
+
1512
1536
  params = {
1513
1537
  "sport": self._enums_to_strings(sports),
1514
1538
  "metric": metric,
1515
- "by": by,
1516
1539
  }
1540
+ if by is not None:
1541
+ params["by"] = by
1517
1542
  if start is not None:
1518
1543
  params["start"] = start
1519
1544
  if end is not None:
@@ -1536,7 +1561,7 @@ class Client(_OAuth2Mixin, _DelegationMixin, _TokenStorageMixin, _LocalCacheMixi
1536
1561
  cache_key = self._generate_cache_key("mean_max", **params)
1537
1562
  cached = self._read_cache("mean_max", cache_key)
1538
1563
  if cached is not None:
1539
- return self._shape_mean_max(pd.read_parquet(BytesIO(cached)), metric, after, by)
1564
+ return self._shape_mean_max(pd.read_parquet(BytesIO(cached)), metric, after, effective_by)
1540
1565
 
1541
1566
  with self._http_client() as client:
1542
1567
  response = client.get(
@@ -1548,7 +1573,7 @@ class Client(_OAuth2Mixin, _DelegationMixin, _TokenStorageMixin, _LocalCacheMixi
1548
1573
  if self._cache_enabled():
1549
1574
  self._write_cache("mean_max", cache_key, response.content)
1550
1575
 
1551
- return self._shape_mean_max(pd.read_parquet(BytesIO(response.content)), metric, after, by)
1576
+ return self._shape_mean_max(pd.read_parquet(BytesIO(response.content)), metric, after, effective_by)
1552
1577
 
1553
1578
  def _shape_mean_max(self, df: pd.DataFrame, metric: str, after, by: str = "intensity") -> pd.DataFrame:
1554
1579
  """Standard post-processing for mean-max responses. The ``after`` response is
@@ -2600,7 +2600,7 @@ wheels = [
2600
2600
 
2601
2601
  [[package]]
2602
2602
  name = "sweatstack"
2603
- version = "0.83.0"
2603
+ version = "0.86.0"
2604
2604
  source = { editable = "." }
2605
2605
  dependencies = [
2606
2606
  { name = "email-validator" },
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