Skip to content

Commit 41b7eea

Browse files
committed
Add more tests and fix some of the code that wasn't working.
1 parent 1667374 commit 41b7eea

File tree

11 files changed

+359
-26
lines changed

11 files changed

+359
-26
lines changed

opentelemetry-instrumentation/src/opentelemetry/instrumentation/_blobupload/api/blob.py

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import base64
2-
from typing import Dict, Optional
2+
import json
3+
4+
from types import MappingProxyType as _frozendict
5+
from typing import Mapping, Dict, Optional
36

47

58
class Blob(object):
@@ -17,7 +20,7 @@ def __init__(
1720
self,
1821
raw_bytes: bytes,
1922
content_type: Optional[str] = None,
20-
labels: Optional[Dict[str, str]] = None,
23+
labels: Optional[Mapping[str, str]] = None,
2124
):
2225
"""Initialize the blob with an explicit set of properties.
2326
@@ -26,12 +29,18 @@ def __init__(
2629
content_type: the MIME type describing the type of data in the payload
2730
labels: additional key/value data about the Blob
2831
"""
29-
self._raw_bytes = _raw_bytes
32+
self._raw_bytes = raw_bytes
3033
self._content_type = content_type
31-
self._labels = labels or {}
34+
self._labels = {}
35+
if labels is not None:
36+
if isinstance(labels, dict):
37+
self._labels.update(labels)
38+
else:
39+
for k in labels:
40+
self._labels[k] = labels[k]
3241

3342
@staticmethod
34-
def from_data_uri(cls, uri: str, labels: Optional[dict] = None) -> "Blob":
43+
def from_data_uri(uri: str, labels: Optional[dict] = None) -> "Blob":
3544
"""Instantiate a blob from a 'data:...' URI.
3645
3746
Args:
@@ -67,10 +76,7 @@ def from_data_uri(cls, uri: str, labels: Optional[dict] = None) -> "Blob":
6776
assert remaining.startswith("base64,")
6877
base64_len = len("base64,")
6978
base64_encoded_content = remaining[base64_len:]
70-
try:
71-
raw_bytes = base64.standard_b64decode(base64_encoded_content)
72-
except ValueError:
73-
raw_bytes = base64.urlsafe_b64decode(base64_encoded_content)
79+
raw_bytes = base64.b64decode(base64_encoded_content)
7480
return Blob(raw_bytes, content_type=content_type, labels=labels)
7581

7682
@property
@@ -84,6 +90,23 @@ def content_type(self) -> Optional[str]:
8490
return self._content_type
8591

8692
@property
87-
def labels(self) -> Dict[str, str]:
93+
def labels(self) -> Mapping[str, str]:
8894
"""Returns the key/value metadata of this Blob."""
8995
return _frozendict(self._labels)
96+
97+
def __eq__(self, o):
98+
return (
99+
(isinstance(o, Blob)) and
100+
(self.raw_bytes == o.raw_bytes) and
101+
(self.content_type == o.content_type) and
102+
(self.labels == o.labels)
103+
)
104+
105+
def __repr__(self):
106+
params = [repr(self._raw_bytes)]
107+
if self._content_type is not None:
108+
params.append('content_type={}'.format(repr(self._content_type)))
109+
if self._labels:
110+
params.append('labels={}'.format(json.dumps(self._labels, sort_keys=True)))
111+
params_string = ', '.join(params)
112+
return 'Blob({})'.format(params_string)

opentelemetry-instrumentation/src/opentelemetry/instrumentation/_blobupload/api/content_type.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,6 @@ def from_buffer(self, raw_bytes, mime=True):
2323

2424
def detect_content_type(raw_bytes: bytes) -> str:
2525
"""Attempts to infer the content type of the specified data."""
26+
if not raw_bytes:
27+
return 'application/octet-stream'
2628
return _module.from_buffer(raw_bytes, mime=True)

opentelemetry-instrumentation/src/opentelemetry/instrumentation/_blobupload/api/provider.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
from opentelemetry.instrumentation._blobupload.api.blob_uploader import (
77
BlobUploader,
88
)
9+
from opentelemetry.instrumentation._blobupload.api.constants import NOT_UPLOADED
10+
911

1012
_logger = logging.getLogger(__name__)
1113

@@ -42,7 +44,7 @@ def get_blob_uploader(self, use_case: Optional[str]) -> BlobUploader:
4244
if use_case:
4345
use_case_formatted = use_case
4446
_logger.warning(
45-
"No BlobUploaderProvider configured; returning a no-op for use case {}".format(
47+
"No BlobUploaderProvider configured; returning a no-op for use case \"{}\". Use 'set_blob_uploader_provider()' to configure.".format(
4648
use_case_formatted
4749
)
4850
)
@@ -52,10 +54,12 @@ def get_blob_uploader(self, use_case: Optional[str]) -> BlobUploader:
5254
_blob_uploader_provider = _DefaultBlobUploaderProvider()
5355

5456

55-
def set_blob_uploader_provider(provider: BlobUploaderProvider):
57+
def set_blob_uploader_provider(provider: BlobUploaderProvider) -> BlobUploaderProvider:
5658
"""Allows configuring the behavior of 'get_blob_uploader."""
5759
global _blob_uploader_provider
60+
old_provider = _blob_uploader_provider
5861
_blob_uploader_provider = provider
62+
return old_provider
5963

