shellsmith 0.5.0__tar.gz → 0.6.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.
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: shellsmith
3
- Version: 0.5.0
3
+ Version: 0.6.0
4
4
  Summary: A Python SDK for Eclipse BaSyx to manage Asset Administration Shells (AAS)
5
5
  Project-URL: Homepage, https://github.com/SmartFactory-KL/shellsmith
6
6
  Project-URL: Issues, https://github.com/SmartFactory-KL/shellsmith/issues
@@ -107,3 +107,6 @@ ignore = [
107
107
 
108
108
  [tool.ruff.lint.pydocstyle]
109
109
  convention = "google"
110
+
111
+ [tool.ruff.lint.pylint]
112
+ max-args = 6
@@ -0,0 +1,202 @@
1
+ """Sync and async auth."""
2
+
3
+ import time
4
+ import typing
5
+ from dataclasses import dataclass
6
+
7
+ import httpx
8
+ from httpx import Request, Response
9
+ from typing_extensions import override
10
+
11
+ from shellsmith.config import config
12
+
13
+
14
+ @dataclass
15
+ class UserAuthentication:
16
+ """Authentication data for user-based authentication.
17
+
18
+ Args:
19
+ username: Username used for authentication.
20
+ password: Password used for authentication.
21
+ """
22
+
23
+ username: str
24
+ password: str
25
+
26
+
27
+ @dataclass
28
+ class ClientAuthentication:
29
+ """Authentication data for client-based authentication.
30
+
31
+ Args:
32
+ client_id: The unique identifier of the client used for authentication.
33
+ client_secret: The secret associated with the client used for authentication.
34
+ """
35
+
36
+ client_id: str
37
+ client_secret: str
38
+
39
+
40
+ class TokenProvider:
41
+ """Base class for token providers.
42
+
43
+ To implement a custom token provider scheme, subclass `TokenProvider`
44
+ and provide data.
45
+ """
46
+
47
+ def __init__(
48
+ self,
49
+ token_url: str,
50
+ data: dict[str, str],
51
+ timeout: float = config.timeout,
52
+ ) -> None:
53
+ """Initialize a token provider.
54
+
55
+ Args:
56
+ token_url: URL of the token endpoint.
57
+ data: Data required to request an access token,
58
+ including the grant type and authentication credentials.
59
+ timeout: Request timeout in seconds.
60
+ """
61
+ self._token_url = token_url
62
+ self._timeout = timeout
63
+
64
+ self._token = None
65
+ self._expires_at = 0
66
+
67
+ self._data = data
68
+
69
+ def _token_valid(self) -> bool:
70
+ """Return whether the current token exists and has not expired."""
71
+ return self._token and time.time() < self._expires_at
72
+
73
+ def _save_token(self, data: dict[str, typing.Any]) -> None:
74
+ """Save the access token and calculate its expiration time.
75
+
76
+ Args:
77
+ data: Token response containing ``access_token`` and optionally
78
+ ``expires_in`` in seconds.
79
+ """
80
+ self._token = data["access_token"]
81
+ self._expires_at = time.time() + data.get("expires_in", 3600)
82
+
83
+ def sync_get_token(self) -> str:
84
+ """Return a valid access token synchronously, refreshing it when necessary."""
85
+ if self._token_valid():
86
+ return self._token
87
+
88
+ with httpx.Client() as client:
89
+ response = client.post(
90
+ self._token_url, timeout=self._timeout, data=self._data
91
+ )
92
+ response.raise_for_status()
93
+ self._save_token(response.json())
94
+
95
+ return self._token
96
+
97
+ async def async_get_token(self) -> str:
98
+ """Return a valid access token asynchronously, refreshing it when necessary."""
99
+ if self._token_valid():
100
+ return self._token
101
+
102
+ async with httpx.AsyncClient() as client:
103
+ r = await client.post(
104
+ self._token_url, timeout=self._timeout, data=self._data
105
+ )
106
+ r.raise_for_status()
107
+ payload = r.json()
108
+
109
+ self._save_token(payload)
110
+ return self._token
111
+
112
+
113
+ class PasswordTokenProvider(TokenProvider):
114
+ """Token provider using password grant type (see https://www.rfc-editor.org/info/rfc6749/#section-4.3)."""
115
+
116
+ def __init__(
117
+ self,
118
+ token_url: str,
119
+ user_authentication: UserAuthentication,
120
+ client_authentication: ClientAuthentication | None = None,
121
+ timeout: float = config.timeout,
122
+ ) -> None:
123
+ """Initialize a password token provider.
124
+
125
+ Args:
126
+ token_url: URL of the token endpoint.
127
+ user_authentication: Username and password.
128
+ client_authentication: Optional client identifier and client secret.
129
+ timeout: Request timeout in seconds.
130
+ """
131
+ _data = {
132
+ "username": user_authentication.username,
133
+ "password": user_authentication.password,
134
+ "grant_type": "password",
135
+ }
136
+
137
+ if client_authentication:
138
+ _data.update(
139
+ {
140
+ "client_id": client_authentication.client_id,
141
+ "client_secret": client_authentication.client_secret,
142
+ }
143
+ )
144
+
145
+ super().__init__(token_url=token_url, data=_data, timeout=timeout)
146
+
147
+
148
+ class ClientCredentialsTokenProvider(TokenProvider):
149
+ """Token provider using client credentials grant type (see https://www.rfc-editor.org/info/rfc6749/#section-2.3)."""
150
+
151
+ def __init__(
152
+ self,
153
+ token_url: str,
154
+ client_authentication: ClientAuthentication,
155
+ timeout: float = config.timeout,
156
+ ) -> None:
157
+ """Initialize a client credentials token provider.
158
+
159
+ Args:
160
+ token_url: URL of the token endpoint.
161
+ client_authentication: Client identifier and client secret.
162
+ timeout: Request timeout in seconds.
163
+ """
164
+ super().__init__(
165
+ token_url=token_url,
166
+ data={
167
+ "client_id": client_authentication.client_id,
168
+ "client_secret": client_authentication.client_secret,
169
+ "grant_type": "client_credentials",
170
+ },
171
+ timeout=timeout,
172
+ )
173
+
174
+
175
+ class Auth(httpx.Auth):
176
+ """Authentication handler that adds an access token to httpx.request."""
177
+
178
+ def __init__(self, token_provider: "TokenProvider") -> None:
179
+ """Initialize the authentication handler.
180
+
181
+ Args:
182
+ token_provider: Provider used to obtain an access token.
183
+ """
184
+ self.token_provider = token_provider
185
+
186
+ @override
187
+ def sync_auth_flow(
188
+ self, request: Request
189
+ ) -> typing.Generator[Request, Response, None]:
190
+ """Synchronously fetch a token and add it to the request header."""
191
+ token = self.token_provider.sync_get_token()
192
+ request.headers["Authorization"] = f"Bearer {token}"
193
+ yield request
194
+
195
+ @override
196
+ async def async_auth_flow(
197
+ self, request: Request
198
+ ) -> typing.Generator[Request, Response, None]:
199
+ """Asynchronously fetch a token and add it to the request header."""
200
+ token = await self.token_provider.async_get_token()
201
+ request.headers["Authorization"] = f"Bearer {token}"
202
+ yield request
@@ -9,6 +9,7 @@ from urllib.parse import quote
9
9
  import httpx
10
10
  from httpx import Response
11
11
 
12
+ from shellsmith.auth import Auth
12
13
  from shellsmith.config import config
13
14
  from shellsmith.types import JSON
14
15
  from shellsmith.utils import base64_encoded
@@ -21,20 +22,23 @@ class AsyncClient:
21
22
  self,
22
23
  host: str = config.host,
23
24
  timeout: float = config.timeout,
25
+ auth: Auth | None = None,
24
26
  ) -> None:
