sweatstack 0.82.0__tar.gz → 0.83.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 (67) hide show
  1. {sweatstack-0.82.0 → sweatstack-0.83.0}/CHANGELOG.md +10 -0
  2. {sweatstack-0.82.0 → sweatstack-0.83.0}/PKG-INFO +1 -1
  3. {sweatstack-0.82.0 → sweatstack-0.83.0}/pyproject.toml +1 -1
  4. sweatstack-0.83.0/src/sweatstack/cli.py +111 -0
  5. {sweatstack-0.82.0 → sweatstack-0.83.0}/src/sweatstack/client.py +1 -1
  6. {sweatstack-0.82.0 → sweatstack-0.83.0}/src/sweatstack/openapi_schemas.py +307 -202
  7. {sweatstack-0.82.0 → sweatstack-0.83.0}/tests/test_dailies.py +32 -14
  8. {sweatstack-0.82.0 → sweatstack-0.83.0}/tests/test_sport_ost.py +2 -4
  9. {sweatstack-0.82.0 → sweatstack-0.83.0}/uv.lock +1 -1
  10. sweatstack-0.82.0/src/sweatstack/cli.py +0 -48
  11. {sweatstack-0.82.0 → sweatstack-0.83.0}/.claude/settings.local.json +0 -0
  12. {sweatstack-0.82.0 → sweatstack-0.83.0}/.claude/skills/sweatstack-python/SKILL.md +0 -0
  13. {sweatstack-0.82.0 → sweatstack-0.83.0}/.claude/skills/sweatstack-python/client.md +0 -0
  14. {sweatstack-0.82.0 → sweatstack-0.83.0}/.claude/skills/sweatstack-python/data-models.md +0 -0
  15. {sweatstack-0.82.0 → sweatstack-0.83.0}/.claude/skills/sweatstack-python/fastapi.md +0 -0
  16. {sweatstack-0.82.0 → sweatstack-0.83.0}/.claude/skills/sweatstack-python/streamlit.md +0 -0
  17. {sweatstack-0.82.0 → sweatstack-0.83.0}/.gitignore +0 -0
  18. {sweatstack-0.82.0 → sweatstack-0.83.0}/.python-version +0 -0
  19. {sweatstack-0.82.0 → sweatstack-0.83.0}/AGENTS.md +0 -0
  20. {sweatstack-0.82.0 → sweatstack-0.83.0}/CONTRIBUTING.md +0 -0
  21. {sweatstack-0.82.0 → sweatstack-0.83.0}/DEVELOPMENT.md +0 -0
  22. {sweatstack-0.82.0 → sweatstack-0.83.0}/LICENSE +0 -0
  23. {sweatstack-0.82.0 → sweatstack-0.83.0}/Makefile +0 -0
  24. {sweatstack-0.82.0 → sweatstack-0.83.0}/README.md +0 -0
  25. {sweatstack-0.82.0 → sweatstack-0.83.0}/docs/conf.py +0 -0
  26. {sweatstack-0.82.0 → sweatstack-0.83.0}/docs/everything.rst +0 -0
  27. {sweatstack-0.82.0 → sweatstack-0.83.0}/docs/index.rst +0 -0
  28. {sweatstack-0.82.0 → sweatstack-0.83.0}/examples/fastapi_webhooks_example.py +0 -0
  29. {sweatstack-0.82.0 → sweatstack-0.83.0}/examples/send_webhook.py +0 -0
  30. {sweatstack-0.82.0 → sweatstack-0.83.0}/plans/001a_tests.md +0 -0
  31. {sweatstack-0.82.0 → sweatstack-0.83.0}/plans/001b_metadata.md +0 -0
  32. {sweatstack-0.82.0 → sweatstack-0.83.0}/plans/001c_dailies.md +0 -0
  33. {sweatstack-0.82.0 → sweatstack-0.83.0}/plans/002_TYPED_EXCEPTIONS.md +0 -0
  34. {sweatstack-0.82.0 → sweatstack-0.83.0}/plans/003_trace_test_linking.md +0 -0
  35. {sweatstack-0.82.0 → sweatstack-0.83.0}/plans/004_codebase_hygiene.md +0 -0
  36. {sweatstack-0.82.0 → sweatstack-0.83.0}/plans/005_ost_sport_bridge.md +0 -0
  37. {sweatstack-0.82.0 → sweatstack-0.83.0}/src/sweatstack/Sweat Stack examples/Getting started.ipynb +0 -0
  38. {sweatstack-0.82.0 → sweatstack-0.83.0}/src/sweatstack/__init__.py +0 -0
  39. {sweatstack-0.82.0 → sweatstack-0.83.0}/src/sweatstack/constants.py +0 -0
  40. {sweatstack-0.82.0 → sweatstack-0.83.0}/src/sweatstack/exceptions.py +0 -0
  41. {sweatstack-0.82.0 → sweatstack-0.83.0}/src/sweatstack/fastapi/__init__.py +0 -0
  42. {sweatstack-0.82.0 → sweatstack-0.83.0}/src/sweatstack/fastapi/access_token_cache.py +0 -0
  43. {sweatstack-0.82.0 → sweatstack-0.83.0}/src/sweatstack/fastapi/config.py +0 -0
  44. {sweatstack-0.82.0 → sweatstack-0.83.0}/src/sweatstack/fastapi/dependencies.py +0 -0
  45. {sweatstack-0.82.0 → sweatstack-0.83.0}/src/sweatstack/fastapi/models.py +0 -0
  46. {sweatstack-0.82.0 → sweatstack-0.83.0}/src/sweatstack/fastapi/routes.py +0 -0
  47. {sweatstack-0.82.0 → sweatstack-0.83.0}/src/sweatstack/fastapi/session.py +0 -0
  48. {sweatstack-0.82.0 → sweatstack-0.83.0}/src/sweatstack/fastapi/token_stores.py +0 -0
  49. {sweatstack-0.82.0 → sweatstack-0.83.0}/src/sweatstack/fastapi/webhooks.py +0 -0
  50. {sweatstack-0.82.0 → sweatstack-0.83.0}/src/sweatstack/ipython_init.py +0 -0
  51. {sweatstack-0.82.0 → sweatstack-0.83.0}/src/sweatstack/jupyterlab_oauth2_startup.py +0 -0
  52. {sweatstack-0.82.0 → sweatstack-0.83.0}/src/sweatstack/py.typed +0 -0
  53. {sweatstack-0.82.0 → sweatstack-0.83.0}/src/sweatstack/schemas.py +0 -0
  54. {sweatstack-0.82.0 → sweatstack-0.83.0}/src/sweatstack/streamlit.py +0 -0
  55. {sweatstack-0.82.0 → sweatstack-0.83.0}/src/sweatstack/sweatshell.py +0 -0
  56. {sweatstack-0.82.0 → sweatstack-0.83.0}/src/sweatstack/utils.py +0 -0
  57. {sweatstack-0.82.0 → sweatstack-0.83.0}/tests/__init__.py +0 -0
  58. {sweatstack-0.82.0 → sweatstack-0.83.0}/tests/test_access_token_cache.py +0 -0
  59. {sweatstack-0.82.0 → sweatstack-0.83.0}/tests/test_dtype_conversion.py +0 -0
  60. {sweatstack-0.82.0 → sweatstack-0.83.0}/tests/test_exceptions.py +0 -0
  61. {sweatstack-0.82.0 → sweatstack-0.83.0}/tests/test_longitudinal_mean_max_after.py +0 -0
  62. {sweatstack-0.82.0 → sweatstack-0.83.0}/tests/test_metadata.py +0 -0
  63. {sweatstack-0.82.0 → sweatstack-0.83.0}/tests/test_public_surface.py +0 -0
  64. {sweatstack-0.82.0 → sweatstack-0.83.0}/tests/test_teams.py +0 -0
  65. {sweatstack-0.82.0 → sweatstack-0.83.0}/tests/test_tests.py +0 -0
  66. {sweatstack-0.82.0 → sweatstack-0.83.0}/tests/test_trace_test_linking.py +0 -0
  67. {sweatstack-0.82.0 → sweatstack-0.83.0}/tests/test_webhooks.py +0 -0