6064

6165
def get_blob_uploader(use_case: Optional[str] = None) -> BlobUploader:
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,5 @@
11
from opentelemetry.instrumentation._blobupload.backend.google.gcs._gcs_impl import GcsBlobUploader
2+
3+
__all__ = [
4+
GcsBlobUploader
5+
]
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
"""Exposes API methods to callers from the package name."""
2+
3+
from opentelemetry.instrumentation._blobupload.utils.simple_blob_uploader_adaptor import blob_uploader_from_simple_blob_uploader
4+
from opentelemetry.instrumentation._blobupload.utils.simple_blob_uploader import SimpleBlobUploader
5+
6+
__all__ = [
7+
blob_uploader_from_simple_blob_uploader,
8+
SimpleBlobUploader,
9+
]
Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
"""Defines a simple, synchronous interface for providing a backend implementation."""
22

3+
import abc
34

4-
class SimpleBlobUploader(ABC):
5+
from opentelemetry.instrumentation._blobupload.api import Blob
6+
7+
class SimpleBlobUploader(abc.ABC):
58
"""Pure abstract base class of a backend implementation that is synchronous."""
69

7-
@abstractmethod
8-
def generate_destination_uri(self, blob: Blob) -> str:
10+
@abc.abstractmethod
11+
def generate_destination_uri(self, blob: Blob) -> str:
912
"""Generates a URI of where the blob will get written.
1013
1114
Args:
@@ -16,8 +19,8 @@ def generate_destination_uri(self, blob: Blob) -> str:
1619
"""
1720
raise NotImplementedError('SimpleBlobUploader.generate_destination_uri')
1821

19-
@abstractmethod
20-
def upload_sync(self, uri: str, blob: Blob):
22+
@abc.abstractmethod
23+
def upload_sync(self, uri: str, blob: Blob):
2124
"""Synchronously writes the blob to the specified destination URI.
2225
2326
Args:
@@ -28,4 +31,4 @@ def upload_sync(self, uri: str, blob: Blob):
2831
Effects:
2932
Attempts to upload/write the Blob to the specified destination URI.
3033
"""
31-
raise NotImplementedError('SimpleBlobUploader.upload_sync')
34+
raise NotImplementedError('SimpleBlobUploader.upload_sync')

opentelemetry-instrumentation/src/opentelemetry/instrumentation/_blobupload/utils/simple_blob_uploader_adaptor.py

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,19 @@
11
import atexit
2+
import logging
23

3-
from concurrent.futures import Executor, ProcessPoolExecutor
4+
from typing import Optional
5+
from concurrent.futures import Executor, ThreadPoolExecutor
46

5-
from opentelemetry.instrumentation._blobupload.api import Blob
6-
from opentelemetry.instrumentation._blobupload.api import BlobUploader
7-
from opentelemetry.instrumentation._blobupload.api import detect_content_type
7+
from opentelemetry.instrumentation._blobupload.api import (
8+
Blob,
9+
BlobUploader,
10+
detect_content_type)
811
from opentelemetry.instrumentation._blobupload.utils.simple_blob_uploader import SimpleBlobUploader
912

