Skip to content

Commit 2b68dcd

Browse files
[Event Hubs] created EventHubConnectionStringProperties and connection string parser (Azure#16204)
* created EventHubConnectionStringProperties and connection string parser * changing event_hub naming to eventhub * fix eventhub_name in test * remove print stmt Co-authored-by: Adam Ling (MSFT) <adam_ling@outlook.com> * add contains to DictMixin Co-authored-by: Adam Ling (MSFT) <adam_ling@outlook.com>
1 parent 79a64bb commit 2b68dcd

File tree

5 files changed

+254
-0
lines changed

5 files changed

+254
-0
lines changed

sdk/eventhub/azure-eventhub/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
## 5.2.2 (Unreleased)
44

5+
**New Features**
6+
7+
* Added a `parse_connection_string` method which parses a connection string into a properties bag, `EventHubConnectionStringProperties`, containing its component parts.
58

69
## 5.2.1 (2021-01-11)
710

sdk/eventhub/azure-eventhub/azure/eventhub/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@
1414
from ._eventprocessor.checkpoint_store import CheckpointStore
1515
from ._eventprocessor.common import CloseReason, LoadBalancingStrategy
1616
from ._eventprocessor.partition_context import PartitionContext
17+
from ._connection_string_parser import (
18+
parse_connection_string,
19+
EventHubConnectionStringProperties
20+
)
1721

1822
TransportType = constants.TransportType
1923

@@ -28,4 +32,6 @@
2832
"CloseReason",
2933
"LoadBalancingStrategy",
3034
"PartitionContext",
35+
"parse_connection_string",
36+
"EventHubConnectionStringProperties"
3137
]

