diaspora-event-sdk 0.4.0__py3-none-any.whl → 0.4.3__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.
- diaspora_event_sdk/__init__.py +5 -2
- diaspora_event_sdk/sdk/_environments.py +0 -2
- diaspora_event_sdk/sdk/client.py +44 -175
- diaspora_event_sdk/sdk/kafka_client.py +112 -67
- diaspora_event_sdk/sdk/login_manager/manager.py +60 -1
- diaspora_event_sdk/sdk/web_client.py +29 -114
- diaspora_event_sdk/version.py +1 -1
- {diaspora_event_sdk-0.4.0.dist-info → diaspora_event_sdk-0.4.3.dist-info}/METADATA +12 -3
- {diaspora_event_sdk-0.4.0.dist-info → diaspora_event_sdk-0.4.3.dist-info}/RECORD +13 -14
- {diaspora_event_sdk-0.4.0.dist-info → diaspora_event_sdk-0.4.3.dist-info}/WHEEL +1 -1
- tests/unit/apis_test.py +267 -114
- tests/unit/client_test.py +0 -100
- {diaspora_event_sdk-0.4.0.dist-info → diaspora_event_sdk-0.4.3.dist-info/licenses}/LICENSE +0 -0
- {diaspora_event_sdk-0.4.0.dist-info → diaspora_event_sdk-0.4.3.dist-info}/top_level.txt +0 -0
diaspora_event_sdk/__init__.py
CHANGED
|
@@ -9,7 +9,10 @@ from diaspora_event_sdk.sdk.client import Client
|
|
|
9
9
|
from diaspora_event_sdk.sdk.kafka_client import (
|
|
10
10
|
KafkaProducer,
|
|
11
11
|
KafkaConsumer,
|
|
12
|
-
block_until_ready,
|
|
13
12
|
)
|
|
14
13
|
|
|
15
|
-
__all__ = (
|
|
14
|
+
__all__ = (
|
|
15
|
+
"Client",
|
|
16
|
+
"KafkaProducer",
|
|
17
|
+
"KafkaConsumer",
|
|
18
|
+
)
|
|
@@ -13,9 +13,7 @@ def get_web_service_url(envname: Union[str, None] = None) -> str:
|
|
|
13
13
|
env = envname or _get_envname()
|
|
14
14
|
urls = {
|
|
15
15
|
"production": "https://diaspora-web-service.qpp943wkvr7b2.us-east-1.cs.amazonlightsail.com/",
|
|
16
|
-
"dev": "https://diaspora-web-service.qpp943wkvr7b2.us-east-1.cs.amazonlightsail.com/",
|
|
17
16
|
"local": "http://localhost:8000",
|
|
18
|
-
"legacy": "https://diaspora-web-service-dev.ml22sevubfnks.us-east-1.cs.amazonlightsail.com",
|
|
19
17
|
}
|
|
20
18
|
|
|
21
19
|
return urls.get(env, urls["production"])
|
diaspora_event_sdk/sdk/client.py
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import json
|
|
2
1
|
from typing import Optional
|
|
3
2
|
|
|
4
3
|
from diaspora_event_sdk.sdk.login_manager import (
|
|
@@ -7,7 +6,7 @@ from diaspora_event_sdk.sdk.login_manager import (
|
|
|
7
6
|
requires_login,
|
|
8
7
|
)
|
|
9
8
|
|
|
10
|
-
from ._environments import
|
|
9
|
+
from ._environments import get_web_service_url
|
|
11
10
|
|
|
12
11
|
|
|
13
12
|
class Client:
|
|
@@ -32,213 +31,83 @@ class Client:
|
|
|
32
31
|
)
|
|
33
32
|
self.auth_client = self.login_manager.get_auth_client()
|
|
34
33
|
self.subject_openid = self.auth_client.userinfo()["sub"]
|
|
34
|
+
self.namespace = f"ns-{self.subject_openid.replace('-', '')[-12:]}"
|
|
35
35
|
|
|
36
36
|
def logout(self):
|
|
37
37
|
"""Remove credentials from your local system"""
|
|
38
38
|
self.login_manager.logout()
|
|
39
39
|
|
|
40
40
|
@requires_login
|
|
41
|
-
def
|
|
42
|
-
"""
|
|
43
|
-
Revokes existing keys, generates a new key, and updates the token storage with the newly created key and the Diaspora endpoint.
|
|
44
|
-
"""
|
|
45
|
-
resp = self.web_client.create_key(self.subject_openid)
|
|
46
|
-
if resp["status"] == "error":
|
|
47
|
-
raise Exception("should not happen")
|
|
48
|
-
tokens = self.login_manager._token_storage.get_token_data(
|
|
49
|
-
DIASPORA_RESOURCE_SERVER
|
|
50
|
-
)
|
|
51
|
-
tokens["access_key"] = resp["access_key"]
|
|
52
|
-
tokens["secret_key"] = resp["secret_key"]
|
|
53
|
-
tokens["endpoint"] = resp["endpoint"]
|
|
54
|
-
with self.login_manager._access_lock:
|
|
55
|
-
self.login_manager._token_storage._connection.executemany(
|
|
56
|
-
"REPLACE INTO token_storage(namespace, resource_server, token_data_json) "
|
|
57
|
-
"VALUES(?, ?, ?)",
|
|
58
|
-
[
|
|
59
|
-
(
|
|
60
|
-
self.login_manager._token_storage.namespace,
|
|
61
|
-
DIASPORA_RESOURCE_SERVER,
|
|
62
|
-
json.dumps(tokens),
|
|
63
|
-
)
|
|
64
|
-
],
|
|
65
|
-
)
|
|
66
|
-
self.login_manager._token_storage._connection.commit()
|
|
67
|
-
return {
|
|
68
|
-
"access_key": tokens["access_key"],
|
|
69
|
-
"secret_key": tokens["secret_key"],
|
|
70
|
-
"endpoint": tokens["endpoint"],
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
@requires_login
|
|
74
|
-
def retrieve_key(self):
|
|
75
|
-
"""
|
|
76
|
-
Retrieves the key from local token storage, and calls create_key() if the local key is not found.
|
|
77
|
-
"""
|
|
78
|
-
tokens = self.login_manager._token_storage.get_token_data(
|
|
79
|
-
DIASPORA_RESOURCE_SERVER
|
|
80
|
-
)
|
|
81
|
-
if (
|
|
82
|
-
tokens is None
|
|
83
|
-
or "endpoint" not in tokens
|
|
84
|
-
or "access_key" not in tokens
|
|
85
|
-
or "secret_key" not in tokens
|
|
86
|
-
):
|
|
87
|
-
return self.create_key()
|
|
88
|
-
else:
|
|
89
|
-
return {
|
|
90
|
-
"access_key": tokens["access_key"],
|
|
91
|
-
"secret_key": tokens["secret_key"],
|
|
92
|
-
"endpoint": tokens["endpoint"],
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
@requires_login
|
|
96
|
-
def put_secret_key(self, access_key, secret_key, endpoint):
|
|
97
|
-
tokens = self.login_manager._token_storage.get_token_data(
|
|
98
|
-
DIASPORA_RESOURCE_SERVER
|
|
99
|
-
)
|
|
100
|
-
tokens["access_key"] = access_key
|
|
101
|
-
tokens["secret_key"] = secret_key
|
|
102
|
-
tokens["endpoint"] = endpoint
|
|
103
|
-
with self.login_manager._access_lock:
|
|
104
|
-
self.login_manager._token_storage._connection.executemany(
|
|
105
|
-
"REPLACE INTO token_storage(namespace, resource_server, token_data_json) "
|
|
106
|
-
"VALUES(?, ?, ?)",
|
|
107
|
-
[
|
|
108
|
-
(
|
|
109
|
-
self.login_manager._token_storage.namespace,
|
|
110
|
-
DIASPORA_RESOURCE_SERVER,
|
|
111
|
-
json.dumps(tokens),
|
|
112
|
-
)
|
|
113
|
-
],
|
|
114
|
-
)
|
|
115
|
-
self.login_manager._token_storage._connection.commit()
|
|
116
|
-
|
|
117
|
-
@requires_login
|
|
118
|
-
def list_topics(self):
|
|
119
|
-
"""
|
|
120
|
-
Returns a list of topics currently registered under the user's account.
|
|
121
|
-
"""
|
|
122
|
-
return self.web_client.list_topics(self.subject_openid)
|
|
123
|
-
|
|
124
|
-
@requires_login
|
|
125
|
-
def register_topic(self, topic):
|
|
126
|
-
"""
|
|
127
|
-
Registers a new topic the user's account with permissions to read, write, and describe the topic.
|
|
128
|
-
"""
|
|
129
|
-
return self.web_client.register_topic(self.subject_openid, topic, "register")
|
|
130
|
-
|
|
131
|
-
@requires_login
|
|
132
|
-
def unregister_topic(self, topic):
|
|
133
|
-
"""
|
|
134
|
-
Unregisters a topic from a user's account, but all existing events within the topic are unaffected.
|
|
135
|
-
"""
|
|
136
|
-
return self.web_client.register_topic(self.subject_openid, topic, "unregister")
|
|
137
|
-
|
|
138
|
-
@requires_login
|
|
139
|
-
def get_topic_configs(self, topic):
|
|
41
|
+
def create_user(self):
|
|
140
42
|
"""
|
|
141
|
-
|
|
43
|
+
Create an IAM user with policy and namespace for the current user (POST /api/v3/user).
|
|
44
|
+
Returns status, message, subject, and namespace.
|
|
142
45
|
"""
|
|
143
|
-
|
|
46
|
+
resp = self.web_client.create_user(self.subject_openid)
|
|
47
|
+
return resp.data if hasattr(resp, "data") else resp
|
|
144
48
|
|
|
145
49
|
@requires_login
|
|
146
|
-
def
|
|
50
|
+
def delete_user(self):
|
|
147
51
|
"""
|
|
148
|
-
|
|
52
|
+
Delete the IAM user and all associated resources for the current user (DELETE /api/v3/user).
|
|
53
|
+
Returns status and message.
|
|
149
54
|
"""
|
|
150
|
-
|
|
55
|
+
resp = self.web_client.delete_user(self.subject_openid)
|
|
56
|
+
return resp.data if hasattr(resp, "data") else resp
|
|
151
57
|
|
|
152
58
|
@requires_login
|
|
153
|
-
def
|
|
154
|
-
"""
|
|
155
|
-
Increases the number of partitions for a given topic to the specified new partition count.
|
|
156
|
-
"""
|
|
157
|
-
return self.web_client.update_topic_partitions(
|
|
158
|
-
self.subject_openid, topic, new_partitions
|
|
159
|
-
)
|
|
160
|
-
|
|
161
|
-
@requires_login
|
|
162
|
-
def reset_topic(self, topic):
|
|
163
|
-
"""
|
|
164
|
-
Deletes and recreates the topic, removing all messages and restoring the topic to the default configurations while user access is not affected.
|
|
165
|
-
"""
|
|
166
|
-
return self.web_client.reset_topic(self.subject_openid, topic)
|
|
167
|
-
|
|
168
|
-
@requires_login
|
|
169
|
-
def grant_user_access(self, topic, user):
|
|
170
|
-
"""
|
|
171
|
-
Authorizes another user to access a registered topic under the invoker's account.
|
|
172
|
-
"""
|
|
173
|
-
return self.web_client.grant_user_access(
|
|
174
|
-
self.subject_openid, topic, user, "grant"
|
|
175
|
-
)
|
|
176
|
-
|
|
177
|
-
@requires_login
|
|
178
|
-
def revoke_user_access(self, topic, user):
|
|
59
|
+
def create_key(self):
|
|
179
60
|
"""
|
|
180
|
-
|
|
61
|
+
Create a new access key for the current user (POST /api/v3/key).
|
|
62
|
+
This will replace any existing access key (force refresh).
|
|
63
|
+
Returns status, message, access_key, secret_key, and create_date.
|
|
181
64
|
"""
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
)
|
|
65
|
+
resp = self.web_client.create_key(self.subject_openid)
|
|
66
|
+
return resp.data if hasattr(resp, "data") else resp
|
|
185
67
|
|
|
186
68
|
@requires_login
|
|
187
|
-
def
|
|
69
|
+
def delete_key(self):
|
|
188
70
|
"""
|
|
189
|
-
|
|
71
|
+
Delete access keys from IAM and DynamoDB for the current user (DELETE /api/v3/key).
|
|
72
|
+
Returns status and message.
|
|
190
73
|
"""
|
|
191
|
-
|
|
74
|
+
resp = self.web_client.delete_key(self.subject_openid)
|
|
75
|
+
return resp.data if hasattr(resp, "data") else resp
|
|
192
76
|
|
|
193
77
|
@requires_login
|
|
194
|
-
def
|
|
78
|
+
def list_namespaces(self):
|
|
195
79
|
"""
|
|
196
|
-
|
|
80
|
+
List all namespaces owned by the current user and their topics (GET /api/v3/namespace).
|
|
81
|
+
Returns status, message, and namespaces dict (namespace -> list of topics).
|
|
197
82
|
"""
|
|
198
|
-
|
|
83
|
+
resp = self.web_client.list_namespaces(self.subject_openid)
|
|
84
|
+
return resp.data if hasattr(resp, "data") else resp
|
|
199
85
|
|
|
200
86
|
@requires_login
|
|
201
|
-
def
|
|
87
|
+
def create_topic(self, topic: str):
|
|
202
88
|
"""
|
|
203
|
-
|
|
89
|
+
Create a topic under the user's default namespace (POST /api/v3/{namespace}/{topic}).
|
|
90
|
+
Returns status, message, and topics list.
|
|
204
91
|
"""
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
topic,
|
|
208
|
-
function,
|
|
209
|
-
"create",
|
|
210
|
-
function_configs,
|
|
211
|
-
trigger_configs,
|
|
212
|
-
)
|
|
92
|
+
resp = self.web_client.create_topic(self.subject_openid, self.namespace, topic)
|
|
93
|
+
return resp.data if hasattr(resp, "data") else resp
|
|
213
94
|
|
|
214
95
|
@requires_login
|
|
215
|
-
def
|
|
96
|
+
def delete_topic(self, topic: str):
|
|
216
97
|
"""
|
|
217
|
-
|
|
98
|
+
Delete a topic from the user's default namespace (DELETE /api/v3/{namespace}/{topic}).
|
|
99
|
+
Returns status, message, and topics list.
|
|
218
100
|
"""
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
)
|
|
101
|
+
resp = self.web_client.delete_topic(self.subject_openid, self.namespace, topic)
|
|
102
|
+
return resp.data if hasattr(resp, "data") else resp
|
|
222
103
|
|
|
223
104
|
@requires_login
|
|
224
|
-
def
|
|
105
|
+
def recreate_topic(self, topic: str):
|
|
225
106
|
"""
|
|
226
|
-
|
|
107
|
+
Recreate a topic in the user's default namespace by deleting and recreating it (PUT /api/v3/{namespace}/{topic}/recreate).
|
|
108
|
+
Returns status and message.
|
|
227
109
|
"""
|
|
228
|
-
|
|
229
|
-
self.subject_openid,
|
|
110
|
+
resp = self.web_client.recreate_topic(
|
|
111
|
+
self.subject_openid, self.namespace, topic
|
|
230
112
|
)
|
|
231
|
-
|
|
232
|
-
@requires_login
|
|
233
|
-
def list_log_streams(self, trigger):
|
|
234
|
-
"""
|
|
235
|
-
List log streams of a trigger under the user's account
|
|
236
|
-
"""
|
|
237
|
-
return self.web_client.list_log_streams(self.subject_openid, trigger)
|
|
238
|
-
|
|
239
|
-
@requires_login
|
|
240
|
-
def get_log_events(self, trigger, stream):
|
|
241
|
-
"""
|
|
242
|
-
Get events in a particular log stream of a trigger under the user's account
|
|
243
|
-
"""
|
|
244
|
-
return self.web_client.get_log_events(self.subject_openid, trigger, stream)
|
|
113
|
+
return resp.data if hasattr(resp, "data") else resp
|
|
@@ -1,24 +1,37 @@
|
|
|
1
1
|
import json
|
|
2
|
-
|
|
3
|
-
import warnings
|
|
2
|
+
import logging
|
|
4
3
|
import time
|
|
4
|
+
import uuid
|
|
5
|
+
import warnings
|
|
6
|
+
from typing import Any, Dict
|
|
5
7
|
|
|
6
|
-
from .client import Client
|
|
7
8
|
from .aws_iam_msk import generate_auth_token
|
|
9
|
+
from .client import Client
|
|
10
|
+
|
|
11
|
+
# File-level logger
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
8
13
|
|
|
9
14
|
# If kafka-python is not installed, Kafka functionality is not available through diaspora-event-sdk.
|
|
10
15
|
kafka_available = True
|
|
11
16
|
try:
|
|
12
|
-
from kafka import KafkaProducer as KProd # type: ignore[import,import-not-found]
|
|
13
|
-
from kafka import KafkaConsumer as KCons # type: ignore[import,import-not-found]
|
|
14
17
|
import os
|
|
15
18
|
|
|
16
|
-
|
|
19
|
+
from kafka import KafkaConsumer as KCons # type: ignore[import,import-not-found]
|
|
20
|
+
from kafka import KafkaProducer as KProd # type: ignore[import,import-not-found]
|
|
21
|
+
from kafka.errors import KafkaTimeoutError, TopicAuthorizationFailedError # type: ignore[import,import-not-found]
|
|
22
|
+
from kafka.sasl.oauth import (
|
|
23
|
+
AbstractTokenProvider, # type: ignore[import,import-not-found]
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
class MSKTokenProvider(AbstractTokenProvider):
|
|
17
27
|
def token(self):
|
|
18
28
|
token, _ = generate_auth_token("us-east-1")
|
|
19
29
|
return token
|
|
20
30
|
except Exception:
|
|
21
31
|
kafka_available = False
|
|
32
|
+
# Fallback if kafka-python is not available
|
|
33
|
+
TopicAuthorizationFailedError = Exception
|
|
34
|
+
KafkaTimeoutError = Exception
|
|
22
35
|
|
|
23
36
|
|
|
24
37
|
def get_diaspora_config(extra_configs: Dict[str, Any] = {}) -> Dict[str, Any]:
|
|
@@ -28,15 +41,11 @@ def get_diaspora_config(extra_configs: Dict[str, Any] = {}) -> Dict[str, Any]:
|
|
|
28
41
|
"""
|
|
29
42
|
|
|
30
43
|
try:
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
keys = Client().retrieve_key()
|
|
37
|
-
os.environ["OCTOPUS_AWS_ACCESS_KEY_ID"] = keys["access_key"]
|
|
38
|
-
os.environ["OCTOPUS_AWS_SECRET_ACCESS_KEY"] = keys["secret_key"]
|
|
39
|
-
os.environ["OCTOPUS_BOOTSTRAP_SERVERS"] = keys["endpoint"]
|
|
44
|
+
client = Client()
|
|
45
|
+
keys = client.create_key() # create or retrieve key
|
|
46
|
+
os.environ["OCTOPUS_AWS_ACCESS_KEY_ID"] = keys["access_key"]
|
|
47
|
+
os.environ["OCTOPUS_AWS_SECRET_ACCESS_KEY"] = keys["secret_key"]
|
|
48
|
+
os.environ["OCTOPUS_BOOTSTRAP_SERVERS"] = keys["endpoint"]
|
|
40
49
|
|
|
41
50
|
except Exception as e:
|
|
42
51
|
raise RuntimeError("Failed to retrieve Kafka keys") from e
|
|
@@ -45,7 +54,7 @@ def get_diaspora_config(extra_configs: Dict[str, Any] = {}) -> Dict[str, Any]:
|
|
|
45
54
|
"bootstrap_servers": os.environ["OCTOPUS_BOOTSTRAP_SERVERS"],
|
|
46
55
|
"security_protocol": "SASL_SSL",
|
|
47
56
|
"sasl_mechanism": "OAUTHBEARER",
|
|
48
|
-
"api_version": (3,
|
|
57
|
+
"api_version": (3, 8, 1),
|
|
49
58
|
"sasl_oauth_token_provider": MSKTokenProvider(),
|
|
50
59
|
}
|
|
51
60
|
conf.update(extra_configs)
|
|
@@ -55,15 +64,35 @@ def get_diaspora_config(extra_configs: Dict[str, Any] = {}) -> Dict[str, Any]:
|
|
|
55
64
|
if kafka_available:
|
|
56
65
|
|
|
57
66
|
class KafkaProducer(KProd):
|
|
58
|
-
|
|
67
|
+
"""
|
|
68
|
+
Wrapper around KProd that:
|
|
69
|
+
- Requires at least one topic
|
|
70
|
+
- Sets a default JSON serializer
|
|
71
|
+
- Does NOT block until topics have partition metadata
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
def __init__(self, *topics, **configs):
|
|
75
|
+
if not topics:
|
|
76
|
+
raise ValueError("KafkaProducer requires at least one topic")
|
|
77
|
+
self.topics = topics
|
|
78
|
+
|
|
59
79
|
configs.setdefault(
|
|
60
|
-
"value_serializer",
|
|
80
|
+
"value_serializer",
|
|
81
|
+
lambda v: json.dumps(v).encode("utf-8"),
|
|
61
82
|
)
|
|
83
|
+
|
|
62
84
|
super().__init__(**get_diaspora_config(configs))
|
|
85
|
+
# Note: We do NOT block on metadata here
|
|
63
86
|
|
|
64
87
|
class KafkaConsumer(KCons):
|
|
65
88
|
def __init__(self, *topics, **configs):
|
|
89
|
+
if not topics:
|
|
90
|
+
raise ValueError("KafkaConsumer requires at least one topic")
|
|
91
|
+
self.topics = topics
|
|
92
|
+
|
|
66
93
|
super().__init__(*topics, **get_diaspora_config(configs))
|
|
94
|
+
# Note: We do NOT block on metadata here
|
|
95
|
+
|
|
67
96
|
|
|
68
97
|
else:
|
|
69
98
|
# Create dummy classes that issue a warning when instantiated
|
|
@@ -82,56 +111,72 @@ else:
|
|
|
82
111
|
)
|
|
83
112
|
|
|
84
113
|
|
|
85
|
-
|
|
86
|
-
def block_until_ready(max_minutes=5):
|
|
87
|
-
"""
|
|
88
|
-
Test Kafka producer and consumer connections.
|
|
89
|
-
By default, this method blocks for five minutes before giving up.
|
|
90
|
-
It returns a boolean that indicates whether the connections can be successfully established.
|
|
114
|
+
def reliable_client_creation() -> str:
|
|
91
115
|
"""
|
|
116
|
+
Reliably create a client and test topic operations with retry logic.
|
|
92
117
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
def consumer_connection_test(result):
|
|
118
|
+
Returns:
|
|
119
|
+
str: The full topic name in format "namespace.topic-name"
|
|
120
|
+
"""
|
|
121
|
+
attempt = 0
|
|
122
|
+
while True:
|
|
123
|
+
attempt += 1
|
|
124
|
+
if attempt > 1:
|
|
125
|
+
time.sleep(5)
|
|
126
|
+
|
|
127
|
+
topic_name = None
|
|
128
|
+
kafka_topic = None
|
|
129
|
+
client = None
|
|
106
130
|
try:
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
)
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
131
|
+
client = Client()
|
|
132
|
+
key_result = client.create_key()
|
|
133
|
+
|
|
134
|
+
# If status is not success, throw exception
|
|
135
|
+
if key_result.get("status") != "success":
|
|
136
|
+
raise RuntimeError(
|
|
137
|
+
f"Failed to create key: {key_result.get('message', 'Unknown error')}"
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
# If key is fresh (just created), wait for IAM policy to propagate
|
|
141
|
+
if key_result.get("fresh", False):
|
|
142
|
+
time.sleep(8)
|
|
143
|
+
|
|
144
|
+
topic_name = f"topic-{str(uuid.uuid4())[:5]}"
|
|
145
|
+
kafka_topic = f"{client.namespace}.{topic_name}"
|
|
146
|
+
|
|
147
|
+
# If status is not success, throw exception
|
|
148
|
+
topic_result = client.create_topic(topic_name)
|
|
149
|
+
if topic_result.get("status") != "success":
|
|
150
|
+
raise RuntimeError(
|
|
151
|
+
f"Failed to create topic: {topic_result.get('message', 'Unknown error')}"
|
|
152
|
+
)
|
|
153
|
+
time.sleep(3) # Wait after topic creation before produce
|
|
154
|
+
|
|
155
|
+
producer = KafkaProducer(kafka_topic)
|
|
156
|
+
for i in range(3):
|
|
157
|
+
future = producer.send(
|
|
158
|
+
kafka_topic, {"message_id": i + 1, "content": f"Message {i + 1}"}
|
|
159
|
+
)
|
|
160
|
+
future.get(timeout=30)
|
|
161
|
+
producer.close()
|
|
162
|
+
time.sleep(2) # Wait for the produced messages to be consumed
|
|
163
|
+
|
|
164
|
+
consumer = KafkaConsumer(kafka_topic, auto_offset_reset="earliest")
|
|
165
|
+
consumer.poll(timeout_ms=10000)
|
|
166
|
+
consumer.close()
|
|
167
|
+
|
|
168
|
+
client.delete_topic(topic_name)
|
|
169
|
+
return kafka_topic
|
|
115
170
|
except Exception as e:
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
"seconds",
|
|
129
|
-
)
|
|
130
|
-
producer_connection_test(result)
|
|
131
|
-
consumer_connection_test(result)
|
|
132
|
-
retry_count += 1
|
|
133
|
-
elapsed_time = time.time() - start_time
|
|
134
|
-
if elapsed_time >= max_minutes * 60:
|
|
135
|
-
print("Time limit exceeded. Exiting loop.")
|
|
136
|
-
return False
|
|
137
|
-
return True
|
|
171
|
+
logger.info(f"Error in attempt {attempt}: {type(e).__name__}: {str(e)}")
|
|
172
|
+
if client:
|
|
173
|
+
try:
|
|
174
|
+
if topic_name:
|
|
175
|
+
client.delete_topic(topic_name)
|
|
176
|
+
except Exception:
|
|
177
|
+
pass
|
|
178
|
+
try:
|
|
179
|
+
client.delete_user()
|
|
180
|
+
except Exception:
|
|
181
|
+
pass
|
|
182
|
+
continue
|
|
@@ -19,6 +19,61 @@ from .login_flow import do_link_auth_flow
|
|
|
19
19
|
log = logging.getLogger(__name__)
|
|
20
20
|
|
|
21
21
|
|
|
22
|
+
class FilteredClientCredentialsAuthorizer(globus_sdk.ClientCredentialsAuthorizer):
|
|
23
|
+
"""
|
|
24
|
+
A custom ClientCredentialsAuthorizer that filters token responses to only
|
|
25
|
+
include tokens for a specific resource server.
|
|
26
|
+
|
|
27
|
+
This is needed when client credentials return tokens for multiple resource
|
|
28
|
+
servers, but ClientCredentialsAuthorizer expects exactly one token.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
confidential_client: globus_sdk.ConfidentialAppAuthClient,
|
|
34
|
+
scopes: list[str],
|
|
35
|
+
*,
|
|
36
|
+
resource_server: str,
|
|
37
|
+
access_token: str | None = None,
|
|
38
|
+
expires_at: int | None = None,
|
|
39
|
+
on_refresh: t.Callable[[globus_sdk.OAuthTokenResponse], None] | None = None,
|
|
40
|
+
) -> None:
|
|
41
|
+
self._target_resource_server = resource_server
|
|
42
|
+
# Store the original on_refresh callback
|
|
43
|
+
self._original_on_refresh = on_refresh
|
|
44
|
+
super().__init__(
|
|
45
|
+
confidential_client=confidential_client,
|
|
46
|
+
scopes=scopes,
|
|
47
|
+
access_token=access_token,
|
|
48
|
+
expires_at=expires_at,
|
|
49
|
+
on_refresh=self._filtered_on_refresh,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
def _extract_token_data(
|
|
53
|
+
self, res: globus_sdk.OAuthClientCredentialsResponse
|
|
54
|
+
) -> dict[str, t.Any]:
|
|
55
|
+
"""
|
|
56
|
+
Extract token data, filtering to only the target resource server.
|
|
57
|
+
"""
|
|
58
|
+
token_data = res.by_resource_server
|
|
59
|
+
if self._target_resource_server in token_data:
|
|
60
|
+
# Return only the token for the target resource server
|
|
61
|
+
return token_data[self._target_resource_server]
|
|
62
|
+
else:
|
|
63
|
+
raise ValueError(
|
|
64
|
+
f"Token response does not contain token for {self._target_resource_server}"
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
def _filtered_on_refresh(
|
|
68
|
+
self, token_response: globus_sdk.OAuthTokenResponse
|
|
69
|
+
) -> None:
|
|
70
|
+
"""
|
|
71
|
+
Call the original on_refresh callback with the filtered token response.
|
|
72
|
+
"""
|
|
73
|
+
if self._original_on_refresh:
|
|
74
|
+
self._original_on_refresh(token_response)
|
|
75
|
+
|
|
76
|
+
|
|
22
77
|
def _get_diaspora_all_scope() -> str:
|
|
23
78
|
return os.getenv(
|
|
24
79
|
"DIASPORA_SCOPE",
|
|
@@ -153,9 +208,13 @@ class LoginManager:
|
|
|
153
208
|
expires_at = tokens["expires_at_seconds"]
|
|
154
209
|
|
|
155
210
|
with self._access_lock:
|
|
156
|
-
|
|
211
|
+
# Use a custom authorizer that filters token responses to only
|
|
212
|
+
# the requested resource server, handling cases where client
|
|
213
|
+
# credentials return tokens for multiple resource servers
|
|
214
|
+
return FilteredClientCredentialsAuthorizer(
|
|
157
215
|
confidential_client=get_client_login(),
|
|
158
216
|
scopes=scopes,
|
|
217
|
+
resource_server=resource_server,
|
|
159
218
|
access_token=access_token,
|
|
160
219
|
expires_at=expires_at,
|
|
161
220
|
on_refresh=self._token_storage.on_refresh,
|