1013

14+
_logger = logging.getLogger(__name__)
15+
16+
1117
def _with_content_type(blob: Blob) -> Blob:
1218
"""Returns a variant of the Blob with the content type auto-detected if needed."""
1319
if blob.content_type is not None:
@@ -16,7 +22,7 @@ def _with_content_type(blob: Blob) -> Blob:
1622
return Blob(blob.raw_bytes, content_type=content_type, labels=blob.labels)
1723

1824

19-
def _UploadAction(object):
25+
class _UploadAction(object):
2026
"""Represents the work to be done in the background to upload a blob."""
2127

2228
def __init__(self, simple_uploader, uri, blob):
@@ -25,7 +31,11 @@ def __init__(self, simple_uploader, uri, blob):
2531
self._blob = blob
2632

2733
def __call__(self):
28-
self._simple_uploader.upload_sync(self._uri, self._blob)
34+
_logger.debug('Uploading blob to "{}".'.format(self._uri))
35+
try:
36+
self._simple_uploader.upload_sync(self._uri, self._blob)
37+
except:
38+
_logger.error('Failed to upload blob to "{}".'.format(self._uri))
2939

3040

3141
def _create_default_executor_no_cleanup():
@@ -37,14 +47,16 @@ def _create_default_executor_no_cleanup():
3747
# It is because of this potential future enhancement, that we
3848
# have moved this logic into a separate function despite it
3949
# being currently logically quite simple.
40-
return ProcessPoolExecutor()
50+
_logger.debug('Creating thread pool executor')
51+
return ThreadPoolExecutor()
4152

4253

4354
def _create_default_executor():
4455
"""Creates an executor and registers appropriate cleanup."""
4556
result = _create_default_executor_no_cleanup()
4657
def _cleanup():
4758
result.shutdown()
59+
_logger.debug('Registering cleanup for the pool')
4860
atexit.register(_cleanup)
4961
return result
5062

@@ -58,7 +70,10 @@ def _get_or_create_default_executor():
5870
"""Return or lazily instantiate a shared default executor."""
5971
global _default_executor
6072
if _default_executor is None:
73+
_logger.debug('No existing executor found; creating one lazily.')
6174
_default_executor = _create_default_executor()
75+
else:
76+
_logger.debug('Reusing existing executor.')
6277
return _default_executor
6378

6479

@@ -79,6 +94,7 @@ def upload_async(self, blob: Blob) -> str:
7994
return uri
8095

8196
def _do_in_background(self, action):
97+
_logger.debug('Scheduling background upload.')
8298
self._executor.submit(action)
8399

