boarddata 3.0.2__tar.gz → 3.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.
- {boarddata-3.0.2 → boarddata-3.1.0}/PKG-INFO +1 -1
- {boarddata-3.0.2 → boarddata-3.1.0}/__init__.py +3 -2
- {boarddata-3.0.2 → boarddata-3.1.0}/_base.py +17 -4
- {boarddata-3.0.2 → boarddata-3.1.0}/boarddata.egg-info/PKG-INFO +1 -1
- boarddata-3.1.0/cache.py +167 -0
- {boarddata-3.0.2 → boarddata-3.1.0}/pyproject.toml +1 -1
- boarddata-3.0.2/cache.py +0 -39
- {boarddata-3.0.2 → boarddata-3.1.0}/CLAUDE.md +0 -0
- {boarddata-3.0.2 → boarddata-3.1.0}/README.md +0 -0
- {boarddata-3.0.2 → boarddata-3.1.0}/_assemblies.py +0 -0
- {boarddata-3.0.2 → boarddata-3.1.0}/_auditors.py +0 -0
- {boarddata-3.0.2 → boarddata-3.1.0}/_comex.py +0 -0
- {boarddata-3.0.2 → boarddata-3.1.0}/_companies.py +0 -0
- {boarddata-3.0.2 → boarddata-3.1.0}/_directors.py +0 -0
- {boarddata-3.0.2 → boarddata-3.1.0}/_documents.py +0 -0
- {boarddata-3.0.2 → boarddata-3.1.0}/_esg.py +0 -0
- {boarddata-3.0.2 → boarddata-3.1.0}/_persons.py +0 -0
- {boarddata-3.0.2 → boarddata-3.1.0}/_sentinel.py +0 -0
- {boarddata-3.0.2 → boarddata-3.1.0}/_utilities.py +0 -0
- {boarddata-3.0.2 → boarddata-3.1.0}/boarddata.egg-info/SOURCES.txt +0 -0
- {boarddata-3.0.2 → boarddata-3.1.0}/boarddata.egg-info/dependency_links.txt +0 -0
- {boarddata-3.0.2 → boarddata-3.1.0}/boarddata.egg-info/requires.txt +0 -0
- {boarddata-3.0.2 → boarddata-3.1.0}/boarddata.egg-info/top_level.txt +0 -0
- {boarddata-3.0.2 → boarddata-3.1.0}/client.py +0 -0
- {boarddata-3.0.2 → boarddata-3.1.0}/errors.py +0 -0
- {boarddata-3.0.2 → boarddata-3.1.0}/py.typed +0 -0
- {boarddata-3.0.2 → boarddata-3.1.0}/setup.cfg +0 -0
- {boarddata-3.0.2 → boarddata-3.1.0}/tests/__init__.py +0 -0
- {boarddata-3.0.2 → boarddata-3.1.0}/tests/conftest.py +0 -0
- {boarddata-3.0.2 → boarddata-3.1.0}/tests/test_assemblies.py +0 -0
- {boarddata-3.0.2 → boarddata-3.1.0}/tests/test_auditors.py +0 -0
- {boarddata-3.0.2 → boarddata-3.1.0}/tests/test_base.py +0 -0
- {boarddata-3.0.2 → boarddata-3.1.0}/tests/test_build_payload.py +0 -0
- {boarddata-3.0.2 → boarddata-3.1.0}/tests/test_cache.py +0 -0
- {boarddata-3.0.2 → boarddata-3.1.0}/tests/test_comex.py +0 -0
- {boarddata-3.0.2 → boarddata-3.1.0}/tests/test_companies.py +0 -0
- {boarddata-3.0.2 → boarddata-3.1.0}/tests/test_config.py +0 -0
- {boarddata-3.0.2 → boarddata-3.1.0}/tests/test_directors.py +0 -0
- {boarddata-3.0.2 → boarddata-3.1.0}/tests/test_documents.py +0 -0
- {boarddata-3.0.2 → boarddata-3.1.0}/tests/test_esg.py +0 -0
- {boarddata-3.0.2 → boarddata-3.1.0}/tests/test_persons.py +0 -0
- {boarddata-3.0.2 → boarddata-3.1.0}/tests/test_sentinel.py +0 -0
- {boarddata-3.0.2 → boarddata-3.1.0}/tests/test_utilities.py +0 -0
- {boarddata-3.0.2 → boarddata-3.1.0}/types/__init__.py +0 -0
- {boarddata-3.0.2 → boarddata-3.1.0}/types/assemblies.py +0 -0
- {boarddata-3.0.2 → boarddata-3.1.0}/types/auditors.py +0 -0
- {boarddata-3.0.2 → boarddata-3.1.0}/types/comex.py +0 -0
- {boarddata-3.0.2 → boarddata-3.1.0}/types/companies.py +0 -0
- {boarddata-3.0.2 → boarddata-3.1.0}/types/core.py +0 -0
- {boarddata-3.0.2 → boarddata-3.1.0}/types/directors.py +0 -0
- {boarddata-3.0.2 → boarddata-3.1.0}/types/documents.py +0 -0
- {boarddata-3.0.2 → boarddata-3.1.0}/types/esg.py +0 -0
- {boarddata-3.0.2 → boarddata-3.1.0}/types/persons.py +0 -0
- {boarddata-3.0.2 → boarddata-3.1.0}/types/sentinel.py +0 -0
|
@@ -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__ = "3.0
|
|
10
|
+
__version__ = "3.1.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
|
]
|
|
@@ -35,11 +35,13 @@ class Base:
|
|
|
35
35
|
|
|
36
36
|
DEFAULT_AUDIENCE = "https://api.boarddata.scalens.com"
|
|
37
37
|
|
|
38
|
+
_DEFAULT_CACHE = object() # sentinel for auto-detection
|
|
39
|
+
|
|
38
40
|
@classmethod
|
|
39
41
|
def from_env(
|
|
40
42
|
cls,
|
|
41
43
|
*,
|
|
42
|
-
token_cache: TokenCache | str | None =
|
|
44
|
+
token_cache: TokenCache | str | None | object = _DEFAULT_CACHE,
|
|
43
45
|
timeout: int = 30,
|
|
44
46
|
) -> Base:
|
|
45
47
|
"""Create a client from environment variables.
|
|
@@ -53,7 +55,10 @@ class Base:
|
|
|
53
55
|
Args:
|
|
54
56
|
token_cache: File path string (e.g. ``"~/.boarddata/token.json"``)
|
|
55
57
|
or a ``TokenCache`` instance. When a string is passed, a
|
|
56
|
-
``FileTokenCache`` is created automatically.
|
|
58
|
+
``FileTokenCache`` is created automatically. Defaults to
|
|
59
|
+
auto-detection: uses Scaleway Secret Manager if ``SCW_SECRET_KEY``
|
|
60
|
+
and ``SCW_PROJECT_ID`` are set, otherwise a local file cache at
|
|
61
|
+
``/tmp/boarddata_token.json``. Pass ``None`` to disable caching.
|
|
57
62
|
timeout: HTTP request timeout in seconds.
|
|
58
63
|
|
|
59
64
|
Returns:
|
|
@@ -64,7 +69,7 @@ class Base:
|
|
|
64
69
|
"""
|
|
65
70
|
import os
|
|
66
71
|
|
|
67
|
-
from .cache import FileTokenCache
|
|
72
|
+
from .cache import FileTokenCache, ScalewaySecretTokenCache
|
|
68
73
|
|
|
69
74
|
env_vars = {
|
|
70
75
|
"BOARDDATA_BACKEND_URL": "base_url",
|
|
@@ -80,7 +85,15 @@ class Base:
|
|
|
80
85
|
raise ValueError(msg)
|
|
81
86
|
kwargs[param_name] = value
|
|
82
87
|
|
|
83
|
-
|
|
88
|
+
if token_cache is cls._DEFAULT_CACHE:
|
|
89
|
+
if os.environ.get("SCW_SECRET_KEY") and os.environ.get("SCW_PROJECT_ID"):
|
|
90
|
+
cache: TokenCache | None = ScalewaySecretTokenCache()
|
|
91
|
+
else:
|
|
92
|
+
cache = FileTokenCache("/tmp/boarddata_token.json")
|
|
93
|
+
elif isinstance(token_cache, str):
|
|
94
|
+
cache = FileTokenCache(token_cache)
|
|
95
|
+
else:
|
|
96
|
+
cache = token_cache # type: ignore[assignment]
|
|
84
97
|
|
|
85
98
|
return cls(
|
|
86
99
|
**kwargs,
|
boarddata-3.1.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")
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|