diaspora-event-sdk 0.4.1__tar.gz → 0.4.4__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.
- {diaspora-event-sdk-0.4.1/diaspora_event_sdk.egg-info → diaspora_event_sdk-0.4.4}/PKG-INFO +21 -3
- {diaspora-event-sdk-0.4.1 → diaspora_event_sdk-0.4.4}/diaspora_event_sdk/__init__.py +7 -2
- {diaspora-event-sdk-0.4.1 → diaspora_event_sdk-0.4.4}/diaspora_event_sdk/sdk/_environments.py +0 -2
- diaspora_event_sdk-0.4.4/diaspora_event_sdk/sdk/client.py +113 -0
- diaspora_event_sdk-0.4.4/diaspora_event_sdk/sdk/kafka_client.py +182 -0
- {diaspora-event-sdk-0.4.1 → diaspora_event_sdk-0.4.4}/diaspora_event_sdk/sdk/login_manager/manager.py +60 -1
- diaspora_event_sdk-0.4.4/diaspora_event_sdk/sdk/web_client.py +70 -0
- diaspora_event_sdk-0.4.4/diaspora_event_sdk/version.py +1 -0
- {diaspora-event-sdk-0.4.1 → diaspora_event_sdk-0.4.4/diaspora_event_sdk.egg-info}/PKG-INFO +21 -3
- {diaspora-event-sdk-0.4.1 → diaspora_event_sdk-0.4.4}/diaspora_event_sdk.egg-info/SOURCES.txt +1 -2
- {diaspora-event-sdk-0.4.1 → diaspora_event_sdk-0.4.4}/diaspora_event_sdk.egg-info/requires.txt +1 -1
- {diaspora-event-sdk-0.4.1 → diaspora_event_sdk-0.4.4}/setup.py +1 -1
- diaspora_event_sdk-0.4.4/tests/unit/apis_test.py +289 -0
- diaspora-event-sdk-0.4.1/diaspora_event_sdk/sdk/client.py +0 -244
- diaspora-event-sdk-0.4.1/diaspora_event_sdk/sdk/kafka_client.py +0 -138
- diaspora-event-sdk-0.4.1/diaspora_event_sdk/sdk/web_client.py +0 -155
- diaspora-event-sdk-0.4.1/diaspora_event_sdk/version.py +0 -1
- diaspora-event-sdk-0.4.1/tests/unit/apis_test.py +0 -136
- diaspora-event-sdk-0.4.1/tests/unit/client_test.py +0 -100
- {diaspora-event-sdk-0.4.1 → diaspora_event_sdk-0.4.4}/LICENSE +0 -0
- {diaspora-event-sdk-0.4.1 → diaspora_event_sdk-0.4.4}/MANIFEST.in +0 -0
- {diaspora-event-sdk-0.4.1 → diaspora_event_sdk-0.4.4}/README.md +0 -0
- {diaspora-event-sdk-0.4.1 → diaspora_event_sdk-0.4.4}/diaspora_event_sdk/sdk/__init__.py +0 -0
- {diaspora-event-sdk-0.4.1 → diaspora_event_sdk-0.4.4}/diaspora_event_sdk/sdk/aws_iam_msk.py +0 -0
- {diaspora-event-sdk-0.4.1 → diaspora_event_sdk-0.4.4}/diaspora_event_sdk/sdk/botocore/__init__.py +0 -0
- {diaspora-event-sdk-0.4.1 → diaspora_event_sdk-0.4.4}/diaspora_event_sdk/sdk/botocore/auth.py +0 -0
- {diaspora-event-sdk-0.4.1 → diaspora_event_sdk-0.4.4}/diaspora_event_sdk/sdk/botocore/awsrequest.py +0 -0
- {diaspora-event-sdk-0.4.1 → diaspora_event_sdk-0.4.4}/diaspora_event_sdk/sdk/botocore/compat.py +0 -0
- {diaspora-event-sdk-0.4.1 → diaspora_event_sdk-0.4.4}/diaspora_event_sdk/sdk/botocore/credentials.py +0 -0
- {diaspora-event-sdk-0.4.1 → diaspora_event_sdk-0.4.4}/diaspora_event_sdk/sdk/botocore/exceptions.py +0 -0
- {diaspora-event-sdk-0.4.1 → diaspora_event_sdk-0.4.4}/diaspora_event_sdk/sdk/botocore/utils.py +0 -0
- {diaspora-event-sdk-0.4.1 → diaspora_event_sdk-0.4.4}/diaspora_event_sdk/sdk/decorators.py +0 -0
- {diaspora-event-sdk-0.4.1 → diaspora_event_sdk-0.4.4}/diaspora_event_sdk/sdk/login_manager/__init__.py +0 -0
- {diaspora-event-sdk-0.4.1 → diaspora_event_sdk-0.4.4}/diaspora_event_sdk/sdk/login_manager/client_login.py +0 -0
- {diaspora-event-sdk-0.4.1 → diaspora_event_sdk-0.4.4}/diaspora_event_sdk/sdk/login_manager/decorators.py +0 -0
- {diaspora-event-sdk-0.4.1 → diaspora_event_sdk-0.4.4}/diaspora_event_sdk/sdk/login_manager/globus_auth.py +0 -0
- {diaspora-event-sdk-0.4.1 → diaspora_event_sdk-0.4.4}/diaspora_event_sdk/sdk/login_manager/login_flow.py +0 -0
- {diaspora-event-sdk-0.4.1 → diaspora_event_sdk-0.4.4}/diaspora_event_sdk/sdk/login_manager/protocol.py +0 -0
- {diaspora-event-sdk-0.4.1 → diaspora_event_sdk-0.4.4}/diaspora_event_sdk/sdk/login_manager/tokenstore.py +0 -0
- {diaspora-event-sdk-0.4.1 → diaspora_event_sdk-0.4.4}/diaspora_event_sdk/sdk/utils/__init__.py +0 -0
- {diaspora-event-sdk-0.4.1 → diaspora_event_sdk-0.4.4}/diaspora_event_sdk/sdk/utils/uuid_like.py +0 -0
- {diaspora-event-sdk-0.4.1 → diaspora_event_sdk-0.4.4}/diaspora_event_sdk.egg-info/dependency_links.txt +0 -0
- {diaspora-event-sdk-0.4.1 → diaspora_event_sdk-0.4.4}/diaspora_event_sdk.egg-info/top_level.txt +0 -0
- {diaspora-event-sdk-0.4.1 → diaspora_event_sdk-0.4.4}/mypy.ini +0 -0
- {diaspora-event-sdk-0.4.1 → diaspora_event_sdk-0.4.4}/setup.cfg +0 -0
- {diaspora-event-sdk-0.4.1 → diaspora_event_sdk-0.4.4}/tests/__init__.py +0 -0
- {diaspora-event-sdk-0.4.1 → diaspora_event_sdk-0.4.4}/tox.ini +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: diaspora-event-sdk
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.4
|
|
4
4
|
Summary: Diaspora Event Fabric SDK
|
|
5
5
|
Home-page: https://github.com/globus-labs/diaspora-event-sdk
|
|
6
6
|
License: Apache 2.0
|
|
@@ -15,9 +15,27 @@ Classifier: Programming Language :: Python :: 3.10
|
|
|
15
15
|
Classifier: Programming Language :: Python :: 3.11
|
|
16
16
|
Classifier: Programming Language :: Python :: 3.12
|
|
17
17
|
Description-Content-Type: text/markdown
|
|
18
|
+
License-File: LICENSE
|
|
19
|
+
Requires-Dist: globus-sdk<4,>=3.59.0
|
|
18
20
|
Provides-Extra: kafka-python
|
|
21
|
+
Requires-Dist: kafka-python; extra == "kafka-python"
|
|
19
22
|
Provides-Extra: test
|
|
20
|
-
|
|
23
|
+
Requires-Dist: pytest; extra == "test"
|
|
24
|
+
Requires-Dist: pytest-cov; extra == "test"
|
|
25
|
+
Requires-Dist: coverage; extra == "test"
|
|
26
|
+
Requires-Dist: mypy; extra == "test"
|
|
27
|
+
Requires-Dist: tox; extra == "test"
|
|
28
|
+
Requires-Dist: check-manifest; extra == "test"
|
|
29
|
+
Requires-Dist: pre-commit; extra == "test"
|
|
30
|
+
Dynamic: classifier
|
|
31
|
+
Dynamic: description
|
|
32
|
+
Dynamic: description-content-type
|
|
33
|
+
Dynamic: home-page
|
|
34
|
+
Dynamic: license
|
|
35
|
+
Dynamic: license-file
|
|
36
|
+
Dynamic: provides-extra
|
|
37
|
+
Dynamic: requires-dist
|
|
38
|
+
Dynamic: summary
|
|
21
39
|
|
|
22
40
|
# Diaspora Event Fabric SDK
|
|
23
41
|
|
|
@@ -9,7 +9,12 @@ 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
|
-
|
|
12
|
+
reliable_client_creation,
|
|
13
13
|
)
|
|
14
14
|
|
|
15
|
-
__all__ = (
|
|
15
|
+
__all__ = (
|
|
16
|
+
"Client",
|
|
17
|
+
"KafkaProducer",
|
|
18
|
+
"KafkaConsumer",
|
|
19
|
+
"reliable_client_creation",
|
|
20
|
+
)
|
{diaspora-event-sdk-0.4.1 → diaspora_event_sdk-0.4.4}/diaspora_event_sdk/sdk/_environments.py
RENAMED
|
@@ -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"])
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
from diaspora_event_sdk.sdk.login_manager import (
|
|
4
|
+
LoginManager,
|
|
5
|
+
LoginManagerProtocol,
|
|
6
|
+
requires_login,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
from ._environments import get_web_service_url
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Client:
|
|
13
|
+
def __init__(
|
|
14
|
+
self,
|
|
15
|
+
environment: Optional[str] = None,
|
|
16
|
+
login_manager: Optional[LoginManagerProtocol] = None,
|
|
17
|
+
):
|
|
18
|
+
self.web_service_address = get_web_service_url(environment)
|
|
19
|
+
|
|
20
|
+
# if a login manager was passed, no login flow is triggered
|
|
21
|
+
if login_manager is not None:
|
|
22
|
+
self.login_manager: LoginManagerProtocol = login_manager
|
|
23
|
+
# but if login handling is implicit (as when no login manager is passed)
|
|
24
|
+
# then ensure that the user is logged in
|
|
25
|
+
else:
|
|
26
|
+
self.login_manager = LoginManager(environment=environment)
|
|
27
|
+
self.login_manager.ensure_logged_in()
|
|
28
|
+
|
|
29
|
+
self.web_client = self.login_manager.get_web_client(
|
|
30
|
+
base_url=self.web_service_address
|
|
31
|
+
)
|
|
32
|
+
self.auth_client = self.login_manager.get_auth_client()
|
|
33
|
+
self.subject_openid = self.auth_client.userinfo()["sub"]
|
|
34
|
+
self.namespace = f"ns-{self.subject_openid.replace('-', '')[-12:]}"
|
|
35
|
+
|
|
36
|
+
def logout(self):
|
|
37
|
+
"""Remove credentials from your local system"""
|
|
38
|
+
self.login_manager.logout()
|
|
39
|
+
|
|
40
|
+
@requires_login
|
|
41
|
+
def create_user(self):
|
|
42
|
+
"""
|
|
43
|
+
Create an IAM user with policy and namespace for the current user (POST /api/v3/user).
|
|
44
|
+
Returns status, message, subject, and namespace.
|
|
45
|
+
"""
|
|
46
|
+
resp = self.web_client.create_user(self.subject_openid)
|
|
47
|
+
return resp.data if hasattr(resp, "data") else resp
|
|
48
|
+
|
|
49
|
+
@requires_login
|
|
50
|
+
def delete_user(self):
|
|
51
|
+
"""
|
|
52
|
+
Delete the IAM user and all associated resources for the current user (DELETE /api/v3/user).
|
|
53
|
+
Returns status and message.
|
|
54
|
+
"""
|
|
55
|
+
resp = self.web_client.delete_user(self.subject_openid)
|
|
56
|
+
return resp.data if hasattr(resp, "data") else resp
|
|
57
|
+
|
|
58
|
+
@requires_login
|
|
59
|
+
def create_key(self):
|
|
60
|
+
"""
|
|
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.
|
|
64
|
+
"""
|
|
65
|
+
resp = self.web_client.create_key(self.subject_openid)
|
|
66
|
+
return resp.data if hasattr(resp, "data") else resp
|
|
67
|
+
|
|
68
|
+
@requires_login
|
|
69
|
+
def delete_key(self):
|
|
70
|
+
"""
|
|
71
|
+
Delete access keys from IAM and DynamoDB for the current user (DELETE /api/v3/key).
|
|
72
|
+
Returns status and message.
|
|
73
|
+
"""
|
|
74
|
+
resp = self.web_client.delete_key(self.subject_openid)
|
|
75
|
+
return resp.data if hasattr(resp, "data") else resp
|
|
76
|
+
|
|
77
|
+
@requires_login
|
|
78
|
+
def list_namespaces(self):
|
|
79
|
+
"""
|
|
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).
|
|
82
|
+
"""
|
|
83
|
+
resp = self.web_client.list_namespaces(self.subject_openid)
|
|
84
|
+
return resp.data if hasattr(resp, "data") else resp
|
|
85
|
+
|
|
86
|
+
@requires_login
|
|
87
|
+
def create_topic(self, topic: str):
|
|
88
|
+
"""
|
|
89
|
+
Create a topic under the user's default namespace (POST /api/v3/{namespace}/{topic}).
|
|
90
|
+
Returns status, message, and topics list.
|
|
91
|
+
"""
|
|
92
|
+
resp = self.web_client.create_topic(self.subject_openid, self.namespace, topic)
|
|
93
|
+
return resp.data if hasattr(resp, "data") else resp
|
|
94
|
+
|
|
95
|
+
@requires_login
|
|
96
|
+
def delete_topic(self, topic: str):
|
|
97
|
+
"""
|
|
98
|
+
Delete a topic from the user's default namespace (DELETE /api/v3/{namespace}/{topic}).
|
|
99
|
+
Returns status, message, and topics list.
|
|
100
|
+
"""
|
|
101
|
+
resp = self.web_client.delete_topic(self.subject_openid, self.namespace, topic)
|
|
102
|
+
return resp.data if hasattr(resp, "data") else resp
|
|
103
|
+
|
|
104
|
+
@requires_login
|
|
105
|
+
def recreate_topic(self, topic: str):
|
|
106
|
+
"""
|
|
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.
|
|
109
|
+
"""
|
|
110
|
+
resp = self.web_client.recreate_topic(
|
|
111
|
+
self.subject_openid, self.namespace, topic
|
|
112
|
+
)
|
|
113
|
+
return resp.data if hasattr(resp, "data") else resp
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
import time
|
|
4
|
+
import uuid
|
|
5
|
+
import warnings
|
|
6
|
+
from typing import Any, Dict
|
|
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__)
|
|
13
|
+
|
|
14
|
+
# If kafka-python is not installed, Kafka functionality is not available through diaspora-event-sdk.
|
|
15
|
+
kafka_available = True
|
|
16
|
+
try:
|
|
17
|
+
import os
|
|
18
|
+
|
|
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):
|
|
27
|
+
def token(self):
|
|
28
|
+
token, _ = generate_auth_token("us-east-1")
|
|
29
|
+
return token
|
|
30
|
+
except Exception:
|
|
31
|
+
kafka_available = False
|
|
32
|
+
# Fallback if kafka-python is not available
|
|
33
|
+
TopicAuthorizationFailedError = Exception
|
|
34
|
+
KafkaTimeoutError = Exception
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def get_diaspora_config(extra_configs: Dict[str, Any] = {}) -> Dict[str, Any]:
|
|
38
|
+
"""
|
|
39
|
+
Retrieve default Diaspora event fabric connection configurations for Kafka clients.
|
|
40
|
+
Merges default configurations with custom ones provided.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
try:
|
|
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"]
|
|
49
|
+
|
|
50
|
+
except Exception as e:
|
|
51
|
+
raise RuntimeError("Failed to retrieve Kafka keys") from e
|
|
52
|
+
|
|
53
|
+
conf = {
|
|
54
|
+
"bootstrap_servers": os.environ["OCTOPUS_BOOTSTRAP_SERVERS"],
|
|
55
|
+
"security_protocol": "SASL_SSL",
|
|
56
|
+
"sasl_mechanism": "OAUTHBEARER",
|
|
57
|
+
"api_version": (3, 8, 1),
|
|
58
|
+
"sasl_oauth_token_provider": MSKTokenProvider(),
|
|
59
|
+
}
|
|
60
|
+
conf.update(extra_configs)
|
|
61
|
+
return conf
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
if kafka_available:
|
|
65
|
+
|
|
66
|
+
class KafkaProducer(KProd):
|
|
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
|
+
|
|
79
|
+
configs.setdefault(
|
|
80
|
+
"value_serializer",
|
|
81
|
+
lambda v: json.dumps(v).encode("utf-8"),
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
super().__init__(**get_diaspora_config(configs))
|
|
85
|
+
# Note: We do NOT block on metadata here
|
|
86
|
+
|
|
87
|
+
class KafkaConsumer(KCons):
|
|
88
|
+
def __init__(self, *topics, **configs):
|
|
89
|
+
if not topics:
|
|
90
|
+
raise ValueError("KafkaConsumer requires at least one topic")
|
|
91
|
+
self.topics = topics
|
|
92
|
+
|
|
93
|
+
super().__init__(*topics, **get_diaspora_config(configs))
|
|
94
|
+
# Note: We do NOT block on metadata here
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
else:
|
|
98
|
+
# Create dummy classes that issue a warning when instantiated
|
|
99
|
+
class KafkaProducer: # type: ignore[no-redef]
|
|
100
|
+
def __init__(self, *args, **kwargs):
|
|
101
|
+
warnings.warn(
|
|
102
|
+
"KafkaProducer is not available. Initialization is a no-op.",
|
|
103
|
+
RuntimeWarning,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
class KafkaConsumer: # type: ignore[no-redef]
|
|
107
|
+
def __init__(self, *args, **kwargs):
|
|
108
|
+
warnings.warn(
|
|
109
|
+
"KafkaConsumer is not available. Initialization is a no-op.",
|
|
110
|
+
RuntimeWarning,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def reliable_client_creation() -> str:
|
|
115
|
+
"""
|
|
116
|
+
Reliably create a client and test topic operations with retry logic.
|
|
117
|
+
|
|
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
|
|
130
|
+
try:
|
|
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
|
|
170
|
+
except Exception as e:
|
|
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,
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
import globus_sdk
|
|
4
|
+
|
|
5
|
+
from diaspora_event_sdk.sdk.utils.uuid_like import UUID_LIKE_T
|
|
6
|
+
|
|
7
|
+
from ._environments import get_web_service_url
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class WebClient(globus_sdk.BaseClient):
|
|
11
|
+
def __init__(
|
|
12
|
+
self,
|
|
13
|
+
*,
|
|
14
|
+
environment: Optional[str] = None,
|
|
15
|
+
base_url: Optional[str] = None,
|
|
16
|
+
app_name: Optional[str] = None,
|
|
17
|
+
**kwargs,
|
|
18
|
+
):
|
|
19
|
+
if base_url is None:
|
|
20
|
+
base_url = get_web_service_url(environment)
|
|
21
|
+
|
|
22
|
+
super().__init__(environment=environment, base_url=base_url, **kwargs)
|
|
23
|
+
|
|
24
|
+
self._user_app_name = None
|
|
25
|
+
self.user_app_name = app_name
|
|
26
|
+
|
|
27
|
+
def create_user(self, subject: UUID_LIKE_T) -> globus_sdk.GlobusHTTPResponse:
|
|
28
|
+
"""Call the v3 create_user endpoint (POST /api/v3/user)."""
|
|
29
|
+
return self.post("/api/v3/user", headers={"Subject": str(subject)})
|
|
30
|
+
|
|
31
|
+
def delete_user(self, subject: UUID_LIKE_T) -> globus_sdk.GlobusHTTPResponse:
|
|
32
|
+
"""Call the v3 delete_user endpoint (DELETE /api/v3/user)."""
|
|
33
|
+
return self.delete("/api/v3/user", headers={"Subject": str(subject)})
|
|
34
|
+
|
|
35
|
+
def create_key(self, subject: UUID_LIKE_T) -> globus_sdk.GlobusHTTPResponse:
|
|
36
|
+
"""Call the v3 create_key endpoint (POST /api/v3/key)."""
|
|
37
|
+
return self.post("/api/v3/key", headers={"Subject": str(subject)})
|
|
38
|
+
|
|
39
|
+
def delete_key(self, subject: UUID_LIKE_T) -> globus_sdk.GlobusHTTPResponse:
|
|
40
|
+
"""Call the v3 delete_key endpoint (DELETE /api/v3/key)."""
|
|
41
|
+
return self.delete("/api/v3/key", headers={"Subject": str(subject)})
|
|
42
|
+
|
|
43
|
+
def list_namespaces(self, subject: UUID_LIKE_T) -> globus_sdk.GlobusHTTPResponse:
|
|
44
|
+
"""Call the v3 list_namespaces endpoint (GET /api/v3/namespace)."""
|
|
45
|
+
return self.get("/api/v3/namespace", headers={"Subject": str(subject)})
|
|
46
|
+
|
|
47
|
+
def create_topic(
|
|
48
|
+
self, subject: UUID_LIKE_T, namespace: str, topic: str
|
|
49
|
+
) -> globus_sdk.GlobusHTTPResponse:
|
|
50
|
+
"""Call the v3 create_topic endpoint (POST /api/v3/{namespace}/{topic})."""
|
|
51
|
+
return self.post(
|
|
52
|
+
f"/api/v3/{namespace}/{topic}", headers={"Subject": str(subject)}
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
def delete_topic(
|
|
56
|
+
self, subject: UUID_LIKE_T, namespace: str, topic: str
|
|
57
|
+
) -> globus_sdk.GlobusHTTPResponse:
|
|
58
|
+
"""Call the v3 delete_topic endpoint (DELETE /api/v3/{namespace}/{topic})."""
|
|
59
|
+
return self.delete(
|
|
60
|
+
f"/api/v3/{namespace}/{topic}", headers={"Subject": str(subject)}
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
def recreate_topic(
|
|
64
|
+
self, subject: UUID_LIKE_T, namespace: str, topic: str
|
|
65
|
+
) -> globus_sdk.GlobusHTTPResponse:
|
|
66
|
+
"""Call the v3 recreate_topic endpoint (PUT /api/v3/{namespace}/{topic}/recreate)."""
|
|
67
|
+
return self.put(
|
|
68
|
+
f"/api/v3/{namespace}/{topic}/recreate",
|
|
69
|
+
headers={"Subject": str(subject)},
|
|
70
|
+
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.4.4"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: diaspora-event-sdk
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.4
|
|
4
4
|
Summary: Diaspora Event Fabric SDK
|
|
5
5
|
Home-page: https://github.com/globus-labs/diaspora-event-sdk
|
|
6
6
|
License: Apache 2.0
|
|
@@ -15,9 +15,27 @@ Classifier: Programming Language :: Python :: 3.10
|
|
|
15
15
|
Classifier: Programming Language :: Python :: 3.11
|
|
16
16
|
Classifier: Programming Language :: Python :: 3.12
|
|
17
17
|
Description-Content-Type: text/markdown
|
|
18
|
+
License-File: LICENSE
|
|
19
|
+
Requires-Dist: globus-sdk<4,>=3.59.0
|
|
18
20
|
Provides-Extra: kafka-python
|
|
21
|
+
Requires-Dist: kafka-python; extra == "kafka-python"
|
|
19
22
|
Provides-Extra: test
|
|
20
|
-
|
|
23
|
+
Requires-Dist: pytest; extra == "test"
|
|
24
|
+
Requires-Dist: pytest-cov; extra == "test"
|
|
25
|
+
Requires-Dist: coverage; extra == "test"
|
|
26
|
+
Requires-Dist: mypy; extra == "test"
|
|
27
|
+
Requires-Dist: tox; extra == "test"
|
|
28
|
+
Requires-Dist: check-manifest; extra == "test"
|
|
29
|
+
Requires-Dist: pre-commit; extra == "test"
|
|
30
|
+
Dynamic: classifier
|
|
31
|
+
Dynamic: description
|
|
32
|
+
Dynamic: description-content-type
|
|
33
|
+
Dynamic: home-page
|
|
34
|
+
Dynamic: license
|
|
35
|
+
Dynamic: license-file
|
|
36
|
+
Dynamic: provides-extra
|
|
37
|
+
Dynamic: requires-dist
|
|
38
|
+
Dynamic: summary
|
|
21
39
|
|
|
22
40
|
# Diaspora Event Fabric SDK
|
|
23
41
|
|