sdk/eventhub/azure-eventhub/azure/eventhub/_common.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -437,3 +437,69 @@ def add(self, event_data):
437437
self.message._body_gen.append(event_data) # pylint: disable=protected-access
438438
self._size = size_after_add
439439
self._count += 1
440+
441+
class DictMixin(object):
442+
def __setitem__(self, key, item):
443+
# type: (Any, Any) -> None
444+
self.__dict__[key] = item
445+
446+
def __getitem__(self, key):
447+
# type: (Any) -> Any
448+
return self.__dict__[key]
449+
450+
def __contains__(self, key):
451+
return key in self.__dict__
452+
453+
def __repr__(self):
454+
# type: () -> str
455+
return str(self)
456+
457+
def __len__(self):
458+
# type: () -> int
459+
return len(self.keys())
460+
461+
def __delitem__(self, key):
462+
# type: (Any) -> None
463+
self.__dict__[key] = None
464+
465+
def __eq__(self, other):
466+
# type: (Any) -> bool
467+
"""Compare objects by comparing all attributes."""
468+
if isinstance(other, self.__class__):
469+
return self.__dict__ == other.__dict__
470+
return False
471+
472+
def __ne__(self, other):
473+
# type: (Any) -> bool
474+
"""Compare objects by comparing all attributes."""
475+
return not self.__eq__(other)
476+
477+
def __str__(self):
478+
# type: () -> str
479+
return str({k: v for k, v in self.__dict__.items() if not k.startswith("_")})
480+
481+
def has_key(self, k):
482+
# type: (Any) -> bool
483+
return k in self.__dict__
484+
485+
def update(self, *args, **kwargs):
486+
# type: (Any, Any) -> None
487+
return self.__dict__.update(*args, **kwargs)
488+
489+
def keys(self):
490+
# type: () -> list
491+
return [k for k in self.__dict__ if not k.startswith("_")]
492+
493+
def values(self):
494+
# type: () -> list
495+
return [v for k, v in self.__dict__.items() if not k.startswith("_")]
496+
497+
def items(self):
498+
# type: () -> list
499+
return [(k, v) for k, v in self.__dict__.items() if not k.startswith("_")]
500+
501+
def get(self, key, default=None):
502+
# type: (Any, Optional[Any]) -> Any
503+
if key in self.__dict__:
504+
return self.__dict__[key]
505+
return default
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
# --------------------------------------------------------------------------------------------
2+
# Copyright (c) Microsoft Corporation. All rights reserved.
3+
# Licensed under the MIT License. See License.txt in the project root for license information.
4+
# --------------------------------------------------------------------------------------------
5+
try:
6+
from urllib.parse import urlparse
7+
except ImportError:
8+
from urlparse import urlparse # type: ignore
9+
10+
from ._common import DictMixin
11+
12+
13+
class EventHubConnectionStringProperties(DictMixin):
14+
"""
15+
Properties of a connection string.
16+
"""
17+
18+
def __init__(self, **kwargs):
19+
self._fully_qualified_namespace = kwargs.pop("fully_qualified_namespace", None)
20+
self._endpoint = kwargs.pop("endpoint", None)
21+
self._eventhub_name = kwargs.pop("eventhub_name", None)
22+
self._shared_access_signature = kwargs.pop("shared_access_signature", None)
23+
self._shared_access_key_name = kwargs.pop("shared_access_key_name", None)
24+
self._shared_access_key = kwargs.pop("shared_access_key", None)
25+
26+
@property
27+
def fully_qualified_namespace(self):
28+
"""The fully qualified host name for the Event Hubs namespace.
29+
The namespace format is: `<yournamespace>.servicebus.windows.net`.
30+
"""
31+
return self._fully_qualified_namespace
32+
33+
@property
34+
def endpoint(self):
35+
"""The endpoint for the Event Hubs resource. In the format sb://<FQDN>/"""
36+
return self._endpoint
37+
38+
@property
39+
def eventhub_name(self):
40+
"""Optional. The name of the Event Hub, represented by `EntityPath` in the connection string."""
41+
return self._eventhub_name
42+
43+
@property
44+
def shared_access_signature(self):
45+
"""
46+
This can be provided instead of the shared_access_key_name and the shared_access_key.
47+
"""
48+
return self._shared_access_signature
49+
50+
@property
51+
def shared_access_key_name(self):
52+
"""
53+
The name of the shared_access_key. This must be used along with the shared_access_key.
54+
"""
55+
return self._shared_access_key_name
56+
57+
@property
58+
def shared_access_key(self):
59+
"""
60+
The shared_access_key can be used along with the shared_access_key_name as a credential.
61+
"""
62+
return self._shared_access_key
63+
64+
65+
def parse_connection_string(conn_str):
66+
# type(str) -> EventHubConnectionStringProperties
67+
"""Parse the connection string into a properties bag containing its component parts.
68+
69+
:param conn_str: The connection string that has to be parsed.
70+
:type conn_str: str
71+
:rtype: ~azure.eventhub.EventHubConnectionStringProperties
72+
"""
73+
conn_settings = [s.split("=", 1) for s in conn_str.split(";")]
74+
if any(len(tup) != 2 for tup in conn_settings):
75+
raise ValueError("Connection string is either blank or malformed.")
76+
conn_settings = dict(conn_settings)
77+
shared_access_signature = None
78+
for key, value in conn_settings.items():
79+
if key.lower() == "sharedaccesssignature":
80+
shared_access_signature = value
81+
shared_access_key = conn_settings.get("SharedAccessKey")
82+
shared_access_key_name = conn_settings.get("SharedAccessKeyName")
83+
if any([shared_access_key, shared_access_key_name]) and not all(
84+
[shared_access_key, shared_access_key_name]
85+
):
86+
raise ValueError(
87+
"Connection string must have both SharedAccessKeyName and SharedAccessKey."
88+
)
89+
if shared_access_signature is not None and shared_access_key is not None:
90+
raise ValueError(
91+
"Only one of the SharedAccessKey or SharedAccessSignature must be present."
92+
)
93+
endpoint = conn_settings.get("Endpoint")
94+
if not endpoint:
95+
raise ValueError("Connection string is either blank or malformed.")
96+
parsed = urlparse(endpoint.rstrip("/"))
97+
if not parsed.netloc:
98+
raise ValueError("Invalid Endpoint on the Connection String.")
99+
namespace = parsed.netloc.strip()
100+
props = {
101+
"fully_qualified_namespace": namespace,
102+
"endpoint": endpoint,
103+
"eventhub_name": conn_settings.get("EntityPath"),
104+
"shared_access_signature": shared_access_signature,
105+
"shared_access_key_name": shared_access_key_name,
106+
"shared_access_key": shared_access_key,
107+
}
108+
return EventHubConnectionStringProperties(**props)
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
#-------------------------------------------------------------------------
2+
# Copyright (c) Microsoft Corporation. All rights reserved.
3+
# Licensed under the MIT License. See License.txt in the project root for
4+
# license information.
5+
#--------------------------------------------------------------------------
6+
7+
import os
8+
import pytest
9+
from azure.eventhub import (
10+
EventHubConnectionStringProperties,
11+
parse_connection_string,
12+
)
13+
14+
from devtools_testutils import AzureMgmtTestCase
15+
16+
class EventHubConnectionStringParserTests(AzureMgmtTestCase):
17+
18+
def test_eh_conn_str_parse_cs(self, **kwargs):
19+
conn_str = 'Endpoint=sb://eh-namespace.servicebus.windows.net/;SharedAccessKeyName=test-policy;SharedAccessKey=THISISATESTKEYXXXXXXXXXXXXXXXXXXXXXXXXXXXX='
20+
parse_result = parse_connection_string(conn_str)
21+
assert parse_result.endpoint == 'sb://eh-namespace.servicebus.windows.net/'
22+
assert parse_result.fully_qualified_namespace == 'eh-namespace.servicebus.windows.net'
23+
assert parse_result.shared_access_key_name == 'test-policy'
24+
assert parse_result.shared_access_key == 'THISISATESTKEYXXXXXXXXXXXXXXXXXXXXXXXXXXXX='
25+
26+
def test_eh_conn_str_parse_with_entity_path(self, **kwargs):
27+
conn_str = 'Endpoint=sb://eh-namespace.servicebus.windows.net/;SharedAccessKeyName=test-policy;SharedAccessKey=THISISATESTKEYXXXXXXXXXXXXXXXXXXXXXXXXXXXX=;EntityPath=eventhub-name'
28+
parse_result = parse_connection_string(conn_str)
29+
assert parse_result.endpoint == 'sb://eh-namespace.servicebus.windows.net/'
30+
assert parse_result.fully_qualified_namespace == 'eh-namespace.servicebus.windows.net'
31+
assert parse_result.shared_access_key_name == 'test-policy'
32+
assert parse_result.shared_access_key == 'THISISATESTKEYXXXXXXXXXXXXXXXXXXXXXXXXXXXX='
33+
assert parse_result.eventhub_name == 'eventhub-name'
34+
35+
def test_eh_conn_str_parse_sas_and_shared_key(self, **kwargs):
36+
conn_str = 'Endpoint=sb://eh-namespace.servicebus.windows.net/;SharedAccessKeyName=test-policy;SharedAccessKey=THISISATESTKEYXXXXXXXXXXXXXXXXXXXXXXXXXXXX=;SharedAccessSignature=THISISASASXXXXXXX='
37+
with pytest.raises(ValueError) as e:
38+
parse_result = parse_connection_string(conn_str)
39+
assert str(e.value) == 'Only one of the SharedAccessKey or SharedAccessSignature must be present.'
40+
41+
def test_eh_parse_malformed_conn_str_no_endpoint(self, **kwargs):
42+
conn_str = 'SharedAccessKeyName=test-policy;SharedAccessKey=THISISATESTKEYXXXXXXXXXXXXXXXXXXXXXXXXXXXX='
43+
with pytest.raises(ValueError) as e:
44+
parse_result = parse_connection_string(conn_str)
45+
assert str(e.value) == 'Connection string is either blank or malformed.'
46+
47+
def test_eh_parse_malformed_conn_str_no_netloc(self, **kwargs):
48+
conn_str = 'Endpoint=MALFORMED;SharedAccessKeyName=test-policy;SharedAccessKey=THISISATESTKEYXXXXXXXXXXXXXXXXXXXXXXXXXXXX='
49+
with pytest.raises(ValueError) as e:
50+
parse_result = parse_connection_string(conn_str)
51+
assert str(e.value) == 'Invalid Endpoint on the Connection String.'
52+
53+
def test_eh_parse_conn_str_sas(self, **kwargs):
54+
conn_str = 'Endpoint=sb://eh-namespace.servicebus.windows.net/;SharedAccessSignature=THISISATESTKEYXXXXXXXXXXXXXXXXXXXXXXXXXXXX='
55+
parse_result = parse_connection_string(conn_str)
56+
assert parse_result.endpoint == 'sb://eh-namespace.servicebus.windows.net/'
57+
assert parse_result.fully_qualified_namespace == 'eh-namespace.servicebus.windows.net'
58+
assert parse_result.shared_access_signature == 'THISISATESTKEYXXXXXXXXXXXXXXXXXXXXXXXXXXXX='
59+
assert parse_result.shared_access_key_name == None
60+
61+
def test_eh_parse_conn_str_no_keyname(self, **kwargs):
62+
conn_str = 'Endpoint=sb://eh-namespace.servicebus.windows.net/;SharedAccessKey=THISISATESTKEYXXXXXXXXXXXXXXXXXXXXXXXXXXXX='
63+
with pytest.raises(ValueError) as e:
64+
parse_result = parse_connection_string(conn_str)
65+
assert str(e.value) == 'Connection string must have both SharedAccessKeyName and SharedAccessKey.'
66+
67+
def test_eh_parse_conn_str_no_key(self, **kwargs):
68+
conn_str = 'Endpoint=sb://eh-namespace.servicebus.windows.net/;SharedAccessKeyName=test-policy'
69+
with pytest.raises(ValueError) as e:
70+
parse_result = parse_connection_string(conn_str)
71+
assert str(e.value) == 'Connection string must have both SharedAccessKeyName and SharedAccessKey.'

0 commit comments

Comments
 (0)