arccos-api 0.2.0__tar.gz → 0.2.1__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.
- {arccos_api-0.2.0 → arccos_api-0.2.1}/PKG-INFO +1 -1
- {arccos_api-0.2.0 → arccos_api-0.2.1}/arccos/__init__.py +21 -0
- {arccos_api-0.2.0 → arccos_api-0.2.1}/arccos/_http.py +16 -3
- {arccos_api-0.2.0 → arccos_api-0.2.1}/arccos/auth.py +1 -1
- {arccos_api-0.2.0 → arccos_api-0.2.1}/arccos/client.py +1 -1
- {arccos_api-0.2.0 → arccos_api-0.2.1}/arccos/exceptions.py +4 -2
- {arccos_api-0.2.0 → arccos_api-0.2.1}/arccos_api.egg-info/PKG-INFO +1 -1
- {arccos_api-0.2.0 → arccos_api-0.2.1}/pyproject.toml +1 -1
- {arccos_api-0.2.0 → arccos_api-0.2.1}/tests/test_exceptions.py +2 -1
- {arccos_api-0.2.0 → arccos_api-0.2.1}/MANIFEST.in +0 -0
- {arccos_api-0.2.0 → arccos_api-0.2.1}/README.md +0 -0
- {arccos_api-0.2.0 → arccos_api-0.2.1}/arccos/__main__.py +0 -0
- {arccos_api-0.2.0 → arccos_api-0.2.1}/arccos/cli/__init__.py +0 -0
- {arccos_api-0.2.0 → arccos_api-0.2.1}/arccos/cli/_helpers.py +0 -0
- {arccos_api-0.2.0 → arccos_api-0.2.1}/arccos/cli/auth.py +0 -0
- {arccos_api-0.2.0 → arccos_api-0.2.1}/arccos/cli/rounds.py +0 -0
- {arccos_api-0.2.0 → arccos_api-0.2.1}/arccos/cli/stats.py +0 -0
- {arccos_api-0.2.0 → arccos_api-0.2.1}/arccos/resources/__init__.py +0 -0
- {arccos_api-0.2.0 → arccos_api-0.2.1}/arccos/resources/clubs.py +0 -0
- {arccos_api-0.2.0 → arccos_api-0.2.1}/arccos/resources/courses.py +0 -0
- {arccos_api-0.2.0 → arccos_api-0.2.1}/arccos/resources/handicap.py +0 -0
- {arccos_api-0.2.0 → arccos_api-0.2.1}/arccos/resources/rounds.py +0 -0
- {arccos_api-0.2.0 → arccos_api-0.2.1}/arccos/resources/stats.py +0 -0
- {arccos_api-0.2.0 → arccos_api-0.2.1}/arccos/types.py +0 -0
- {arccos_api-0.2.0 → arccos_api-0.2.1}/arccos_api.egg-info/SOURCES.txt +0 -0
- {arccos_api-0.2.0 → arccos_api-0.2.1}/arccos_api.egg-info/dependency_links.txt +0 -0
- {arccos_api-0.2.0 → arccos_api-0.2.1}/arccos_api.egg-info/entry_points.txt +0 -0
- {arccos_api-0.2.0 → arccos_api-0.2.1}/arccos_api.egg-info/requires.txt +0 -0
- {arccos_api-0.2.0 → arccos_api-0.2.1}/arccos_api.egg-info/top_level.txt +0 -0
- {arccos_api-0.2.0 → arccos_api-0.2.1}/setup.cfg +0 -0
- {arccos_api-0.2.0 → arccos_api-0.2.1}/tests/test_auth.py +0 -0
- {arccos_api-0.2.0 → arccos_api-0.2.1}/tests/test_cli.py +0 -0
- {arccos_api-0.2.0 → arccos_api-0.2.1}/tests/test_client.py +0 -0
- {arccos_api-0.2.0 → arccos_api-0.2.1}/tests/test_http.py +0 -0
- {arccos_api-0.2.0 → arccos_api-0.2.1}/tests/test_resources.py +0 -0
- {arccos_api-0.2.0 → arccos_api-0.2.1}/tests/test_rounds.py +0 -0
- {arccos_api-0.2.0 → arccos_api-0.2.1}/tests/test_security.py +0 -0
|
@@ -9,6 +9,8 @@ Quick start:
|
|
|
9
9
|
print(client.handicap.current())
|
|
10
10
|
"""
|
|
11
11
|
|
|
12
|
+
import logging as _logging
|
|
13
|
+
import re as _re
|
|
12
14
|
from importlib.metadata import version as _pkg_version
|
|
13
15
|
|
|
14
16
|
from .auth import ArccosAuth
|
|
@@ -21,6 +23,25 @@ from .exceptions import (
|
|
|
21
23
|
ArccosRateLimitError,
|
|
22
24
|
)
|
|
23
25
|
|
|
26
|
+
|
|
27
|
+
class _SensitiveFilter(_logging.Filter):
|
|
28
|
+
"""Redact tokens, access keys, and passwords from log messages."""
|
|
29
|
+
|
|
30
|
+
_PATTERNS = _re.compile(
|
|
31
|
+
r"(Bearer\s+)\S+|"
|
|
32
|
+
r"(eyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.)[A-Za-z0-9_-]+|"
|
|
33
|
+
r"(accessKey[\"']?\s*[:=]\s*[\"']?)\S+",
|
|
34
|
+
_re.IGNORECASE,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
def filter(self, record: _logging.LogRecord) -> bool:
|
|
38
|
+
if isinstance(record.msg, str):
|
|
39
|
+
record.msg = self._PATTERNS.sub(r"\1\2\3[REDACTED]", record.msg)
|
|
40
|
+
return True
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
_logging.getLogger("arccos").addFilter(_SensitiveFilter())
|
|
44
|
+
|
|
24
45
|
__version__ = _pkg_version("arccos-api")
|
|
25
46
|
__all__ = [
|
|
26
47
|
"ArccosClient",
|
|
@@ -7,6 +7,7 @@ Retries transient failures (429, 5xx, timeouts) with exponential backoff.
|
|
|
7
7
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
|
+
import contextlib
|
|
10
11
|
import logging
|
|
11
12
|
import os
|
|
12
13
|
import time
|
|
@@ -44,7 +45,13 @@ class HttpClient:
|
|
|
44
45
|
"Content-Type": "application/json",
|
|
45
46
|
"Accept": "application/json",
|
|
46
47
|
})
|
|
47
|
-
|
|
48
|
+
try:
|
|
49
|
+
self._timeout = int(os.environ.get("ARCCOS_TIMEOUT", DEFAULT_TIMEOUT))
|
|
50
|
+
if self._timeout <= 0:
|
|
51
|
+
raise ValueError
|
|
52
|
+
except (ValueError, TypeError):
|
|
53
|
+
logger.warning("Invalid ARCCOS_TIMEOUT, using default %ds", DEFAULT_TIMEOUT)
|
|
54
|
+
self._timeout = DEFAULT_TIMEOUT
|
|
48
55
|
|
|
49
56
|
# ------------------------------------------------------------------
|
|
50
57
|
# Public interface
|
|
@@ -125,7 +132,7 @@ class HttpClient:
|
|
|
125
132
|
raise_for_status(resp)
|
|
126
133
|
except Exception as exc:
|
|
127
134
|
last_exc = exc
|
|
128
|
-
self._backoff(attempt)
|
|
135
|
+
self._backoff(attempt, resp)
|
|
129
136
|
continue
|
|
130
137
|
|
|
131
138
|
# Non-retryable error (4xx other than 429)
|
|
@@ -135,7 +142,13 @@ class HttpClient:
|
|
|
135
142
|
raise last_exc # type: ignore[misc]
|
|
136
143
|
|
|
137
144
|
@staticmethod
|
|
138
|
-
def _backoff(attempt: int) -> None:
|
|
145
|
+
def _backoff(attempt: int, resp: requests.Response | None = None) -> None:
|
|
139
146
|
delay = RETRY_BACKOFF * (2 ** attempt)
|
|
147
|
+
# Respect Retry-After header if present (429 responses)
|
|
148
|
+
if resp is not None:
|
|
149
|
+
retry_after = resp.headers.get("Retry-After")
|
|
150
|
+
if retry_after:
|
|
151
|
+
with contextlib.suppress(ValueError, TypeError):
|
|
152
|
+
delay = max(float(retry_after), delay)
|
|
140
153
|
logger.warning("Retrying in %.1fs (attempt %d/%d)", delay, attempt + 1, MAX_RETRIES)
|
|
141
154
|
time.sleep(delay)
|
|
@@ -58,10 +58,12 @@ def raise_for_status(response) -> None:
|
|
|
58
58
|
body = response.json()
|
|
59
59
|
error = body.get("error", {})
|
|
60
60
|
code = error.get("code", status)
|
|
61
|
-
|
|
61
|
+
raw = response.text
|
|
62
|
+
message = error.get("description", raw[:200] + ("\u2026" if len(raw) > 200 else ""))
|
|
62
63
|
except Exception:
|
|
63
64
|
code = status
|
|
64
|
-
|
|
65
|
+
raw = response.text
|
|
66
|
+
message = raw[:200] + ("\u2026" if len(raw) > 200 else "")
|
|
65
67
|
|
|
66
68
|
if status == 401 or (isinstance(code, int) and 40100 <= code <= 40199):
|
|
67
69
|
raise ArccosAuthError(message, status_code=status, error_code=code)
|
|
@@ -83,7 +83,8 @@ class TestRaiseForStatus:
|
|
|
83
83
|
resp = _mock_response(500, text="x" * 500)
|
|
84
84
|
with pytest.raises(ArccosError) as exc_info:
|
|
85
85
|
raise_for_status(resp)
|
|
86
|
-
|
|
86
|
+
# 200 chars + ellipsis indicator
|
|
87
|
+
assert len(str(exc_info.value)) <= 201
|
|
87
88
|
|
|
88
89
|
|
|
89
90
|
class TestArccosError:
|
|
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
|
|
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
|