saturday 0.1.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.
- saturday-0.1.0/.github/workflows/publish.yml +31 -0
- saturday-0.1.0/.gitignore +11 -0
- saturday-0.1.0/CONTRIBUTING.md +20 -0
- saturday-0.1.0/LICENSE +21 -0
- saturday-0.1.0/PKG-INFO +129 -0
- saturday-0.1.0/README.md +104 -0
- saturday-0.1.0/SECURITY.md +10 -0
- saturday-0.1.0/pyproject.toml +46 -0
- saturday-0.1.0/saturday/__init__.py +43 -0
- saturday-0.1.0/saturday/client.py +419 -0
- saturday-0.1.0/saturday/errors.py +77 -0
- saturday-0.1.0/saturday/py.typed +0 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
|
|
7
|
+
permissions:
|
|
8
|
+
contents: read
|
|
9
|
+
id-token: write
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
publish:
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
environment: pypi
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
|
|
18
|
+
- uses: actions/setup-python@v5
|
|
19
|
+
with:
|
|
20
|
+
python-version: '3.12'
|
|
21
|
+
|
|
22
|
+
- name: Install build tools
|
|
23
|
+
run: pip install hatchling build
|
|
24
|
+
|
|
25
|
+
- name: Build package
|
|
26
|
+
run: python -m build
|
|
27
|
+
|
|
28
|
+
- name: Publish to PyPI
|
|
29
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
30
|
+
with:
|
|
31
|
+
packages-dir: dist/
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
Thank you for your interest in contributing to the Saturday SDK.
|
|
4
|
+
|
|
5
|
+
## Bug Reports
|
|
6
|
+
|
|
7
|
+
Please file an issue on this repository with:
|
|
8
|
+
- SDK version
|
|
9
|
+
- Language/runtime version
|
|
10
|
+
- Minimal reproduction steps
|
|
11
|
+
- Expected vs actual behavior
|
|
12
|
+
|
|
13
|
+
## Development
|
|
14
|
+
|
|
15
|
+
This SDK is generated from Saturday's OpenAPI specification and polished for production use.
|
|
16
|
+
See `REGENERATION.md` in the root SDK directory for the update workflow.
|
|
17
|
+
|
|
18
|
+
## Code of Conduct
|
|
19
|
+
|
|
20
|
+
Be kind. Be constructive. We're building tools that help athletes stay safe.
|
saturday-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Saturday Inc.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
saturday-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: saturday
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Official Python SDK for the Saturday Nutrition Intelligence API
|
|
5
|
+
Project-URL: Documentation, https://docs.saturday.fit
|
|
6
|
+
Project-URL: Repository, https://github.com/SaturdayInc/saturday-python
|
|
7
|
+
Project-URL: Issues, https://github.com/SaturdayInc/saturday-python/issues
|
|
8
|
+
Author-email: "Saturday Inc." <api@saturday.fit>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: api,electrolytes,endurance,hydration,nutrition,saturday,sports-nutrition
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Typing :: Typed
|
|
22
|
+
Requires-Python: >=3.9
|
|
23
|
+
Requires-Dist: httpx>=0.25.0
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# Saturday Python SDK
|
|
27
|
+
|
|
28
|
+
[](https://pypi.org/project/saturday/)
|
|
29
|
+
[](LICENSE)
|
|
30
|
+
|
|
31
|
+
Official Python SDK for the [Saturday Nutrition Intelligence API](https://docs.saturday.fit).
|
|
32
|
+
|
|
33
|
+
Personalized fuel, hydration, and electrolyte prescriptions for endurance athletes.
|
|
34
|
+
|
|
35
|
+
## Install
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install saturday
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Quick Start
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
from saturday import Saturday
|
|
45
|
+
|
|
46
|
+
client = Saturday(api_key="sat_live_...")
|
|
47
|
+
|
|
48
|
+
# Calculate a nutrition prescription
|
|
49
|
+
prescription = client.nutrition.calculate(
|
|
50
|
+
activity_type="bike",
|
|
51
|
+
duration_min=180,
|
|
52
|
+
athlete_weight_kg=75,
|
|
53
|
+
thermal_stress_level=7,
|
|
54
|
+
is_race=True,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
# Safety metadata is ALWAYS included — athlete safety is never paywalled
|
|
58
|
+
print(prescription["safety"]["warnings"])
|
|
59
|
+
print(f"Carbs: {prescription['carbohydrate']['target_g_per_hr']} g/hr")
|
|
60
|
+
print(f"Sodium: {prescription['sodium']['target_mg_per_hr']} mg/hr")
|
|
61
|
+
print(f"Fluid: {prescription['fluid']['target_ml_per_hr']} mL/hr")
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Features
|
|
65
|
+
|
|
66
|
+
- Fully typed with `py.typed` marker (PEP 561)
|
|
67
|
+
- Automatic retry with exponential backoff (429s and 5xx)
|
|
68
|
+
- Typed errors (`AuthenticationError`, `RateLimitError`, `ValidationError`, `NotFoundError`)
|
|
69
|
+
- API key and OAuth2 Bearer token authentication
|
|
70
|
+
- Context manager support for clean connection handling
|
|
71
|
+
- Safety types prominently surfaced (`not_instructions` documented)
|
|
72
|
+
|
|
73
|
+
## Authentication
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
# API key (server-to-server)
|
|
77
|
+
client = Saturday(api_key="sat_live_...")
|
|
78
|
+
|
|
79
|
+
# OAuth2 Bearer token (athlete-delegated access)
|
|
80
|
+
client = Saturday(api_key="sat_live_...", bearer_token="eyJ...")
|
|
81
|
+
|
|
82
|
+
# Context manager for automatic cleanup
|
|
83
|
+
with Saturday(api_key="sat_live_...") as client:
|
|
84
|
+
rx = client.nutrition.calculate(activity_type="run", duration_min=60)
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Error Handling
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
from saturday import Saturday, RateLimitError, ValidationError, NotFoundError
|
|
91
|
+
|
|
92
|
+
client = Saturday(api_key="sat_live_...")
|
|
93
|
+
|
|
94
|
+
try:
|
|
95
|
+
rx = client.nutrition.calculate(activity_type="swim", duration_min=60)
|
|
96
|
+
except ValidationError as e:
|
|
97
|
+
print(f"Invalid request: {e} (param: {e.param})")
|
|
98
|
+
except RateLimitError as e:
|
|
99
|
+
print(f"Rate limited. Retry after {e.retry_after}s")
|
|
100
|
+
except NotFoundError:
|
|
101
|
+
print("Resource not found")
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Resources
|
|
105
|
+
|
|
106
|
+
| Resource | Description |
|
|
107
|
+
|----------|-------------|
|
|
108
|
+
| `client.nutrition` | Calculate prescriptions, batch, compare |
|
|
109
|
+
| `client.athletes` | Athlete CRUD, settings, batch create, GDPR export |
|
|
110
|
+
| `client.activities` | Activity CRUD, prescription calculation, feedback |
|
|
111
|
+
| `client.products` | Product search, barcode lookup, curated list |
|
|
112
|
+
| `client.ai` | AI coaching conversations |
|
|
113
|
+
| `client.webhooks` | Webhook registration and management |
|
|
114
|
+
| `client.organizations` | Team/org management with members |
|
|
115
|
+
| `client.gear` | Athlete gear inventory |
|
|
116
|
+
| `client.knowledge` | Sports nutrition knowledge base search |
|
|
117
|
+
|
|
118
|
+
## Documentation
|
|
119
|
+
|
|
120
|
+
Full API documentation: [docs.saturday.fit](https://docs.saturday.fit)
|
|
121
|
+
|
|
122
|
+
## Requirements
|
|
123
|
+
|
|
124
|
+
- Python 3.9+
|
|
125
|
+
- httpx 0.25+
|
|
126
|
+
|
|
127
|
+
## License
|
|
128
|
+
|
|
129
|
+
MIT
|
saturday-0.1.0/README.md
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# Saturday Python SDK
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/saturday/)
|
|
4
|
+
[](LICENSE)
|
|
5
|
+
|
|
6
|
+
Official Python SDK for the [Saturday Nutrition Intelligence API](https://docs.saturday.fit).
|
|
7
|
+
|
|
8
|
+
Personalized fuel, hydration, and electrolyte prescriptions for endurance athletes.
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
pip install saturday
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Quick Start
|
|
17
|
+
|
|
18
|
+
```python
|
|
19
|
+
from saturday import Saturday
|
|
20
|
+
|
|
21
|
+
client = Saturday(api_key="sat_live_...")
|
|
22
|
+
|
|
23
|
+
# Calculate a nutrition prescription
|
|
24
|
+
prescription = client.nutrition.calculate(
|
|
25
|
+
activity_type="bike",
|
|
26
|
+
duration_min=180,
|
|
27
|
+
athlete_weight_kg=75,
|
|
28
|
+
thermal_stress_level=7,
|
|
29
|
+
is_race=True,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
# Safety metadata is ALWAYS included — athlete safety is never paywalled
|
|
33
|
+
print(prescription["safety"]["warnings"])
|
|
34
|
+
print(f"Carbs: {prescription['carbohydrate']['target_g_per_hr']} g/hr")
|
|
35
|
+
print(f"Sodium: {prescription['sodium']['target_mg_per_hr']} mg/hr")
|
|
36
|
+
print(f"Fluid: {prescription['fluid']['target_ml_per_hr']} mL/hr")
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Features
|
|
40
|
+
|
|
41
|
+
- Fully typed with `py.typed` marker (PEP 561)
|
|
42
|
+
- Automatic retry with exponential backoff (429s and 5xx)
|
|
43
|
+
- Typed errors (`AuthenticationError`, `RateLimitError`, `ValidationError`, `NotFoundError`)
|
|
44
|
+
- API key and OAuth2 Bearer token authentication
|
|
45
|
+
- Context manager support for clean connection handling
|
|
46
|
+
- Safety types prominently surfaced (`not_instructions` documented)
|
|
47
|
+
|
|
48
|
+
## Authentication
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
# API key (server-to-server)
|
|
52
|
+
client = Saturday(api_key="sat_live_...")
|
|
53
|
+
|
|
54
|
+
# OAuth2 Bearer token (athlete-delegated access)
|
|
55
|
+
client = Saturday(api_key="sat_live_...", bearer_token="eyJ...")
|
|
56
|
+
|
|
57
|
+
# Context manager for automatic cleanup
|
|
58
|
+
with Saturday(api_key="sat_live_...") as client:
|
|
59
|
+
rx = client.nutrition.calculate(activity_type="run", duration_min=60)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Error Handling
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
from saturday import Saturday, RateLimitError, ValidationError, NotFoundError
|
|
66
|
+
|
|
67
|
+
client = Saturday(api_key="sat_live_...")
|
|
68
|
+
|
|
69
|
+
try:
|
|
70
|
+
rx = client.nutrition.calculate(activity_type="swim", duration_min=60)
|
|
71
|
+
except ValidationError as e:
|
|
72
|
+
print(f"Invalid request: {e} (param: {e.param})")
|
|
73
|
+
except RateLimitError as e:
|
|
74
|
+
print(f"Rate limited. Retry after {e.retry_after}s")
|
|
75
|
+
except NotFoundError:
|
|
76
|
+
print("Resource not found")
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Resources
|
|
80
|
+
|
|
81
|
+
| Resource | Description |
|
|
82
|
+
|----------|-------------|
|
|
83
|
+
| `client.nutrition` | Calculate prescriptions, batch, compare |
|
|
84
|
+
| `client.athletes` | Athlete CRUD, settings, batch create, GDPR export |
|
|
85
|
+
| `client.activities` | Activity CRUD, prescription calculation, feedback |
|
|
86
|
+
| `client.products` | Product search, barcode lookup, curated list |
|
|
87
|
+
| `client.ai` | AI coaching conversations |
|
|
88
|
+
| `client.webhooks` | Webhook registration and management |
|
|
89
|
+
| `client.organizations` | Team/org management with members |
|
|
90
|
+
| `client.gear` | Athlete gear inventory |
|
|
91
|
+
| `client.knowledge` | Sports nutrition knowledge base search |
|
|
92
|
+
|
|
93
|
+
## Documentation
|
|
94
|
+
|
|
95
|
+
Full API documentation: [docs.saturday.fit](https://docs.saturday.fit)
|
|
96
|
+
|
|
97
|
+
## Requirements
|
|
98
|
+
|
|
99
|
+
- Python 3.9+
|
|
100
|
+
- httpx 0.25+
|
|
101
|
+
|
|
102
|
+
## License
|
|
103
|
+
|
|
104
|
+
MIT
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Security Policy
|
|
2
|
+
|
|
3
|
+
## Reporting a Vulnerability
|
|
4
|
+
|
|
5
|
+
If you discover a security vulnerability in the Saturday SDK, please report it responsibly:
|
|
6
|
+
|
|
7
|
+
**Email**: security@saturday.fit
|
|
8
|
+
**Response time**: We will acknowledge within 48 hours and provide a detailed response within 5 business days.
|
|
9
|
+
|
|
10
|
+
Please do NOT file a public GitHub issue for security vulnerabilities.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "saturday"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Official Python SDK for the Saturday Nutrition Intelligence API"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.9"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Saturday Inc.", email = "api@saturday.fit" },
|
|
14
|
+
]
|
|
15
|
+
keywords = [
|
|
16
|
+
"saturday",
|
|
17
|
+
"nutrition",
|
|
18
|
+
"endurance",
|
|
19
|
+
"hydration",
|
|
20
|
+
"electrolytes",
|
|
21
|
+
"sports-nutrition",
|
|
22
|
+
"api",
|
|
23
|
+
]
|
|
24
|
+
classifiers = [
|
|
25
|
+
"Development Status :: 3 - Alpha",
|
|
26
|
+
"Intended Audience :: Developers",
|
|
27
|
+
"License :: OSI Approved :: MIT License",
|
|
28
|
+
"Programming Language :: Python :: 3",
|
|
29
|
+
"Programming Language :: Python :: 3.9",
|
|
30
|
+
"Programming Language :: Python :: 3.10",
|
|
31
|
+
"Programming Language :: Python :: 3.11",
|
|
32
|
+
"Programming Language :: Python :: 3.12",
|
|
33
|
+
"Programming Language :: Python :: 3.13",
|
|
34
|
+
"Typing :: Typed",
|
|
35
|
+
]
|
|
36
|
+
dependencies = [
|
|
37
|
+
"httpx>=0.25.0",
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
[project.urls]
|
|
41
|
+
Documentation = "https://docs.saturday.fit"
|
|
42
|
+
Repository = "https://github.com/SaturdayInc/saturday-python"
|
|
43
|
+
Issues = "https://github.com/SaturdayInc/saturday-python/issues"
|
|
44
|
+
|
|
45
|
+
[tool.hatch.build.targets.wheel]
|
|
46
|
+
packages = ["saturday"]
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Saturday Nutrition Intelligence API — Official Python SDK
|
|
3
|
+
|
|
4
|
+
Personalized fuel, hydration, and electrolyte prescriptions for endurance
|
|
5
|
+
athletes. Calculate carbohydrate, sodium, and fluid targets based on activity
|
|
6
|
+
type, duration, athlete profile, and environmental conditions.
|
|
7
|
+
|
|
8
|
+
Example::
|
|
9
|
+
|
|
10
|
+
from saturday import Saturday
|
|
11
|
+
|
|
12
|
+
client = Saturday(api_key="sat_live_...")
|
|
13
|
+
|
|
14
|
+
prescription = client.nutrition.calculate(
|
|
15
|
+
activity_type="bike",
|
|
16
|
+
duration_min=180,
|
|
17
|
+
athlete_weight_kg=75.0,
|
|
18
|
+
thermal_stress_level=7,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
# Safety metadata is ALWAYS included — athlete safety cannot be paywalled
|
|
22
|
+
print(prescription["safety"]["warnings"])
|
|
23
|
+
print(f"Carbs: {prescription['carbohydrate']['target_g_per_hr']} g/hr")
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from saturday.client import Saturday
|
|
27
|
+
from saturday.errors import (
|
|
28
|
+
SaturdayError,
|
|
29
|
+
AuthenticationError,
|
|
30
|
+
RateLimitError,
|
|
31
|
+
ValidationError,
|
|
32
|
+
NotFoundError,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
__version__ = "0.1.0"
|
|
36
|
+
__all__ = [
|
|
37
|
+
"Saturday",
|
|
38
|
+
"SaturdayError",
|
|
39
|
+
"AuthenticationError",
|
|
40
|
+
"RateLimitError",
|
|
41
|
+
"ValidationError",
|
|
42
|
+
"NotFoundError",
|
|
43
|
+
]
|
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Saturday API client with automatic retry and typed resource accessors.
|
|
3
|
+
|
|
4
|
+
Safety metadata is ALWAYS included in nutrition responses — athlete safety
|
|
5
|
+
cannot be paywalled. The ``not_instructions`` field is present in every
|
|
6
|
+
nutrition response for AI consumers.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import time
|
|
12
|
+
from typing import Any, Dict, List, Optional
|
|
13
|
+
|
|
14
|
+
import httpx
|
|
15
|
+
|
|
16
|
+
from saturday.errors import SaturdayError
|
|
17
|
+
|
|
18
|
+
SDK_VERSION = "0.1.0"
|
|
19
|
+
DEFAULT_BASE_URL = "https://api.saturday.fit"
|
|
20
|
+
DEFAULT_TIMEOUT = 30.0
|
|
21
|
+
DEFAULT_MAX_RETRIES = 3
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Saturday:
|
|
25
|
+
"""
|
|
26
|
+
Saturday Nutrition Intelligence API client.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
api_key: Your partner API key (sat_live_... or sat_test_...).
|
|
30
|
+
base_url: Base URL override. Defaults to https://api.saturday.fit.
|
|
31
|
+
timeout: Request timeout in seconds. Defaults to 30.
|
|
32
|
+
max_retries: Maximum retry attempts for transient failures. Defaults to 3.
|
|
33
|
+
bearer_token: OAuth2 Bearer token (alternative to API key).
|
|
34
|
+
|
|
35
|
+
Example::
|
|
36
|
+
|
|
37
|
+
client = Saturday(api_key="sat_live_...")
|
|
38
|
+
rx = client.nutrition.calculate(
|
|
39
|
+
activity_type="run",
|
|
40
|
+
duration_min=90,
|
|
41
|
+
thermal_stress_level=8,
|
|
42
|
+
)
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
def __init__(
|
|
46
|
+
self,
|
|
47
|
+
api_key: str,
|
|
48
|
+
*,
|
|
49
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
50
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
51
|
+
max_retries: int = DEFAULT_MAX_RETRIES,
|
|
52
|
+
bearer_token: Optional[str] = None,
|
|
53
|
+
):
|
|
54
|
+
self._api_key = api_key
|
|
55
|
+
self._base_url = base_url.rstrip("/")
|
|
56
|
+
self._timeout = timeout
|
|
57
|
+
self._max_retries = max_retries
|
|
58
|
+
self._bearer_token = bearer_token
|
|
59
|
+
|
|
60
|
+
headers = {
|
|
61
|
+
"User-Agent": f"saturday-python/{SDK_VERSION}",
|
|
62
|
+
"X-SDK-Version": SDK_VERSION,
|
|
63
|
+
"Content-Type": "application/json",
|
|
64
|
+
}
|
|
65
|
+
if bearer_token:
|
|
66
|
+
headers["Authorization"] = f"Bearer {bearer_token}"
|
|
67
|
+
else:
|
|
68
|
+
headers["X-API-Key"] = api_key
|
|
69
|
+
|
|
70
|
+
self._client = httpx.Client(
|
|
71
|
+
base_url=self._base_url,
|
|
72
|
+
headers=headers,
|
|
73
|
+
timeout=timeout,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
# Resource accessors
|
|
77
|
+
self.nutrition = _NutritionResource(self)
|
|
78
|
+
self.athletes = _AthletesResource(self)
|
|
79
|
+
self.activities = _ActivitiesResource(self)
|
|
80
|
+
self.products = _ProductsResource(self)
|
|
81
|
+
self.ai = _AIResource(self)
|
|
82
|
+
self.webhooks = _WebhooksResource(self)
|
|
83
|
+
self.organizations = _OrganizationsResource(self)
|
|
84
|
+
self.gear = _GearResource(self)
|
|
85
|
+
self.knowledge = _KnowledgeResource(self)
|
|
86
|
+
|
|
87
|
+
def close(self) -> None:
|
|
88
|
+
"""Close the underlying HTTP client."""
|
|
89
|
+
self._client.close()
|
|
90
|
+
|
|
91
|
+
def __enter__(self) -> "Saturday":
|
|
92
|
+
return self
|
|
93
|
+
|
|
94
|
+
def __exit__(self, *args: Any) -> None:
|
|
95
|
+
self.close()
|
|
96
|
+
|
|
97
|
+
def request(
|
|
98
|
+
self,
|
|
99
|
+
method: str,
|
|
100
|
+
path: str,
|
|
101
|
+
*,
|
|
102
|
+
json: Optional[Dict[str, Any]] = None,
|
|
103
|
+
params: Optional[Dict[str, Any]] = None,
|
|
104
|
+
) -> Any:
|
|
105
|
+
"""Make an authenticated API request with automatic retry on 429/5xx."""
|
|
106
|
+
last_error: Optional[SaturdayError] = None
|
|
107
|
+
|
|
108
|
+
for attempt in range(self._max_retries + 1):
|
|
109
|
+
if attempt > 0:
|
|
110
|
+
delay = (2 ** (attempt - 1))
|
|
111
|
+
time.sleep(delay)
|
|
112
|
+
|
|
113
|
+
try:
|
|
114
|
+
response = self._client.request(
|
|
115
|
+
method,
|
|
116
|
+
path,
|
|
117
|
+
json=json,
|
|
118
|
+
params=params,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
if response.is_success:
|
|
122
|
+
if response.status_code == 204:
|
|
123
|
+
return None
|
|
124
|
+
return response.json()
|
|
125
|
+
|
|
126
|
+
# Parse error
|
|
127
|
+
try:
|
|
128
|
+
error_body = response.json()
|
|
129
|
+
except Exception:
|
|
130
|
+
error_body = {"error": {"type": "api_error", "code": "unknown", "message": response.text}}
|
|
131
|
+
|
|
132
|
+
error = SaturdayError.from_response(response.status_code, error_body)
|
|
133
|
+
last_error = error
|
|
134
|
+
|
|
135
|
+
# Only retry on rate limit or server errors
|
|
136
|
+
if response.status_code == 429 or response.status_code >= 500:
|
|
137
|
+
continue
|
|
138
|
+
|
|
139
|
+
raise error
|
|
140
|
+
|
|
141
|
+
except httpx.TimeoutException:
|
|
142
|
+
raise SaturdayError(
|
|
143
|
+
message=f"Request timed out after {self._timeout}s",
|
|
144
|
+
code="timeout",
|
|
145
|
+
)
|
|
146
|
+
except SaturdayError:
|
|
147
|
+
raise
|
|
148
|
+
except httpx.HTTPError as e:
|
|
149
|
+
raise SaturdayError(message=str(e), code="connection_error")
|
|
150
|
+
|
|
151
|
+
# All retries exhausted
|
|
152
|
+
if last_error:
|
|
153
|
+
raise last_error
|
|
154
|
+
raise SaturdayError(message="Max retries exceeded", code="max_retries_exceeded")
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
# --- Resource Classes ---
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
class _NutritionResource:
|
|
161
|
+
def __init__(self, client: Saturday):
|
|
162
|
+
self._client = client
|
|
163
|
+
|
|
164
|
+
def calculate(self, **kwargs: Any) -> Dict[str, Any]:
|
|
165
|
+
"""Calculate a personalized fuel/hydration/electrolyte prescription."""
|
|
166
|
+
return self._client.request("POST", "/v1/nutrition/calculate", json=kwargs)
|
|
167
|
+
|
|
168
|
+
def batch_calculate(self, scenarios: List[Dict[str, Any]]) -> Dict[str, Any]:
|
|
169
|
+
"""Batch calculate prescriptions for multiple scenarios (max 50)."""
|
|
170
|
+
return self._client.request("POST", "/v1/nutrition/calculate/batch", json={"scenarios": scenarios})
|
|
171
|
+
|
|
172
|
+
def compare(self, scenarios: List[Dict[str, Any]]) -> Dict[str, Any]:
|
|
173
|
+
"""Compare prescriptions across different scenarios."""
|
|
174
|
+
return self._client.request("POST", "/v1/nutrition/calculate/compare", json={"scenarios": scenarios})
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
class _AthletesResource:
|
|
178
|
+
def __init__(self, client: Saturday):
|
|
179
|
+
self._client = client
|
|
180
|
+
|
|
181
|
+
def create(self, **kwargs: Any) -> Dict[str, Any]:
|
|
182
|
+
"""Create a new athlete under your partner account."""
|
|
183
|
+
return self._client.request("POST", "/v1/athletes", json=kwargs)
|
|
184
|
+
|
|
185
|
+
def get(self, athlete_id: str) -> Dict[str, Any]:
|
|
186
|
+
"""Get an athlete by ID."""
|
|
187
|
+
return self._client.request("GET", f"/v1/athletes/{athlete_id}")
|
|
188
|
+
|
|
189
|
+
def list(self, *, limit: int = 50, offset: int = 0, search: Optional[str] = None) -> Dict[str, Any]:
|
|
190
|
+
"""List athletes for your partner account."""
|
|
191
|
+
params: Dict[str, Any] = {"limit": limit, "offset": offset}
|
|
192
|
+
if search:
|
|
193
|
+
params["search"] = search
|
|
194
|
+
return self._client.request("GET", "/v1/athletes", params=params)
|
|
195
|
+
|
|
196
|
+
def update(self, athlete_id: str, **kwargs: Any) -> Dict[str, Any]:
|
|
197
|
+
"""Partially update an athlete's profile."""
|
|
198
|
+
return self._client.request("PATCH", f"/v1/athletes/{athlete_id}", json=kwargs)
|
|
199
|
+
|
|
200
|
+
def delete(self, athlete_id: str) -> None:
|
|
201
|
+
"""Delete an athlete and all associated data."""
|
|
202
|
+
self._client.request("DELETE", f"/v1/athletes/{athlete_id}")
|
|
203
|
+
|
|
204
|
+
def get_settings(self, athlete_id: str) -> Dict[str, Any]:
|
|
205
|
+
"""Get an athlete's fueling preference settings."""
|
|
206
|
+
return self._client.request("GET", f"/v1/athletes/{athlete_id}/settings")
|
|
207
|
+
|
|
208
|
+
def update_settings(self, athlete_id: str, **kwargs: Any) -> Dict[str, Any]:
|
|
209
|
+
"""Update an athlete's fueling preference settings."""
|
|
210
|
+
return self._client.request("PATCH", f"/v1/athletes/{athlete_id}/settings", json=kwargs)
|
|
211
|
+
|
|
212
|
+
def batch_create(self, athletes: List[Dict[str, Any]]) -> Dict[str, Any]:
|
|
213
|
+
"""Batch create up to 100 athletes."""
|
|
214
|
+
return self._client.request("POST", "/v1/athletes/batch", json={"athletes": athletes})
|
|
215
|
+
|
|
216
|
+
def export(self, athlete_id: str) -> Dict[str, Any]:
|
|
217
|
+
"""Export all athlete data (GDPR data portability)."""
|
|
218
|
+
return self._client.request("POST", f"/v1/athletes/{athlete_id}/export")
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
class _ActivitiesResource:
|
|
222
|
+
def __init__(self, client: Saturday):
|
|
223
|
+
self._client = client
|
|
224
|
+
|
|
225
|
+
def create(self, athlete_id: str, **kwargs: Any) -> Dict[str, Any]:
|
|
226
|
+
"""Create a new activity for an athlete."""
|
|
227
|
+
return self._client.request("POST", f"/v1/athletes/{athlete_id}/activities", json=kwargs)
|
|
228
|
+
|
|
229
|
+
def get(self, athlete_id: str, activity_id: str) -> Dict[str, Any]:
|
|
230
|
+
"""Get an activity by ID."""
|
|
231
|
+
return self._client.request("GET", f"/v1/athletes/{athlete_id}/activities/{activity_id}")
|
|
232
|
+
|
|
233
|
+
def list(self, athlete_id: str, *, limit: int = 20, offset: int = 0) -> Dict[str, Any]:
|
|
234
|
+
"""List activities for an athlete."""
|
|
235
|
+
return self._client.request("GET", f"/v1/athletes/{athlete_id}/activities", params={"limit": limit, "offset": offset})
|
|
236
|
+
|
|
237
|
+
def update(self, athlete_id: str, activity_id: str, **kwargs: Any) -> Dict[str, Any]:
|
|
238
|
+
"""Partially update an activity."""
|
|
239
|
+
return self._client.request("PATCH", f"/v1/athletes/{athlete_id}/activities/{activity_id}", json=kwargs)
|
|
240
|
+
|
|
241
|
+
def delete(self, athlete_id: str, activity_id: str) -> None:
|
|
242
|
+
"""Delete an activity and its prescription."""
|
|
243
|
+
self._client.request("DELETE", f"/v1/athletes/{athlete_id}/activities/{activity_id}")
|
|
244
|
+
|
|
245
|
+
def calculate_prescription(self, athlete_id: str, activity_id: str) -> Dict[str, Any]:
|
|
246
|
+
"""Calculate/recalculate a nutrition prescription for this activity."""
|
|
247
|
+
return self._client.request("POST", f"/v1/athletes/{athlete_id}/activities/{activity_id}/prescription")
|
|
248
|
+
|
|
249
|
+
def get_prescription(self, athlete_id: str, activity_id: str) -> Dict[str, Any]:
|
|
250
|
+
"""Get the stored prescription for an activity."""
|
|
251
|
+
return self._client.request("GET", f"/v1/athletes/{athlete_id}/activities/{activity_id}/prescription")
|
|
252
|
+
|
|
253
|
+
def submit_feedback(self, athlete_id: str, activity_id: str, **kwargs: Any) -> Dict[str, Any]:
|
|
254
|
+
"""Submit post-activity feedback on prescription quality."""
|
|
255
|
+
return self._client.request("POST", f"/v1/athletes/{athlete_id}/activities/{activity_id}/feedback", json=kwargs)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
class _ProductsResource:
|
|
259
|
+
def __init__(self, client: Saturday):
|
|
260
|
+
self._client = client
|
|
261
|
+
|
|
262
|
+
def get_by_barcode(self, barcode: str) -> Dict[str, Any]:
|
|
263
|
+
"""Look up a product by barcode."""
|
|
264
|
+
return self._client.request("GET", f"/v1/products/{barcode}")
|
|
265
|
+
|
|
266
|
+
def search(self, query: str, *, category: Optional[str] = None, limit: int = 20) -> Dict[str, Any]:
|
|
267
|
+
"""Search the product database."""
|
|
268
|
+
params: Dict[str, Any] = {"q": query, "limit": limit}
|
|
269
|
+
if category:
|
|
270
|
+
params["category"] = category
|
|
271
|
+
return self._client.request("GET", "/v1/products/search", params=params)
|
|
272
|
+
|
|
273
|
+
def list_curated(self, *, category: Optional[str] = None, limit: int = 50) -> Dict[str, Any]:
|
|
274
|
+
"""List Saturday's curated product database."""
|
|
275
|
+
params: Dict[str, Any] = {"limit": limit}
|
|
276
|
+
if category:
|
|
277
|
+
params["category"] = category
|
|
278
|
+
return self._client.request("GET", "/v1/products/curated", params=params)
|
|
279
|
+
|
|
280
|
+
def list_categories(self) -> Dict[str, Any]:
|
|
281
|
+
"""List product categories."""
|
|
282
|
+
return self._client.request("GET", "/v1/products/categories")
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
class _AIResource:
|
|
286
|
+
def __init__(self, client: Saturday):
|
|
287
|
+
self._client = client
|
|
288
|
+
|
|
289
|
+
def create_conversation(self, athlete_id: str, initial_message: Optional[str] = None) -> Dict[str, Any]:
|
|
290
|
+
"""Start a new AI coaching conversation for an athlete."""
|
|
291
|
+
body: Dict[str, Any] = {"athlete_id": athlete_id}
|
|
292
|
+
if initial_message:
|
|
293
|
+
body["initial_message"] = initial_message
|
|
294
|
+
return self._client.request("POST", "/v1/ai/conversations", json=body)
|
|
295
|
+
|
|
296
|
+
def send_message(self, conv_id: str, message: str) -> Dict[str, Any]:
|
|
297
|
+
"""Send a message and receive the AI response."""
|
|
298
|
+
return self._client.request("POST", f"/v1/ai/conversations/{conv_id}/messages", json={"message": message})
|
|
299
|
+
|
|
300
|
+
def get_messages(self, conv_id: str, *, limit: int = 50) -> Dict[str, Any]:
|
|
301
|
+
"""Get conversation history."""
|
|
302
|
+
return self._client.request("GET", f"/v1/ai/conversations/{conv_id}/messages", params={"limit": limit})
|
|
303
|
+
|
|
304
|
+
def get_conversation(self, conv_id: str) -> Dict[str, Any]:
|
|
305
|
+
"""Get conversation metadata."""
|
|
306
|
+
return self._client.request("GET", f"/v1/ai/conversations/{conv_id}")
|
|
307
|
+
|
|
308
|
+
def delete_conversation(self, conv_id: str) -> None:
|
|
309
|
+
"""Delete a conversation."""
|
|
310
|
+
self._client.request("DELETE", f"/v1/ai/conversations/{conv_id}")
|
|
311
|
+
|
|
312
|
+
def list_conversations(self, athlete_id: str, *, limit: int = 20) -> Dict[str, Any]:
|
|
313
|
+
"""List conversations for an athlete."""
|
|
314
|
+
return self._client.request("GET", f"/v1/athletes/{athlete_id}/ai/conversations", params={"limit": limit})
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
class _WebhooksResource:
|
|
318
|
+
def __init__(self, client: Saturday):
|
|
319
|
+
self._client = client
|
|
320
|
+
|
|
321
|
+
def create(self, url: str, events: List[str], description: Optional[str] = None) -> Dict[str, Any]:
|
|
322
|
+
"""Register a webhook endpoint."""
|
|
323
|
+
body: Dict[str, Any] = {"url": url, "events": events}
|
|
324
|
+
if description:
|
|
325
|
+
body["description"] = description
|
|
326
|
+
return self._client.request("POST", "/v1/webhooks", json=body)
|
|
327
|
+
|
|
328
|
+
def list(self) -> Dict[str, Any]:
|
|
329
|
+
"""List registered webhooks."""
|
|
330
|
+
return self._client.request("GET", "/v1/webhooks")
|
|
331
|
+
|
|
332
|
+
def get(self, webhook_id: str) -> Dict[str, Any]:
|
|
333
|
+
"""Get webhook details."""
|
|
334
|
+
return self._client.request("GET", f"/v1/webhooks/{webhook_id}")
|
|
335
|
+
|
|
336
|
+
def update(self, webhook_id: str, **kwargs: Any) -> Dict[str, Any]:
|
|
337
|
+
"""Update a webhook."""
|
|
338
|
+
return self._client.request("PATCH", f"/v1/webhooks/{webhook_id}", json=kwargs)
|
|
339
|
+
|
|
340
|
+
def delete(self, webhook_id: str) -> None:
|
|
341
|
+
"""Delete a webhook."""
|
|
342
|
+
self._client.request("DELETE", f"/v1/webhooks/{webhook_id}")
|
|
343
|
+
|
|
344
|
+
def test(self, webhook_id: str) -> Dict[str, Any]:
|
|
345
|
+
"""Send a test event."""
|
|
346
|
+
return self._client.request("POST", f"/v1/webhooks/{webhook_id}/test")
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
class _OrganizationsResource:
|
|
350
|
+
def __init__(self, client: Saturday):
|
|
351
|
+
self._client = client
|
|
352
|
+
|
|
353
|
+
def create(self, display_name: str, **kwargs: Any) -> Dict[str, Any]:
|
|
354
|
+
"""Create an organization."""
|
|
355
|
+
return self._client.request("POST", "/v1/organizations", json={"display_name": display_name, **kwargs})
|
|
356
|
+
|
|
357
|
+
def list(self) -> Dict[str, Any]:
|
|
358
|
+
"""List organizations."""
|
|
359
|
+
return self._client.request("GET", "/v1/organizations")
|
|
360
|
+
|
|
361
|
+
def get(self, org_id: str) -> Dict[str, Any]:
|
|
362
|
+
"""Get organization details."""
|
|
363
|
+
return self._client.request("GET", f"/v1/organizations/{org_id}")
|
|
364
|
+
|
|
365
|
+
def add_member(self, org_id: str, email: str, role: str, athlete_id: Optional[str] = None) -> Dict[str, Any]:
|
|
366
|
+
"""Add a member to an organization."""
|
|
367
|
+
body: Dict[str, Any] = {"email": email, "role": role}
|
|
368
|
+
if athlete_id:
|
|
369
|
+
body["athlete_id"] = athlete_id
|
|
370
|
+
return self._client.request("POST", f"/v1/organizations/{org_id}/members", json=body)
|
|
371
|
+
|
|
372
|
+
def list_members(self, org_id: str) -> Dict[str, Any]:
|
|
373
|
+
"""List organization members."""
|
|
374
|
+
return self._client.request("GET", f"/v1/organizations/{org_id}/members")
|
|
375
|
+
|
|
376
|
+
def remove_member(self, org_id: str, member_id: str) -> None:
|
|
377
|
+
"""Remove a member from an organization."""
|
|
378
|
+
self._client.request("DELETE", f"/v1/organizations/{org_id}/members/{member_id}")
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
class _GearResource:
|
|
382
|
+
def __init__(self, client: Saturday):
|
|
383
|
+
self._client = client
|
|
384
|
+
|
|
385
|
+
def list(self, athlete_id: str) -> Dict[str, Any]:
|
|
386
|
+
"""List athlete's gear."""
|
|
387
|
+
return self._client.request("GET", f"/v1/athletes/{athlete_id}/gear")
|
|
388
|
+
|
|
389
|
+
def create(self, athlete_id: str, **kwargs: Any) -> Dict[str, Any]:
|
|
390
|
+
"""Add a gear item."""
|
|
391
|
+
return self._client.request("POST", f"/v1/athletes/{athlete_id}/gear", json=kwargs)
|
|
392
|
+
|
|
393
|
+
def update(self, athlete_id: str, gear_id: str, **kwargs: Any) -> Dict[str, Any]:
|
|
394
|
+
"""Update a gear item."""
|
|
395
|
+
return self._client.request("PATCH", f"/v1/athletes/{athlete_id}/gear/{gear_id}", json=kwargs)
|
|
396
|
+
|
|
397
|
+
def delete(self, athlete_id: str, gear_id: str) -> None:
|
|
398
|
+
"""Delete a gear item."""
|
|
399
|
+
self._client.request("DELETE", f"/v1/athletes/{athlete_id}/gear/{gear_id}")
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
class _KnowledgeResource:
|
|
403
|
+
def __init__(self, client: Saturday):
|
|
404
|
+
self._client = client
|
|
405
|
+
|
|
406
|
+
def search(self, query: str, *, limit: int = 5, category: Optional[str] = None) -> Dict[str, Any]:
|
|
407
|
+
"""Search the nutrition knowledge base."""
|
|
408
|
+
body: Dict[str, Any] = {"query": query, "limit": limit}
|
|
409
|
+
if category:
|
|
410
|
+
body["category"] = category
|
|
411
|
+
return self._client.request("POST", "/v1/knowledge/search", json=body)
|
|
412
|
+
|
|
413
|
+
def list_topics(self) -> Dict[str, Any]:
|
|
414
|
+
"""List knowledge base topics."""
|
|
415
|
+
return self._client.request("GET", "/v1/knowledge/topics")
|
|
416
|
+
|
|
417
|
+
def get_article(self, article_id: str) -> Dict[str, Any]:
|
|
418
|
+
"""Get a knowledge article."""
|
|
419
|
+
return self._client.request("GET", f"/v1/knowledge/articles/{article_id}")
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Typed error classes for Saturday API errors.
|
|
3
|
+
|
|
4
|
+
All errors follow the Stripe-style format with type, code, message,
|
|
5
|
+
and optional param/documentation_url fields.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
from typing import Optional
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class SaturdayError(Exception):
|
|
13
|
+
"""Base error for all Saturday API errors."""
|
|
14
|
+
|
|
15
|
+
def __init__(
|
|
16
|
+
self,
|
|
17
|
+
message: str,
|
|
18
|
+
status: int = 0,
|
|
19
|
+
error_type: str = "api_error",
|
|
20
|
+
code: str = "unknown",
|
|
21
|
+
param: Optional[str] = None,
|
|
22
|
+
request_id: Optional[str] = None,
|
|
23
|
+
):
|
|
24
|
+
super().__init__(message)
|
|
25
|
+
self.status = status
|
|
26
|
+
self.error_type = error_type
|
|
27
|
+
self.code = code
|
|
28
|
+
self.param = param
|
|
29
|
+
self.request_id = request_id
|
|
30
|
+
|
|
31
|
+
@classmethod
|
|
32
|
+
def from_response(cls, status: int, error_body: dict) -> "SaturdayError":
|
|
33
|
+
"""Create the appropriate error subclass from an API error response."""
|
|
34
|
+
detail = error_body.get("error", error_body)
|
|
35
|
+
msg = detail.get("message", "Unknown error")
|
|
36
|
+
kwargs = {
|
|
37
|
+
"message": msg,
|
|
38
|
+
"status": status,
|
|
39
|
+
"error_type": detail.get("type", "api_error"),
|
|
40
|
+
"code": detail.get("code", "unknown"),
|
|
41
|
+
"param": detail.get("param"),
|
|
42
|
+
"request_id": detail.get("request_id"),
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if status == 401:
|
|
46
|
+
return AuthenticationError(**kwargs)
|
|
47
|
+
elif status == 404:
|
|
48
|
+
return NotFoundError(**kwargs)
|
|
49
|
+
elif status == 429:
|
|
50
|
+
return RateLimitError(**kwargs)
|
|
51
|
+
elif status in (400, 422):
|
|
52
|
+
return ValidationError(**kwargs)
|
|
53
|
+
else:
|
|
54
|
+
return cls(**kwargs)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class AuthenticationError(SaturdayError):
|
|
58
|
+
"""Missing or invalid API key / Bearer token."""
|
|
59
|
+
pass
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class RateLimitError(SaturdayError):
|
|
63
|
+
"""Rate limit exceeded. Check retry_after for seconds to wait."""
|
|
64
|
+
|
|
65
|
+
def __init__(self, *args, retry_after: int = 60, **kwargs):
|
|
66
|
+
super().__init__(*args, **kwargs)
|
|
67
|
+
self.retry_after = retry_after
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class ValidationError(SaturdayError):
|
|
71
|
+
"""Request validation failed (400 or 422)."""
|
|
72
|
+
pass
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class NotFoundError(SaturdayError):
|
|
76
|
+
"""Resource not found (404)."""
|
|
77
|
+
pass
|
|
File without changes
|