84100

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
#! /usr/bin/env python3
2+
3+
if __name__ == "__main__":
4+
import sys
5+
sys.path.append("../../../src")
6+
7+
import base64
8+
import unittest
9+
10+
11+
from opentelemetry.instrumentation._blobupload.api import Blob
12+
13+
14+
class TestBlob(unittest.TestCase):
15+
16+
def test_construction_with_just_bytes(self):
17+
data = 'some string'.encode()
18+
blob = Blob(data)
19+
self.assertEqual(blob.raw_bytes, data)
20+
self.assertIsNone(blob.content_type)
21+
self.assertIsNotNone(blob.labels)
22+
self.assertEqual(len(blob.labels), 0)
23+
24+
def test_construction_with_bytes_and_content_type(self):
25+
data = 'some string'.encode()
26+
content_type = 'text/plain'
27+
blob = Blob(data, content_type=content_type)
28+
self.assertEqual(blob.raw_bytes, data)
29+
self.assertEqual(blob.content_type, content_type)
30+
self.assertIsNotNone(blob.labels)
31+
self.assertEqual(len(blob.labels), 0)
32+
33+
def test_construction_with_bytes_and_labels(self):
34+
data = 'some string'.encode()
35+
labels = {'key1': 'value1', 'key2': 'value2'}
36+
blob = Blob(data, labels=labels)
37+
self.assertEqual(blob.raw_bytes, data)
38+
self.assertIsNone(blob.content_type)
39+
self.assert_labels_equal(blob.labels, labels)
40+
41+
def test_construction_with_all_fields(self):
42+
data = 'some string'.encode()
43+
content_type = 'text/plain'
44+
labels = {'key1': 'value1', 'key2': 'value2'}
45+
blob = Blob(data, content_type=content_type, labels=labels)
46+
self.assertEqual(blob.raw_bytes, data)
47+
self.assertEqual(blob.content_type, content_type)
48+
self.assert_labels_equal(blob.labels, labels)
49+
50+
def test_from_data_uri_without_labels(self):
51+
data = 'some string'.encode()
52+
content_type = 'text/plain'
53+
encoded_data = base64.b64encode(data).decode()
54+
uri = 'data:{};base64,{}'.format(content_type, encoded_data)
55+
blob = Blob.from_data_uri(uri)
56+
self.assertEqual(blob.raw_bytes, data)
57+
self.assertEqual(blob.content_type, content_type)
58+
self.assertIsNotNone(blob.labels)
59+
self.assertEqual(len(blob.labels), 0)
60+
61+
def test_from_data_uri_with_labels(self):
62+
data = 'some string'.encode()
63+
content_type = 'text/plain'
64+
encoded_data = base64.b64encode(data).decode()
65+
uri = 'data:{};base64,{}'.format(content_type, encoded_data)
66+
labels = {'key1': 'value1', 'key2': 'value2'}
67+
blob = Blob.from_data_uri(uri, labels=labels)
68+
self.assertEqual(blob.raw_bytes, data)
69+
self.assertEqual(blob.content_type, content_type)
70+
self.assert_labels_equal(blob.labels, labels)
71+
72+
def test_from_data_uri_with_valid_standard_base64(self):
73+
data = 'some string'.encode()
74+
content_type = 'text/plain'
75+
encoded_data = base64.standard_b64encode(data).decode()
76+
uri = 'data:{};base64,{}'.format(content_type, encoded_data)
77+
blob = Blob.from_data_uri(uri)
78+
self.assertEqual(blob.raw_bytes, data)
79+
self.assertEqual(blob.content_type, content_type)
80+
81+
def test_from_data_uri_with_valid_websafe_base64(self):
82+
data = 'some string'.encode()
83+
content_type = 'text/plain'
84+
encoded_data = base64.urlsafe_b64encode(data).decode()
85+
uri = 'data:{};base64,{}'.format(content_type, encoded_data)
86+
blob = Blob.from_data_uri(uri)
87+
self.assertEqual(blob.raw_bytes, data)
88+
self.assertEqual(blob.content_type, content_type)
89+
90+
def test_from_data_uri_with_non_data_uri_content(self):
91+
with self.assertRaisesRegex(ValueError, 'expected "data:" prefix'):
92+
Blob.from_data_uri('not a valid data uri')
93+
94+
def test_from_data_uri_with_non_base64_content(self):
95+
with self.assertRaisesRegex(ValueError, 'expected ";base64," section'):
96+
Blob.from_data_uri('data:text/plain,validifpercentencoded')
97+
98+
def assert_labels_equal(self, a, b):
99+
self.assertEqual(len(a), len(b), msg='Different sizes: {} vs {}; a={}, b={}'.format(len(a), len(b), a, b))
100+
for k in a:
101+
self.assertTrue(k in b, msg='Key {} found in a but not b'.format(k))
102+
va = a[k]
103+
vb = b[k]
104+
self.assertEqual(va, vb, msg='Values for key {} different for a vs b: {} vs {}'.format(k, va, vb))
105+
106+
107+
if __name__ == "__main__":
108+
unittest.main()

opentelemetry-instrumentation/tests/_blobupload/api/test_content_type.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
if __name__ == "__main__":
44
import sys
5-
65
sys.path.append("../../../src")
76

87
import io
@@ -25,6 +24,12 @@ def create_test_image(format):
2524

2625

2726
class TestContentType(unittest.TestCase):
27+
28+
def test_handles_empty_correctly(self):
29+
input = bytes()
30+
output = detect_content_type(input)
31+
self.assertEqual(output, "application/octet-stream")
32+
2833
def test_detects_plaintext(self):
2934
input = "this is just regular text"
3035
output = detect_content_type(input.encode())

0 commit comments

Comments
 (0)