usso 0.27.11__py3-none-any.whl → 0.27.13__py3-none-any.whl
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.
- usso/core.py +64 -141
- usso/exceptions.py +4 -4
- usso/schemas.py +29 -1
- usso/session/async_session.py +12 -12
- usso/session/base_session.py +6 -0
- usso/session/session.py +12 -13
- {usso-0.27.11.dist-info → usso-0.27.13.dist-info}/METADATA +1 -1
- {usso-0.27.11.dist-info → usso-0.27.13.dist-info}/RECORD +12 -12
- {usso-0.27.11.dist-info → usso-0.27.13.dist-info}/LICENSE.txt +0 -0
- {usso-0.27.11.dist-info → usso-0.27.13.dist-info}/WHEEL +0 -0
- {usso-0.27.11.dist-info → usso-0.27.13.dist-info}/entry_points.txt +0 -0
- {usso-0.27.11.dist-info → usso-0.27.13.dist-info}/top_level.txt +0 -0
usso/core.py
CHANGED
@@ -5,13 +5,11 @@ from datetime import datetime, timedelta
|
|
5
5
|
from urllib.parse import urlparse
|
6
6
|
|
7
7
|
import cachetools.func
|
8
|
+
import httpx
|
8
9
|
import jwt
|
9
|
-
import requests
|
10
|
-
from cachetools import TTLCache, cached
|
11
|
-
from pydantic import BaseModel, model_validator
|
12
10
|
|
13
11
|
from .exceptions import USSOException
|
14
|
-
from .schemas import UserData
|
12
|
+
from .schemas import JWTConfig, UserData
|
15
13
|
|
16
14
|
logger = logging.getLogger("usso")
|
17
15
|
|
@@ -26,36 +24,34 @@ def get_authorization_scheme_param(
|
|
26
24
|
|
27
25
|
|
28
26
|
def decode_token(key, token: str, algorithms=["RS256"], **kwargs) -> dict:
|
27
|
+
"""Decode a JWT token."""
|
29
28
|
try:
|
30
29
|
decoded = jwt.decode(token, key, algorithms=algorithms)
|
31
|
-
decoded
|
32
|
-
decoded["token"] = token
|
30
|
+
decoded.update({"data": decoded, "token": token})
|
33
31
|
return UserData(**decoded)
|
34
|
-
except jwt.
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
except jwt.
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
except jwt.exceptions.InvalidTokenError:
|
47
|
-
if kwargs.get("raise_exception", True):
|
48
|
-
raise USSOException(status_code=401, error="invalid_token")
|
49
|
-
except jwt.exceptions.InvalidKeyError:
|
50
|
-
if kwargs.get("raise_exception", True):
|
51
|
-
raise USSOException(status_code=401, error="invalid_key")
|
52
|
-
except USSOException as e:
|
53
|
-
if kwargs.get("raise_exception", True):
|
54
|
-
raise e
|
32
|
+
except jwt.ExpiredSignatureError:
|
33
|
+
_handle_exception("expired_signature", **kwargs)
|
34
|
+
except jwt.InvalidSignatureError:
|
35
|
+
_handle_exception("invalid_signature", **kwargs)
|
36
|
+
except jwt.InvalidAlgorithmError:
|
37
|
+
_handle_exception("invalid_algorithm", **kwargs)
|
38
|
+
except jwt.InvalidIssuedAtError:
|
39
|
+
_handle_exception("invalid_issued_at", **kwargs)
|
40
|
+
except jwt.InvalidTokenError:
|
41
|
+
_handle_exception("invalid_token", **kwargs)
|
42
|
+
except jwt.InvalidKeyError:
|
43
|
+
_handle_exception("invalid_key", **kwargs)
|
55
44
|
except Exception as e:
|
56
|
-
|
57
|
-
|
58
|
-
|
45
|
+
_handle_exception("error", message=str(e), **kwargs)
|
46
|
+
|
47
|
+
|
48
|
+
def _handle_exception(error_type: str, **kwargs):
|
49
|
+
"""Handle JWT-related exceptions."""
|
50
|
+
if kwargs.get("raise_exception", True):
|
51
|
+
raise USSOException(
|
52
|
+
status_code=401, error=error_type, message=kwargs.get("message")
|
53
|
+
)
|
54
|
+
logger.error(kwargs.get("message") or error_type)
|
59
55
|
|
60
56
|
|
61
57
|
def is_expired(token: str, **kwargs) -> bool:
|
@@ -66,69 +62,31 @@ def is_expired(token: str, **kwargs) -> bool:
|
|
66
62
|
return exp < now
|
67
63
|
|
68
64
|
|
69
|
-
@
|
65
|
+
@cachetools.func.ttl_cache(maxsize=128, ttl=10 * 60)
|
70
66
|
def get_jwk_keys(jwk_url: str) -> jwt.PyJWKClient:
|
71
67
|
return jwt.PyJWKClient(jwk_url, headers={"User-Agent": "usso-python"})
|
72
68
|
|
73
69
|
|
74
|
-
def
|
70
|
+
def decode_token_with_jwk(jwk_url: str, token: str, **kwargs) -> UserData | None:
|
75
71
|
"""Return the user associated with a token value."""
|
76
72
|
try:
|
77
73
|
jwk_client = get_jwk_keys(jwk_url)
|
78
74
|
signing_key = jwk_client.get_signing_key_from_jwt(token)
|
79
75
|
return decode_token(signing_key.key, token, **kwargs)
|
80
|
-
except USSOException as e:
|
81
|
-
if kwargs.get("raise_exception", True):
|
82
|
-
raise e
|
83
|
-
logger.error(e)
|
84
76
|
except Exception as e:
|
85
|
-
|
86
|
-
raise USSOException(
|
87
|
-
status_code=401,
|
88
|
-
error="error",
|
89
|
-
message=str(e),
|
90
|
-
)
|
91
|
-
logger.error(e)
|
77
|
+
_handle_exception("error", message=str(e), **kwargs)
|
92
78
|
|
93
79
|
|
94
|
-
@
|
95
|
-
def
|
80
|
+
@cachetools.func.ttl_cache(maxsize=128, ttl=10 * 60)
|
81
|
+
def fetch_api_key_data(jwk_url: str, api_key: str):
|
96
82
|
parsed = urlparse(jwk_url)
|
97
83
|
url = f"{parsed.scheme}://{parsed.netloc}/api_key/verify"
|
98
|
-
response =
|
84
|
+
response = httpx.post(url, json={"api_key": api_key})
|
99
85
|
response.raise_for_status()
|
100
86
|
return UserData(**response.json())
|
101
87
|
|
102
88
|
|
103
|
-
class JWTConfig(BaseModel):
|
104
|
-
jwk_url: str | None = None
|
105
|
-
secret: str | None = None
|
106
|
-
type: str = "RS256"
|
107
|
-
header: dict[str, str] = {"type": "Cookie", "name": "usso_access_token"}
|
108
|
-
|
109
|
-
def __hash__(self):
|
110
|
-
return hash(self.model_dump_json())
|
111
|
-
|
112
|
-
@model_validator(mode="before")
|
113
|
-
def validate_secret(cls, data: dict):
|
114
|
-
if not data.get("jwk_url") and not data.get("secret"):
|
115
|
-
raise ValueError("Either jwk_url or secret must be provided")
|
116
|
-
return data
|
117
|
-
|
118
|
-
@classmethod
|
119
|
-
@cachetools.func.ttl_cache(maxsize=128, ttl=10 * 60)
|
120
|
-
def get_jwk_keys(cls, jwk_url):
|
121
|
-
return get_jwk_keys(jwk_url)
|
122
|
-
|
123
|
-
@cachetools.func.ttl_cache(maxsize=128, ttl=10 * 60)
|
124
|
-
def decode(self, token: str):
|
125
|
-
if self.jwk_url:
|
126
|
-
return decode_token_jwk(self.jwk_url, token)
|
127
|
-
return decode_token(self.secret, token, algorithms=[self.type])
|
128
|
-
|
129
|
-
|
130
89
|
class Usso:
|
131
|
-
|
132
90
|
def __init__(
|
133
91
|
self,
|
134
92
|
*,
|
@@ -138,45 +96,44 @@ class Usso:
|
|
138
96
|
jwk_url: str | None = None,
|
139
97
|
secret: str | None = None,
|
140
98
|
):
|
99
|
+
self.jwt_configs = self._initialize_configs(jwt_config, jwk_url, secret)
|
100
|
+
|
101
|
+
def _initialize_configs(
|
102
|
+
self,
|
103
|
+
jwt_config: (
|
104
|
+
str | dict | JWTConfig | list[str] | list[dict] | list[JWTConfig] | None
|
105
|
+
) = None,
|
106
|
+
jwk_url: str | None = None,
|
107
|
+
secret: str | None = None,
|
108
|
+
):
|
109
|
+
"""Initialize JWT configurations."""
|
141
110
|
if jwt_config is None:
|
142
111
|
jwt_config = os.getenv("USSO_JWT_CONFIG")
|
143
112
|
|
144
113
|
if jwt_config is None:
|
145
|
-
|
146
|
-
|
114
|
+
jwk_url = jwk_url or os.getenv("USSO_JWK_URL") or os.getenv("USSO_JWKS_URL")
|
115
|
+
secret = secret or os.getenv("USSO_SECRET")
|
147
116
|
if jwk_url:
|
148
|
-
|
149
|
-
return
|
150
|
-
|
151
|
-
if not secret:
|
152
|
-
secret = os.getenv("USSO_SECRET")
|
117
|
+
return [JWTConfig(jwk_url=jwk_url)]
|
153
118
|
if secret:
|
154
|
-
|
155
|
-
return
|
156
|
-
|
119
|
+
return [JWTConfig(secret=secret)]
|
157
120
|
raise ValueError(
|
158
|
-
"
|
159
|
-
[
|
160
|
-
"jwt_config or jwk_url or secret must be provided",
|
161
|
-
"or set the environment variable USSO_JWT_CONFIG or USSO_JWK_URL or USSO_SECRET",
|
162
|
-
]
|
163
|
-
)
|
121
|
+
"Provide jwt_config, jwk_url, or secret, or set the appropriate environment variables."
|
164
122
|
)
|
165
123
|
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
return jwt_config
|
124
|
+
if isinstance(jwt_config, (str, dict, JWTConfig)):
|
125
|
+
return [self._parse_config(jwt_config)]
|
126
|
+
if isinstance(jwt_config, list):
|
127
|
+
return [self._parse_config(config) for config in jwt_config]
|
128
|
+
raise ValueError("Invalid jwt_config format")
|
172
129
|
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
130
|
+
def _parse_config(self, config):
|
131
|
+
"""Parse a single JWT configuration."""
|
132
|
+
if isinstance(config, str):
|
133
|
+
config = json.loads(config)
|
134
|
+
if isinstance(config, dict):
|
135
|
+
return JWTConfig(**config)
|
136
|
+
return config
|
180
137
|
|
181
138
|
def user_data_from_token(self, token: str, **kwargs) -> UserData | None:
|
182
139
|
"""Return the user associated with a token value."""
|
@@ -185,49 +142,15 @@ class Usso:
|
|
185
142
|
try:
|
186
143
|
user_data = jwk_config.decode(token)
|
187
144
|
if user_data.token_type.lower() != kwargs.get("token_type", "access"):
|
188
|
-
|
189
|
-
status_code=401,
|
190
|
-
error="invalid_token_type",
|
191
|
-
message="Token type must be 'access'",
|
192
|
-
)
|
193
|
-
|
145
|
+
_handle_exception("invalid_token_type", **kwargs)
|
194
146
|
return user_data
|
195
|
-
|
196
147
|
except USSOException as e:
|
197
148
|
exp = e
|
198
149
|
|
199
150
|
if kwargs.get("raise_exception", True):
|
200
151
|
if exp:
|
201
|
-
|
202
|
-
|
203
|
-
status_code=401,
|
204
|
-
error="unauthorized",
|
205
|
-
)
|
206
|
-
|
207
|
-
def user_data_api_key(self, api_key: str, **kwargs) -> UserData | None:
|
208
|
-
"""get user data from auth server by api_key."""
|
209
|
-
for jwk_config in self.jwt_configs:
|
210
|
-
try:
|
211
|
-
user_data = jwk_config.decode(api_key)
|
212
|
-
if user_data.token_type.lower() != kwargs.get("token_type", "access"):
|
213
|
-
raise USSOException(
|
214
|
-
status_code=401,
|
215
|
-
error="invalid_token_type",
|
216
|
-
message="Token type must be 'access'",
|
217
|
-
)
|
218
|
-
|
219
|
-
return user_data
|
220
|
-
|
221
|
-
except USSOException as e:
|
222
|
-
exp = e
|
223
|
-
|
224
|
-
if kwargs.get("raise_exception", True):
|
225
|
-
if exp:
|
226
|
-
raise exp
|
227
|
-
raise USSOException(
|
228
|
-
status_code=401,
|
229
|
-
error="unauthorized",
|
230
|
-
)
|
152
|
+
_handle_exception(exp.error, message=str(exp), **kwargs)
|
153
|
+
_handle_exception("unauthorized", **kwargs)
|
231
154
|
|
232
155
|
def user_data_from_api_key(self, api_key: str):
|
233
|
-
return
|
156
|
+
return fetch_api_key_data(self.jwt_configs[0].jwk_url, api_key)
|
usso/exceptions.py
CHANGED
@@ -1,8 +1,9 @@
|
|
1
1
|
error_messages = {
|
2
|
-
"invalid_signature": "
|
3
|
-
"invalid_token": "
|
4
|
-
"expired_signature": "
|
2
|
+
"invalid_signature": "Unauthorized. The JWT signature is invalid.",
|
3
|
+
"invalid_token": "Unauthorized. The JWT is invalid or not provided.",
|
4
|
+
"expired_signature": "Unauthorized. The JWT is expired.",
|
5
5
|
"unauthorized": "Unauthorized",
|
6
|
+
"invalid_token_type": "Unauthorized. Token type must be 'access'",
|
6
7
|
}
|
7
8
|
|
8
9
|
|
@@ -14,4 +15,3 @@ class USSOException(Exception):
|
|
14
15
|
if message is None:
|
15
16
|
self.message = error_messages[error]
|
16
17
|
super().__init__(message)
|
17
|
-
|
usso/schemas.py
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
import uuid
|
2
2
|
|
3
|
-
|
3
|
+
import cachetools.func
|
4
|
+
from pydantic import BaseModel, model_validator
|
4
5
|
|
5
6
|
from . import b64tools
|
6
7
|
|
@@ -37,3 +38,30 @@ class UserData(BaseModel):
|
|
37
38
|
@property
|
38
39
|
def b64id(self) -> uuid.UUID:
|
39
40
|
return b64tools.b64_encode_uuid_strip(self.uid)
|
41
|
+
|
42
|
+
|
43
|
+
class JWTConfig(BaseModel):
|
44
|
+
"""Configuration for JWT processing."""
|
45
|
+
|
46
|
+
jwk_url: str | None = None
|
47
|
+
secret: str | None = None
|
48
|
+
algorithm: str = "RS256"
|
49
|
+
header: dict[str, str] = {"type": "Cookie", "name": "usso_access_token"}
|
50
|
+
|
51
|
+
def __hash__(self):
|
52
|
+
return hash(self.model_dump_json())
|
53
|
+
|
54
|
+
@model_validator(mode="before")
|
55
|
+
def validate_config(cls, data: dict):
|
56
|
+
if not data.get("jwk_url") and not data.get("secret"):
|
57
|
+
raise ValueError("Either jwk_url or secret must be provided")
|
58
|
+
return data
|
59
|
+
|
60
|
+
@cachetools.func.ttl_cache(maxsize=128, ttl=600)
|
61
|
+
def decode(self, token: str):
|
62
|
+
"""Decode a token using the configured method."""
|
63
|
+
from .core import decode_token, decode_token_with_jwk
|
64
|
+
|
65
|
+
if self.jwk_url:
|
66
|
+
return decode_token_with_jwk(self.jwk_url, token)
|
67
|
+
return decode_token(self.secret, token, algorithms=[self.algorithm])
|
usso/session/async_session.py
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
import os
|
2
|
+
|
2
3
|
import httpx
|
4
|
+
|
3
5
|
from ..core import is_expired
|
4
6
|
from .base_session import BaseUssoSession
|
5
7
|
|
@@ -17,18 +19,16 @@ class AsyncUssoSession(httpx.AsyncClient, BaseUssoSession):
|
|
17
19
|
client: "AsyncUssoSession" | None = None,
|
18
20
|
):
|
19
21
|
httpx.AsyncClient.__init__(self)
|
20
|
-
|
21
|
-
self
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
user_id=user_id,
|
31
|
-
)
|
22
|
+
BaseUssoSession.__init__(
|
23
|
+
self,
|
24
|
+
usso_base_url=usso_base_url,
|
25
|
+
api_key=api_key,
|
26
|
+
usso_refresh_url=usso_refresh_url,
|
27
|
+
refresh_token=refresh_token,
|
28
|
+
usso_api_key=usso_api_key,
|
29
|
+
user_id=user_id,
|
30
|
+
client=client,
|
31
|
+
)
|
32
32
|
self._refresh_sync()
|
33
33
|
|
34
34
|
def _prepare_refresh_request(self) -> tuple[dict, dict]:
|
usso/session/base_session.py
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
import os
|
2
2
|
from urllib.parse import urlparse
|
3
|
+
|
3
4
|
from usso.core import is_expired
|
4
5
|
|
5
6
|
|
@@ -13,7 +14,12 @@ class BaseUssoSession:
|
|
13
14
|
refresh_token: str | None = os.getenv("USSO_REFRESH_TOKEN"),
|
14
15
|
usso_api_key: str | None = os.getenv("USSO_ADMIN_API_KEY"),
|
15
16
|
user_id: str | None = None,
|
17
|
+
client: "BaseUssoSession" | None = None,
|
16
18
|
):
|
19
|
+
if client:
|
20
|
+
self.copy_attributes_from(client)
|
21
|
+
return
|
22
|
+
|
17
23
|
assert (
|
18
24
|
usso_base_url or usso_refresh_url
|
19
25
|
), "usso_base_url or usso_refresh_url is required"
|
usso/session/session.py
CHANGED
@@ -1,12 +1,13 @@
|
|
1
1
|
import os
|
2
2
|
|
3
3
|
import httpx
|
4
|
+
|
4
5
|
from usso.core import is_expired
|
5
6
|
|
6
7
|
from .base_session import BaseUssoSession
|
7
8
|
|
8
9
|
|
9
|
-
class UssoSession(
|
10
|
+
class UssoSession(httpx.Client, BaseUssoSession):
|
10
11
|
|
11
12
|
def __init__(
|
12
13
|
self,
|
@@ -21,18 +22,16 @@ class UssoSession(BaseUssoSession, httpx.Client):
|
|
21
22
|
):
|
22
23
|
httpx.Client.__init__(self)
|
23
24
|
|
24
|
-
|
25
|
-
self
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
user_id=user_id,
|
35
|
-
)
|
25
|
+
BaseUssoSession.__init__(
|
26
|
+
self,
|
27
|
+
usso_base_url=usso_base_url,
|
28
|
+
api_key=api_key,
|
29
|
+
usso_refresh_url=usso_refresh_url,
|
30
|
+
refresh_token=refresh_token,
|
31
|
+
usso_api_key=usso_api_key,
|
32
|
+
user_id=user_id,
|
33
|
+
client=client,
|
34
|
+
)
|
36
35
|
self._refresh()
|
37
36
|
|
38
37
|
def _refresh_api(self):
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: usso
|
3
|
-
Version: 0.27.
|
3
|
+
Version: 0.27.13
|
4
4
|
Summary: A plug-and-play client for integrating universal single sign-on (SSO) with Python frameworks, enabling secure and seamless authentication across microservices.
|
5
5
|
Author-email: Mahdi Kiani <mahdikiany@gmail.com>
|
6
6
|
Maintainer-email: Mahdi Kiani <mahdikiany@gmail.com>
|
@@ -1,8 +1,8 @@
|
|
1
1
|
usso/__init__.py,sha256=NnOS_S1a-JKTOlGe1nw-kCL3m0y82mA2mDraus7BQ2o,120
|
2
2
|
usso/b64tools.py,sha256=HGQ0E59vzjrQo2-4jrcY03ebtTaYwTtCZ7KgJaEmxO0,610
|
3
|
-
usso/core.py,sha256=
|
4
|
-
usso/exceptions.py,sha256=
|
5
|
-
usso/schemas.py,sha256=
|
3
|
+
usso/core.py,sha256=R4WrOn5bkBJzAnvJZldfihfwNvMn0BnjoKkO5eCnDTA,5533
|
4
|
+
usso/exceptions.py,sha256=ogJsjdUK0HoZdQv5uCnzIVoG-bTTMHBqyvB4swAMsiE,653
|
5
|
+
usso/schemas.py,sha256=aK_UWZvqjZLz5r1yBIZX_nL2yPCNUjxpZ93AsV9mAes,1810
|
6
6
|
usso/client/__init__.py,sha256=ilGFrugI7bhGXVIcETdbRAye8S7k2mVjkEeziToVzSs,100
|
7
7
|
usso/client/api.py,sha256=xlDq2nZNpq3mhAvqIbGEfANHNjJpPquSeULBfS7iMJw,5094
|
8
8
|
usso/client/async_api.py,sha256=VBmuUsx9vBy-naeiVNhsGgJOTpD1z7VgH_23lazz3_4,5156
|
@@ -11,12 +11,12 @@ usso/django/middleware.py,sha256=EEEpHvMQ6QiWw2HY8zQ2Aec0RCATcLWsCKeyiPWJKio,324
|
|
11
11
|
usso/fastapi/__init__.py,sha256=0EcdOzb4f3yu9nILIdGWnlyUz-0VaVX2az1e3f2BusI,201
|
12
12
|
usso/fastapi/integration.py,sha256=IonxxNj_B9sG2j672rIzE047qo972vk7ch4-eGENp3Q,2638
|
13
13
|
usso/session/__init__.py,sha256=tE4qWUdSI7iN_pywm47Mg8NKOTBa2nCNwCy3wCZWRmU,124
|
14
|
-
usso/session/async_session.py,sha256=
|
15
|
-
usso/session/base_session.py,sha256=
|
16
|
-
usso/session/session.py,sha256=
|
17
|
-
usso-0.27.
|
18
|
-
usso-0.27.
|
19
|
-
usso-0.27.
|
20
|
-
usso-0.27.
|
21
|
-
usso-0.27.
|
22
|
-
usso-0.27.
|
14
|
+
usso/session/async_session.py,sha256=7n-Gp7mfW2W4X24qPo8bWSBj4BT1VJ3tfWia9cR7OSA,3491
|
15
|
+
usso/session/base_session.py,sha256=rUYKWO9UtDfDAv-WrUPMz99LRgf2ilZ9EGtq6B5NDcA,2964
|
16
|
+
usso/session/session.py,sha256=PUGfki7dfJrGBJ6VC_KKTzcYI7NC3stLPf0BgTWd_ss,2363
|
17
|
+
usso-0.27.13.dist-info/LICENSE.txt,sha256=ceC9ZJOV9H6CtQDcYmHOS46NA3dHJ_WD4J9blH513pc,1081
|
18
|
+
usso-0.27.13.dist-info/METADATA,sha256=syYQdcLcUBNeLlqwZoj8GBTTGn1jUG1-mt-awGeFKTY,4518
|
19
|
+
usso-0.27.13.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
|
20
|
+
usso-0.27.13.dist-info/entry_points.txt,sha256=4Zgpm5ELaAWPf0jPGJFz1_X69H7un8ycT3WdGoJ0Vvk,35
|
21
|
+
usso-0.27.13.dist-info/top_level.txt,sha256=g9Jf6h1Oyidh0vPiFni7UHInTJjSvu6cUalpLTIvthg,5
|
22
|
+
usso-0.27.13.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|