boarddata 3.0.2__tar.gz → 4.0.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.
- {boarddata-3.0.2 → boarddata-4.0.0}/CLAUDE.md +1 -1
- {boarddata-3.0.2 → boarddata-4.0.0}/PKG-INFO +3 -5
- {boarddata-3.0.2 → boarddata-4.0.0}/README.md +2 -4
- {boarddata-3.0.2 → boarddata-4.0.0}/__init__.py +3 -2
- {boarddata-3.0.2 → boarddata-4.0.0}/_base.py +28 -23
- {boarddata-3.0.2 → boarddata-4.0.0}/boarddata.egg-info/PKG-INFO +3 -5
- boarddata-4.0.0/cache.py +167 -0
- {boarddata-3.0.2 → boarddata-4.0.0}/pyproject.toml +1 -1
- {boarddata-3.0.2 → boarddata-4.0.0}/tests/test_config.py +2 -4
- boarddata-3.0.2/cache.py +0 -39
- {boarddata-3.0.2 → boarddata-4.0.0}/_assemblies.py +0 -0
- {boarddata-3.0.2 → boarddata-4.0.0}/_auditors.py +0 -0
- {boarddata-3.0.2 → boarddata-4.0.0}/_comex.py +0 -0
- {boarddata-3.0.2 → boarddata-4.0.0}/_companies.py +0 -0
- {boarddata-3.0.2 → boarddata-4.0.0}/_directors.py +0 -0
- {boarddata-3.0.2 → boarddata-4.0.0}/_documents.py +0 -0
- {boarddata-3.0.2 → boarddata-4.0.0}/_esg.py +0 -0
- {boarddata-3.0.2 → boarddata-4.0.0}/_persons.py +0 -0
- {boarddata-3.0.2 → boarddata-4.0.0}/_sentinel.py +0 -0
- {boarddata-3.0.2 → boarddata-4.0.0}/_utilities.py +0 -0
- {boarddata-3.0.2 → boarddata-4.0.0}/boarddata.egg-info/SOURCES.txt +0 -0
- {boarddata-3.0.2 → boarddata-4.0.0}/boarddata.egg-info/dependency_links.txt +0 -0
- {boarddata-3.0.2 → boarddata-4.0.0}/boarddata.egg-info/requires.txt +0 -0
- {boarddata-3.0.2 → boarddata-4.0.0}/boarddata.egg-info/top_level.txt +0 -0
- {boarddata-3.0.2 → boarddata-4.0.0}/client.py +0 -0
- {boarddata-3.0.2 → boarddata-4.0.0}/errors.py +0 -0
- {boarddata-3.0.2 → boarddata-4.0.0}/py.typed +0 -0
- {boarddata-3.0.2 → boarddata-4.0.0}/setup.cfg +0 -0
- {boarddata-3.0.2 → boarddata-4.0.0}/tests/__init__.py +0 -0
- {boarddata-3.0.2 → boarddata-4.0.0}/tests/conftest.py +0 -0
- {boarddata-3.0.2 → boarddata-4.0.0}/tests/test_assemblies.py +0 -0
- {boarddata-3.0.2 → boarddata-4.0.0}/tests/test_auditors.py +0 -0
- {boarddata-3.0.2 → boarddata-4.0.0}/tests/test_base.py +0 -0
- {boarddata-3.0.2 → boarddata-4.0.0}/tests/test_build_payload.py +0 -0
- {boarddata-3.0.2 → boarddata-4.0.0}/tests/test_cache.py +0 -0
- {boarddata-3.0.2 → boarddata-4.0.0}/tests/test_comex.py +0 -0
- {boarddata-3.0.2 → boarddata-4.0.0}/tests/test_companies.py +0 -0
- {boarddata-3.0.2 → boarddata-4.0.0}/tests/test_directors.py +0 -0
- {boarddata-3.0.2 → boarddata-4.0.0}/tests/test_documents.py +0 -0
- {boarddata-3.0.2 → boarddata-4.0.0}/tests/test_esg.py +0 -0
- {boarddata-3.0.2 → boarddata-4.0.0}/tests/test_persons.py +0 -0
- {boarddata-3.0.2 → boarddata-4.0.0}/tests/test_sentinel.py +0 -0
- {boarddata-3.0.2 → boarddata-4.0.0}/tests/test_utilities.py +0 -0
- {boarddata-3.0.2 → boarddata-4.0.0}/types/__init__.py +0 -0
- {boarddata-3.0.2 → boarddata-4.0.0}/types/assemblies.py +0 -0
- {boarddata-3.0.2 → boarddata-4.0.0}/types/auditors.py +0 -0
- {boarddata-3.0.2 → boarddata-4.0.0}/types/comex.py +0 -0
- {boarddata-3.0.2 → boarddata-4.0.0}/types/companies.py +0 -0
- {boarddata-3.0.2 → boarddata-4.0.0}/types/core.py +0 -0
- {boarddata-3.0.2 → boarddata-4.0.0}/types/directors.py +0 -0
- {boarddata-3.0.2 → boarddata-4.0.0}/types/documents.py +0 -0
- {boarddata-3.0.2 → boarddata-4.0.0}/types/esg.py +0 -0
- {boarddata-3.0.2 → boarddata-4.0.0}/types/persons.py +0 -0
- {boarddata-3.0.2 → boarddata-4.0.0}/types/sentinel.py +0 -0
|
@@ -10,7 +10,7 @@ compensations, assemblies, resolutions, ESG data, and sentinel analyses.
|
|
|
10
10
|
|
|
11
11
|
- **Mixin-based**: Each domain is a mixin class in `_companies.py`, `_persons.py`, etc.
|
|
12
12
|
- **Single facade**: `BoardDataClient` in `client.py` composes all mixins via multiple inheritance.
|
|
13
|
-
- **Base class**: `_base.py` provides
|
|
13
|
+
- **Base class**: `_base.py` provides OAuth2 Client Credentials authentication and HTTP transport (`_get`, `_post`, `_patch`, `_delete`).
|
|
14
14
|
- **Types**: `types/` contains TypedDict definitions for all API request/response shapes.
|
|
15
15
|
|
|
16
16
|
## Key Patterns
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: boarddata
|
|
3
|
-
Version:
|
|
3
|
+
Version: 4.0.0
|
|
4
4
|
Summary: Python SDK for the BoardData V2 REST API
|
|
5
5
|
License: MIT
|
|
6
6
|
Classifier: Development Status :: 4 - Beta
|
|
@@ -43,7 +43,6 @@ client = BoardDataClient.from_env()
|
|
|
43
43
|
# Or explicit configuration
|
|
44
44
|
client = BoardDataClient(
|
|
45
45
|
base_url="https://api.boarddata.scalens.com",
|
|
46
|
-
auth0_domain="your-tenant.auth0.com",
|
|
47
46
|
client_id="your-client-id",
|
|
48
47
|
client_secret="your-client-secret",
|
|
49
48
|
)
|
|
@@ -65,9 +64,8 @@ print(result["id"]) # company UUID
|
|
|
65
64
|
| Variable | Description |
|
|
66
65
|
|---|---|
|
|
67
66
|
| `BOARDDATA_BACKEND_URL` | Base URL of the BoardData API |
|
|
68
|
-
| `
|
|
69
|
-
| `
|
|
70
|
-
| `BOARDDATA_AUTH0_CLIENT_SECRET` | Auth0 client secret |
|
|
67
|
+
| `BOARDDATA_CLIENT_ID` | OAuth2 client ID (from Django admin) |
|
|
68
|
+
| `BOARDDATA_CLIENT_SECRET` | OAuth2 client secret (from Django admin) |
|
|
71
69
|
|
|
72
70
|
## Token Caching
|
|
73
71
|
|
|
@@ -19,7 +19,6 @@ client = BoardDataClient.from_env()
|
|
|
19
19
|
# Or explicit configuration
|
|
20
20
|
client = BoardDataClient(
|
|
21
21
|
base_url="https://api.boarddata.scalens.com",
|
|
22
|
-
auth0_domain="your-tenant.auth0.com",
|
|
23
22
|
client_id="your-client-id",
|
|
24
23
|
client_secret="your-client-secret",
|
|
25
24
|
)
|
|
@@ -41,9 +40,8 @@ print(result["id"]) # company UUID
|
|
|
41
40
|
| Variable | Description |
|
|
42
41
|
|---|---|
|
|
43
42
|
| `BOARDDATA_BACKEND_URL` | Base URL of the BoardData API |
|
|
44
|
-
| `
|
|
45
|
-
| `
|
|
46
|
-
| `BOARDDATA_AUTH0_CLIENT_SECRET` | Auth0 client secret |
|
|
43
|
+
| `BOARDDATA_CLIENT_ID` | OAuth2 client ID (from Django admin) |
|
|
44
|
+
| `BOARDDATA_CLIENT_SECRET` | OAuth2 client secret (from Django admin) |
|
|
47
45
|
|
|
48
46
|
## Token Caching
|
|
49
47
|
|
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
"""BoardData API Python SDK."""
|
|
2
2
|
|
|
3
|
-
from .cache import FileTokenCache
|
|
3
|
+
from .cache import FileTokenCache, ScalewaySecretTokenCache
|
|
4
4
|
from ._base import Base, TokenCache
|
|
5
5
|
from .client import BoardDataClient
|
|
6
6
|
from .errors import BoardDataError
|
|
7
7
|
|
|
8
8
|
from . import types as types # noqa: F401 — makes types accessible as boarddata.types
|
|
9
9
|
|
|
10
|
-
__version__ = "
|
|
10
|
+
__version__ = "4.0.0"
|
|
11
11
|
__all__ = [
|
|
12
12
|
"Base",
|
|
13
13
|
"BoardDataClient",
|
|
14
14
|
"BoardDataError",
|
|
15
15
|
"FileTokenCache",
|
|
16
|
+
"ScalewaySecretTokenCache",
|
|
16
17
|
"TokenCache",
|
|
17
18
|
"__version__",
|
|
18
19
|
]
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""OAuth2, HTTP transport, and shared helpers for BoardDataClient."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
@@ -27,33 +27,35 @@ class TokenCache(Protocol):
|
|
|
27
27
|
|
|
28
28
|
|
|
29
29
|
class Base:
|
|
30
|
-
"""
|
|
30
|
+
"""OAuth2 Client Credentials authentication and HTTP transport layer.
|
|
31
31
|
|
|
32
32
|
All domain mixins inherit from this class and use ``self._get()``,
|
|
33
33
|
``self._post()``, ``self._patch()``, ``self._put()``, ``self._delete()``.
|
|
34
34
|
"""
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
_DEFAULT_CACHE = object() # sentinel for auto-detection
|
|
37
37
|
|
|
38
38
|
@classmethod
|
|
39
39
|
def from_env(
|
|
40
40
|
cls,
|
|
41
41
|
*,
|
|
42
|
-
token_cache: TokenCache | str | None =
|
|
42
|
+
token_cache: TokenCache | str | None | object = _DEFAULT_CACHE,
|
|
43
43
|
timeout: int = 30,
|
|
44
44
|
) -> Base:
|
|
45
45
|
"""Create a client from environment variables.
|
|
46
46
|
|
|
47
47
|
Reads:
|
|
48
48
|
BOARDDATA_BACKEND_URL: Base URL of the BoardData API.
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
BOARDDATA_AUTH0_CLIENT_SECRET: Auth0 client secret.
|
|
49
|
+
BOARDDATA_CLIENT_ID: OAuth2 client ID.
|
|
50
|
+
BOARDDATA_CLIENT_SECRET: OAuth2 client secret.
|
|
52
51
|
|
|
53
52
|
Args:
|
|
54
53
|
token_cache: File path string (e.g. ``"~/.boarddata/token.json"``)
|
|
55
54
|
or a ``TokenCache`` instance. When a string is passed, a
|
|
56
|
-
``FileTokenCache`` is created automatically.
|
|
55
|
+
``FileTokenCache`` is created automatically. Defaults to
|
|
56
|
+
auto-detection: uses Scaleway Secret Manager if ``SCW_SECRET_KEY``
|
|
57
|
+
and ``SCW_PROJECT_ID`` are set, otherwise a local file cache at
|
|
58
|
+
``/tmp/boarddata_token.json``. Pass ``None`` to disable caching.
|
|
57
59
|
timeout: HTTP request timeout in seconds.
|
|
58
60
|
|
|
59
61
|
Returns:
|
|
@@ -64,13 +66,12 @@ class Base:
|
|
|
64
66
|
"""
|
|
65
67
|
import os
|
|
66
68
|
|
|
67
|
-
from .cache import FileTokenCache
|
|
69
|
+
from .cache import FileTokenCache, ScalewaySecretTokenCache
|
|
68
70
|
|
|
69
71
|
env_vars = {
|
|
70
72
|
"BOARDDATA_BACKEND_URL": "base_url",
|
|
71
|
-
"
|
|
72
|
-
"
|
|
73
|
-
"BOARDDATA_AUTH0_CLIENT_SECRET": "client_secret",
|
|
73
|
+
"BOARDDATA_CLIENT_ID": "client_id",
|
|
74
|
+
"BOARDDATA_CLIENT_SECRET": "client_secret",
|
|
74
75
|
}
|
|
75
76
|
kwargs: dict[str, str] = {}
|
|
76
77
|
for env_key, param_name in env_vars.items():
|
|
@@ -80,7 +81,15 @@ class Base:
|
|
|
80
81
|
raise ValueError(msg)
|
|
81
82
|
kwargs[param_name] = value
|
|
82
83
|
|
|
83
|
-
|
|
84
|
+
if token_cache is cls._DEFAULT_CACHE:
|
|
85
|
+
if os.environ.get("SCW_SECRET_KEY") and os.environ.get("SCW_PROJECT_ID"):
|
|
86
|
+
cache: TokenCache | None = ScalewaySecretTokenCache()
|
|
87
|
+
else:
|
|
88
|
+
cache = FileTokenCache("/tmp/boarddata_token.json")
|
|
89
|
+
elif isinstance(token_cache, str):
|
|
90
|
+
cache = FileTokenCache(token_cache)
|
|
91
|
+
else:
|
|
92
|
+
cache = token_cache # type: ignore[assignment]
|
|
84
93
|
|
|
85
94
|
return cls(
|
|
86
95
|
**kwargs,
|
|
@@ -91,18 +100,14 @@ class Base:
|
|
|
91
100
|
def __init__(
|
|
92
101
|
self,
|
|
93
102
|
base_url: str,
|
|
94
|
-
auth0_domain: str,
|
|
95
103
|
client_id: str,
|
|
96
104
|
client_secret: str,
|
|
97
|
-
audience: str = DEFAULT_AUDIENCE,
|
|
98
105
|
timeout: int = 30,
|
|
99
106
|
token_cache: TokenCache | None = None,
|
|
100
107
|
) -> None:
|
|
101
108
|
self.base_url = base_url.rstrip("/")
|
|
102
|
-
self.auth0_domain = auth0_domain
|
|
103
109
|
self.client_id = client_id
|
|
104
110
|
self.client_secret = client_secret
|
|
105
|
-
self.audience = audience
|
|
106
111
|
self.timeout = timeout
|
|
107
112
|
self._token_cache = token_cache
|
|
108
113
|
|
|
@@ -114,7 +119,7 @@ class Base:
|
|
|
114
119
|
# ------------------------------------------------------------------
|
|
115
120
|
|
|
116
121
|
def _get_token(self) -> str:
|
|
117
|
-
"""Obtain or return a cached
|
|
122
|
+
"""Obtain or return a cached OAuth2 access token."""
|
|
118
123
|
if self._token and time.time() < self._token_expires_at:
|
|
119
124
|
return self._token
|
|
120
125
|
|
|
@@ -124,18 +129,18 @@ class Base:
|
|
|
124
129
|
if cached and cached.get("access_token") and time.time() < cached.get("expires_at", 0):
|
|
125
130
|
self._token = cached["access_token"]
|
|
126
131
|
self._token_expires_at = cached["expires_at"]
|
|
127
|
-
logger.debug("Loaded cached
|
|
132
|
+
logger.debug("Loaded cached OAuth2 token from storage")
|
|
128
133
|
return self._token
|
|
129
134
|
except Exception:
|
|
130
135
|
logger.debug("Could not read token from cache")
|
|
131
136
|
|
|
132
137
|
resp = requests.post(
|
|
133
|
-
f"
|
|
134
|
-
|
|
138
|
+
f"{self.base_url}/api/oauth/token/",
|
|
139
|
+
data={
|
|
135
140
|
"client_id": self.client_id,
|
|
136
141
|
"client_secret": self.client_secret,
|
|
137
|
-
"audience": self.audience,
|
|
138
142
|
"grant_type": "client_credentials",
|
|
143
|
+
"scope": "read write",
|
|
139
144
|
},
|
|
140
145
|
timeout=self.timeout,
|
|
141
146
|
)
|
|
@@ -150,7 +155,7 @@ class Base:
|
|
|
150
155
|
"access_token": self._token,
|
|
151
156
|
"expires_at": self._token_expires_at,
|
|
152
157
|
})
|
|
153
|
-
logger.debug("Persisted
|
|
158
|
+
logger.debug("Persisted OAuth2 token to storage")
|
|
154
159
|
except Exception:
|
|
155
160
|
logger.debug("Could not persist token to cache")
|
|
156
161
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: boarddata
|
|
3
|
-
Version:
|
|
3
|
+
Version: 4.0.0
|
|
4
4
|
Summary: Python SDK for the BoardData V2 REST API
|
|
5
5
|
License: MIT
|
|
6
6
|
Classifier: Development Status :: 4 - Beta
|
|
@@ -43,7 +43,6 @@ client = BoardDataClient.from_env()
|
|
|
43
43
|
# Or explicit configuration
|
|
44
44
|
client = BoardDataClient(
|
|
45
45
|
base_url="https://api.boarddata.scalens.com",
|
|
46
|
-
auth0_domain="your-tenant.auth0.com",
|
|
47
46
|
client_id="your-client-id",
|
|
48
47
|
client_secret="your-client-secret",
|
|
49
48
|
)
|
|
@@ -65,9 +64,8 @@ print(result["id"]) # company UUID
|
|
|
65
64
|
| Variable | Description |
|
|
66
65
|
|---|---|
|
|
67
66
|
| `BOARDDATA_BACKEND_URL` | Base URL of the BoardData API |
|
|
68
|
-
| `
|
|
69
|
-
| `
|
|
70
|
-
| `BOARDDATA_AUTH0_CLIENT_SECRET` | Auth0 client secret |
|
|
67
|
+
| `BOARDDATA_CLIENT_ID` | OAuth2 client ID (from Django admin) |
|
|
68
|
+
| `BOARDDATA_CLIENT_SECRET` | OAuth2 client secret (from Django admin) |
|
|
71
69
|
|
|
72
70
|
## Token Caching
|
|
73
71
|
|
boarddata-4.0.0/cache.py
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""Token cache implementations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger("boarddata")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class FileTokenCache:
|
|
14
|
+
"""Persists Auth0 tokens to a local JSON file.
|
|
15
|
+
|
|
16
|
+
Creates parent directories automatically. Reads return None
|
|
17
|
+
if the file is missing or contains invalid JSON.
|
|
18
|
+
|
|
19
|
+
Usage::
|
|
20
|
+
|
|
21
|
+
cache = FileTokenCache("~/.boarddata/token.json")
|
|
22
|
+
bd = BoardDataClient.from_env(token_cache=cache)
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(self, path: str) -> None:
|
|
26
|
+
self._path = str(Path(path).expanduser().resolve())
|
|
27
|
+
|
|
28
|
+
def read(self) -> dict[str, Any] | None:
|
|
29
|
+
"""Return cached token data or None."""
|
|
30
|
+
try:
|
|
31
|
+
return json.loads(Path(self._path).read_text()) # type: ignore[no-any-return]
|
|
32
|
+
except (FileNotFoundError, json.JSONDecodeError):
|
|
33
|
+
return None
|
|
34
|
+
|
|
35
|
+
def write(self, data: dict[str, Any]) -> None:
|
|
36
|
+
"""Persist token data."""
|
|
37
|
+
p = Path(self._path)
|
|
38
|
+
p.parent.mkdir(parents=True, exist_ok=True)
|
|
39
|
+
p.write_text(json.dumps(data))
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class ScalewaySecretTokenCache:
|
|
43
|
+
"""Persists Auth0 tokens to Scaleway Secret Manager.
|
|
44
|
+
|
|
45
|
+
Shares a single cached token across all job runs, so only one
|
|
46
|
+
Auth0 M2M token request is made per token lifetime.
|
|
47
|
+
|
|
48
|
+
Requires ``SCW_SECRET_KEY`` and ``SCW_PROJECT_ID`` env vars (already
|
|
49
|
+
available in Scaleway Job containers).
|
|
50
|
+
|
|
51
|
+
Usage::
|
|
52
|
+
|
|
53
|
+
cache = ScalewaySecretTokenCache(secret_name="boarddata-m2m-token")
|
|
54
|
+
bd = BoardDataClient.from_env(token_cache=cache)
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
_API_BASE = "https://api.scaleway.com/secret-manager/v1beta1/regions/{region}"
|
|
58
|
+
|
|
59
|
+
def __init__(
|
|
60
|
+
self,
|
|
61
|
+
secret_name: str = "boarddata-m2m-token",
|
|
62
|
+
region: str | None = None,
|
|
63
|
+
) -> None:
|
|
64
|
+
import os
|
|
65
|
+
|
|
66
|
+
self._secret_name = secret_name
|
|
67
|
+
self._region = region or os.environ.get("SCW_REGION", "fr-par")
|
|
68
|
+
self._base_url = self._API_BASE.format(region=self._region)
|
|
69
|
+
|
|
70
|
+
def _headers(self) -> dict[str, str]:
|
|
71
|
+
import os
|
|
72
|
+
|
|
73
|
+
secret_key = os.environ.get("SCW_SECRET_KEY", "")
|
|
74
|
+
return {"X-Auth-Token": secret_key}
|
|
75
|
+
|
|
76
|
+
def _project_id(self) -> str:
|
|
77
|
+
import os
|
|
78
|
+
|
|
79
|
+
return os.environ.get("SCW_PROJECT_ID", "")
|
|
80
|
+
|
|
81
|
+
def _find_secret_id(self) -> str | None:
|
|
82
|
+
"""Find the secret ID by name, or None if it doesn't exist."""
|
|
83
|
+
import requests as _requests
|
|
84
|
+
|
|
85
|
+
try:
|
|
86
|
+
resp = _requests.get(
|
|
87
|
+
f"{self._base_url}/secrets",
|
|
88
|
+
headers=self._headers(),
|
|
89
|
+
params={"name": self._secret_name, "project_id": self._project_id()},
|
|
90
|
+
timeout=10,
|
|
91
|
+
)
|
|
92
|
+
if not resp.ok:
|
|
93
|
+
return None
|
|
94
|
+
secrets = resp.json().get("secrets", [])
|
|
95
|
+
return secrets[0]["id"] if secrets else None
|
|
96
|
+
except Exception:
|
|
97
|
+
logger.debug("Failed to look up secret %s", self._secret_name)
|
|
98
|
+
return None
|
|
99
|
+
|
|
100
|
+
def _create_secret(self) -> str | None:
|
|
101
|
+
"""Create the secret and return its ID."""
|
|
102
|
+
import requests as _requests
|
|
103
|
+
|
|
104
|
+
try:
|
|
105
|
+
resp = _requests.post(
|
|
106
|
+
f"{self._base_url}/secrets",
|
|
107
|
+
headers=self._headers(),
|
|
108
|
+
json={
|
|
109
|
+
"name": self._secret_name,
|
|
110
|
+
"project_id": self._project_id(),
|
|
111
|
+
"description": "Cached Auth0 M2M token for BoardData client",
|
|
112
|
+
},
|
|
113
|
+
timeout=10,
|
|
114
|
+
)
|
|
115
|
+
if not resp.ok:
|
|
116
|
+
return None
|
|
117
|
+
return resp.json().get("id")
|
|
118
|
+
except Exception:
|
|
119
|
+
logger.debug("Failed to create secret %s", self._secret_name)
|
|
120
|
+
return None
|
|
121
|
+
|
|
122
|
+
def read(self) -> dict[str, Any] | None:
|
|
123
|
+
"""Read the latest secret version from Scaleway Secret Manager."""
|
|
124
|
+
import base64
|
|
125
|
+
|
|
126
|
+
import requests as _requests
|
|
127
|
+
|
|
128
|
+
secret_id = self._find_secret_id()
|
|
129
|
+
if not secret_id:
|
|
130
|
+
return None
|
|
131
|
+
|
|
132
|
+
try:
|
|
133
|
+
resp = _requests.get(
|
|
134
|
+
f"{self._base_url}/secrets/{secret_id}/versions/latest/access",
|
|
135
|
+
headers=self._headers(),
|
|
136
|
+
timeout=10,
|
|
137
|
+
)
|
|
138
|
+
if not resp.ok:
|
|
139
|
+
return None
|
|
140
|
+
raw = base64.b64decode(resp.json()["data"]).decode()
|
|
141
|
+
return json.loads(raw) # type: ignore[no-any-return]
|
|
142
|
+
except Exception:
|
|
143
|
+
logger.debug("Failed to read token from Scaleway Secret Manager")
|
|
144
|
+
return None
|
|
145
|
+
|
|
146
|
+
def write(self, data: dict[str, Any]) -> None:
|
|
147
|
+
"""Write a new secret version to Scaleway Secret Manager."""
|
|
148
|
+
import base64
|
|
149
|
+
|
|
150
|
+
import requests as _requests
|
|
151
|
+
|
|
152
|
+
secret_id = self._find_secret_id()
|
|
153
|
+
if not secret_id:
|
|
154
|
+
secret_id = self._create_secret()
|
|
155
|
+
if not secret_id:
|
|
156
|
+
return
|
|
157
|
+
|
|
158
|
+
try:
|
|
159
|
+
encoded = base64.b64encode(json.dumps(data).encode()).decode()
|
|
160
|
+
_requests.post(
|
|
161
|
+
f"{self._base_url}/secrets/{secret_id}/versions",
|
|
162
|
+
headers=self._headers(),
|
|
163
|
+
json={"data": encoded},
|
|
164
|
+
timeout=10,
|
|
165
|
+
)
|
|
166
|
+
except Exception:
|
|
167
|
+
logger.debug("Failed to write token to Scaleway Secret Manager")
|
|
@@ -10,9 +10,8 @@ from boarddata.client import BoardDataClient
|
|
|
10
10
|
|
|
11
11
|
ENV = {
|
|
12
12
|
"BOARDDATA_BACKEND_URL": "https://api.example.com",
|
|
13
|
-
"
|
|
14
|
-
"
|
|
15
|
-
"BOARDDATA_AUTH0_CLIENT_SECRET": "csecret",
|
|
13
|
+
"BOARDDATA_CLIENT_ID": "cid",
|
|
14
|
+
"BOARDDATA_CLIENT_SECRET": "csecret",
|
|
16
15
|
}
|
|
17
16
|
|
|
18
17
|
|
|
@@ -21,7 +20,6 @@ class TestFromEnv:
|
|
|
21
20
|
def test_creates_client_from_env(self) -> None:
|
|
22
21
|
client = BoardDataClient.from_env()
|
|
23
22
|
assert client.base_url == "https://api.example.com"
|
|
24
|
-
assert client.auth0_domain == "tenant.eu.auth0.com"
|
|
25
23
|
assert client.client_id == "cid"
|
|
26
24
|
assert client.client_secret == "csecret"
|
|
27
25
|
|
boarddata-3.0.2/cache.py
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
"""Token cache implementations."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import json
|
|
6
|
-
import logging
|
|
7
|
-
from pathlib import Path
|
|
8
|
-
from typing import Any
|
|
9
|
-
|
|
10
|
-
logger = logging.getLogger("boarddata")
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
class FileTokenCache:
|
|
14
|
-
"""Persists Auth0 tokens to a local JSON file.
|
|
15
|
-
|
|
16
|
-
Creates parent directories automatically. Reads return None
|
|
17
|
-
if the file is missing or contains invalid JSON.
|
|
18
|
-
|
|
19
|
-
Usage::
|
|
20
|
-
|
|
21
|
-
cache = FileTokenCache("~/.boarddata/token.json")
|
|
22
|
-
bd = BoardDataClient.from_env(token_cache=cache)
|
|
23
|
-
"""
|
|
24
|
-
|
|
25
|
-
def __init__(self, path: str) -> None:
|
|
26
|
-
self._path = str(Path(path).expanduser().resolve())
|
|
27
|
-
|
|
28
|
-
def read(self) -> dict[str, Any] | None:
|
|
29
|
-
"""Return cached token data or None."""
|
|
30
|
-
try:
|
|
31
|
-
return json.loads(Path(self._path).read_text()) # type: ignore[no-any-return]
|
|
32
|
-
except (FileNotFoundError, json.JSONDecodeError):
|
|
33
|
-
return None
|
|
34
|
-
|
|
35
|
-
def write(self, data: dict[str, Any]) -> None:
|
|
36
|
-
"""Persist token data."""
|
|
37
|
-
p = Path(self._path)
|
|
38
|
-
p.parent.mkdir(parents=True, exist_ok=True)
|
|
39
|
-
p.write_text(json.dumps(data))
|
|
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
|
|
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
|