25
27
  """Initialize async client.
26
28
 
27
29
  Args:
28
30
  host: Base URL of the AAS server. Defaults to configured host.
29
31
  timeout: Request timeout in seconds.
32
+ auth: Authentication configuration for requests.
30
33
  """
31
34
  self.host = host
32
35
  self.timeout = timeout
33
36
  self._client: httpx.AsyncClient | None = None
37
+ self._auth = auth
34
38
 
35
39
  async def __aenter__(self) -> "AsyncClient":
36
40
  """Enter async context manager."""
37
- self._client = httpx.AsyncClient(timeout=self.timeout)
41
+ self._client = httpx.AsyncClient(timeout=self.timeout, auth=self._auth)
38
42
  return self
39
43
 
40
44
  async def __aexit__(
@@ -705,20 +709,23 @@ class Client:
705
709
  self,
706
710
  host: str = config.host,
707
711
  timeout: float = config.timeout,
712
+ auth: Auth | None = None,
708
713
  ) -> None:
709
714
  """Initialize sync client.
710
715
 
711
716
  Args:
712
717
  host: Base URL of the AAS server. Defaults to configured host.
713
718
  timeout: Request timeout in seconds.
719
+ auth: Authentication configuration for requests.
714
720
  """
715
721
  self.host = host
716
722
  self.timeout = timeout
717
723
  self._client: httpx.Client | None = None
724
+ self._auth = auth
718
725
 
719
726
  def __enter__(self) -> "Client":
720
727
  """Enter context manager."""
721
- self._client = httpx.Client(timeout=self.timeout)
728
+ self._client = httpx.Client(timeout=self.timeout, auth=self._auth)
722
729
  return self
723
730
 
724
731
  def __exit__(
File without changes
File without changes
File without changes