@@ -5,6 +5,16 @@ All notable changes to this project will be documented in this file.
5
5
  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
+
9
+ ## [0.83.0] - 2026-06-16
10
+
11
+ ### Added
12
+ - Trace responses now include `test` and `test_match` (server-side test matching).
13
+
14
+ ### Fixed
15
+ - Fixes Dailies response schema.
16
+
17
+
8
18
  ## [0.82.0] - 2026-06-16
9
19
 
10
20
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sweatstack
3
- Version: 0.82.0
3
+ Version: 0.83.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.82.0"
3
+ version = "0.83.0"
4
4
  description = "The official Python client for SweatStack"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -0,0 +1,111 @@
1
+ import ast
2
+ import re
3
+ from pathlib import Path
4
+
5
+ import httpx
6
+
7
+ from datamodel_code_generator import InputFileType, generate
8
+ from datamodel_code_generator import DataModelType
9
+
10
+
11
+ def _bind_sport_to_ost(path: Path) -> None:
12
+ """Type every ``sport`` / ``sports`` model field as OpenSportTaxonomy's permissive ``SportField``.
13
+
14
+ The API exposes ``sport`` as a free-form OpenSportTaxonomy string, so datamodel-codegen types these
15
+ fields as plain ``str``. We retype them to ``SportField``, which validates an inbound string to an
16
+ ``open_sport_taxonomy.Sport`` and serialises back to the canonical wire string, tolerating sports
17
+ newer than the bundled taxonomy. Any leftover generated ``Sport`` schema is dropped. Anchored on the
18
+ AST (not a text match) and idempotent, so it survives regeneration.
19
+ """
20
+ src = path.read_text()
21
+ tree = ast.parse(src)
22
+ lines = src.splitlines(keepends=True)
23
+
24
+ drop: set[int] = set() # 0-indexed lines to remove
25
+ replace: dict[int, str] = {} # 0-indexed line -> new text
26
+
27
+ # Drop a leftover generated `Sport` schema (the server may still emit an unused one) ...
28
+ for node in tree.body:
29
+ if isinstance(node, ast.ClassDef) and node.name == "Sport":
30
+ drop.update(range(node.lineno - 1, node.end_lineno))
31
+ # ... and any SportField import from a previous run (re-injected cleanly below).
32
+ for i, line in enumerate(lines):
33
+ if line.startswith("from open_sport_taxonomy.pydantic import SportField"):
34
+ drop.add(i)
35
+
36
+ # Retype every `sport` / `sports` field annotation: str -> SportField (str | None, list[str], ...).
37
+ for node in ast.walk(tree):
38
+ if (isinstance(node, ast.AnnAssign) and isinstance(node.target, ast.Name)
39
+ and node.target.id in ("sport", "sports")):
40
+ annotation = ast.get_source_segment(src, node.annotation)
41
+ retyped = re.sub(r"\bstr\b", "SportField", annotation)
42
+ if retyped != annotation:
43
+ i = node.lineno - 1
44
+ replace[i] = lines[i].replace(annotation, retyped, 1)
45
+
46
+ out: list[str] = []
47
+ for i, line in enumerate(lines):
48
+ if i in drop:
49
+ continue
50
+ out.append(replace.get(i, line))
51
+ if line.startswith("from __future__ import annotations"):
52
+ out.append("from open_sport_taxonomy.pydantic import SportField # OST sport type (see schemas.py)\n")
53
+ path.write_text("".join(out))
54
+
55
+
56
+ def _restore_naive_local_datetimes(path: Path) -> None:
57
+ """Type local timestamps as ``NaiveDatetime`` rather than ``AwareDatetime``.
58
+
59
+ The API returns *local* timestamps without a timezone, but datamodel-codegen types every
60
+ ``date-time`` field as ``AwareDatetime`` -- which rejects a naive value. Retype the local fields
61
+ (those whose name ends in ``_local``) back to ``NaiveDatetime``, and keep ``registered_at``
62
+ accepting either. AST-anchored and idempotent, so it survives regeneration (and replaces the
63
+ manual fixups this file has needed in the past).
64
+ """
65
+ src = path.read_text()
66
+ tree = ast.parse(src)
67
+ lines = src.splitlines(keepends=True)
68
+ replace: dict[int, str] = {}
69
+
70
+ for node in ast.walk(tree):
71
+ if not (isinstance(node, ast.AnnAssign) and isinstance(node.target, ast.Name)):
72
+ continue
73
+ name = node.target.id
74
+ annotation = ast.get_source_segment(src, node.annotation)
75
+ if name.endswith("_local") and "AwareDatetime" in annotation:
76
+ retyped = annotation.replace("AwareDatetime", "NaiveDatetime")
77
+ elif name == "registered_at" and annotation == "AwareDatetime":
78
+ retyped = "AwareDatetime | NaiveDatetime"
79
+ else:
80
+ continue
81
+ i = node.lineno - 1
82
+ replace[i] = lines[i].replace(annotation, retyped, 1)
83
+
84
+ if not replace:
85
+ return # already naive (idempotent re-run)
86
+
87
+ src = "".join(replace.get(i, line) for i, line in enumerate(lines))
88
+ if " NaiveDatetime,\n" not in src: # ensure the import exists
89
+ src = src.replace(" AwareDatetime,\n", " AwareDatetime,\n NaiveDatetime,\n", 1)
90
+ path.write_text(src)
91
+
92
+
93
+ def generate_response_models():
94
+ response = httpx.get("http://localhost:8080/openapi.json")
95
+ response.raise_for_status()
96
+ output_directory = Path(__file__).parent
97
+ output = Path(output_directory / "openapi_schemas.py")
98
+ output.unlink(missing_ok=True)
99
+ generate(
100
+ response.text,
101
+ input_file_type=InputFileType.OpenAPI,
102
+ input_filename="openapi.json",
103
+ output=output,
104
+ # set up the output model types
105
+ output_model_type=DataModelType.PydanticV2BaseModel,
106
+ )
107
+ _bind_sport_to_ost(output)
108
+ _restore_naive_local_datetimes(output)
109
+
110
+ model = output.read_text()
111
+ print(model)
@@ -2181,7 +2181,7 @@ class Client(_OAuth2Mixin, _DelegationMixin, _TokenStorageMixin, _LocalCacheMixi
2181
2181
  dailies = [DailyResponse.model_validate(item) for item in response.json()]
2182
2182
  if as_dataframe:
2183
2183
  if not dailies:
2184
- df = pd.DataFrame(columns=["date", "value", "source"])
2184
+ df = pd.DataFrame(columns=["date", "value", "status", "source"])
2185
2185
  df = df.set_index("date")
2186
2186
  else:
2187
2187
  df = pd.DataFrame([d.model_dump() for d in dailies])