Repository: cloudevents/sdk-python

Issue:
Structured extensions aren't serialized top level in to_http result
## Actual Behavior
```python
from cloudevents.sdk.http_events import CloudEvent
attributes = {"source": "<source-url>", "type": "com.issue.extensions", "example-extension": "ext1"}
data = 'Hello'
event = CloudEvent(attributes, data)
headers, body = event.to_http()
print(body)
```

The above code will produce the following:

```
b'{"specversion": "1.0", "id": "fc713795-93f6-44a8-b67e-7b8bd7071e2a", "source": "<source-url>", "type": "com.issue.extensions", "time": "2020-07-16T22:41:48.222788+00:00", "extensions": {"example-extension": "ext1"}, "data": "Hello"}'
```

When in fact we expectthis to be outputted:
```
b'{"specversion": "1.0", "id": "fc713795-93f6-44a8-b67e-7b8bd7071e2a", "source": "<source-url>", "type": "com.issue.extensions", "time": "2020-07-16T22:41:48.222788+00:00", "example-extension": "ext1", "data": "Hello"}'
```

Current output is readable by python CloudEvents, but possibly not by other cloudevent systems unless I'm misunderstanding extensions. 

Discussion:
grayside: I have not read into the spec sufficiently to evaluate this requirement.

Patch:
diff --git a/cloudevents/sdk/event/base.py b/cloudevents/sdk/event/base.py
index 791eb67..2004dbb 100644
--- a/cloudevents/sdk/event/base.py
+++ b/cloudevents/sdk/event/base.py
@@ -201,6 +201,9 @@ class BaseEvent(EventGetterSetter):
                 props["data_base64"] = base64.b64encode(data).decode("ascii")
             else:
                 props["data"] = data
+        if "extensions" in props:
+            extensions = props.pop("extensions")
+            props.update(extensions)
         return json.dumps(props)
 
     def UnmarshalJSON(


Test patch:
diff --git a/cloudevents/tests/test_event_extensions.py b/cloudevents/tests/test_event_extensions.py
new file mode 100644
index 0000000..57afff4
--- /dev/null
+++ b/cloudevents/tests/test_event_extensions.py
@@ -0,0 +1,92 @@
+# All Rights Reserved.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+import json
+
+import pytest
+
+from cloudevents.sdk.http import (
+    CloudEvent,
+    from_http,
+    to_binary_http,
+    to_structured_http,
+)
+
+test_data = json.dumps({"data-key": "val"})
+test_attributes = {
+    "type": "com.example.string",
+    "source": "https://example.com/event-producer",
+    "ext1": "testval",
+}
+
+
+@pytest.mark.parametrize("specversion", ["0.3", "1.0"])
+def test_cloudevent_access_extensions(specversion):
+    event = CloudEvent(test_attributes, test_data)
+    assert event["ext1"] == "testval"
+
+
+@pytest.mark.parametrize("specversion", ["0.3", "1.0"])
+def test_to_binary_extensions(specversion):
+    event = CloudEvent(test_attributes, test_data)
+    headers, body = to_binary_http(event)
+
+    assert "ce-ext1" in headers
+    assert headers.get("ce-ext1") == test_attributes["ext1"]
+
+
+@pytest.mark.parametrize("specversion", ["0.3", "1.0"])
+def test_from_binary_extensions(specversion):
+    headers = {
+        "ce-id": "1234",
+        "ce-source": "<my url>",
+        "ce-type": "sample",
+        "ce-specversion": specversion,
+        "ce-ext1": "test1",
+        "ce-ext2": "test2",
+    }
+    body = json.dumps({"data-key": "val"})
+    event = from_http(body, headers)
+
+    assert headers["ce-ext1"] == event["ext1"]
+    assert headers["ce-ext2"] == event["ext2"]
+
+
+@pytest.mark.parametrize("specversion", ["0.3", "1.0"])
+def test_to_structured_extensions(specversion):
+    event = CloudEvent(test_attributes, test_data)
+    headers, body = to_structured_http(event)
+
+    body = json.loads(body)
+
+    assert "ext1" in body
+    assert "extensions" not in body
+
+
+@pytest.mark.parametrize("specversion", ["0.3", "1.0"])
+def test_from_structured_extensions(specversion):
+    headers = {"Content-Type": "application/cloudevents+json"}
+    body = {
+        "id": "1234",
+        "source": "<my url>",
+        "type": "sample",
+        "specversion": specversion,
+        "ext1": "test1",
+        "ext2": "test2",
+    }
+
+    data = json.dumps(body)
+    event = from_http(data, headers)
+
+    assert body["ext1"] == event["ext1"]
+    assert body["ext2"] == event["ext2"]
diff --git a/requirements/test.txt b/requirements/test.txt
index 4c9fb75..1d5f308 100644
--- a/requirements/test.txt
+++ b/requirements/test.txt
@@ -9,3 +9,5 @@ pytest-cov==2.4.0
 sanic
 aiohttp
 Pillow
+requests
+