Initial release: CloudEvents Python SDK 0.0.1a0

Signed-off-by: Denis Makogon <lildee1991@gmail.com>
This commit is contained in:
Denis Makogon 2018-11-16 00:49:16 +02:00
parent 159332b754
commit 91224583c4
23 changed files with 830 additions and 0 deletions

125
.gitignore vendored Normal file
View File

@ -0,0 +1,125 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
env/
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
.hypothesis/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# Jupyter Notebook
.ipynb_checkpoints
# pyenv
.python-version
# celery beat schedule file
celerybeat-schedule
# SageMath parsed files
*.sage.py
# dotenv
.env
# virtualenv
.venv
venv/
ENV/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
*.pyc
.testrepository
.tox/*
dist/*
build/*
html/*
*.egg*
cover/*
.coverage
rdserver.txt
python-troveclient.iml
# Files created by releasenotes build
releasenotes/build
.coverage.*
*.json
.cache
*.log*
*.csv
venv
.venv
ChangeLog
AUTHORS
.pytest_cache/

0
cloudevents/__init__.py Normal file
View File

View File

View File

@ -0,0 +1,19 @@
# 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.
from cloudevents.sdk.converters import binary
from cloudevents.sdk.converters import structured
TypeBinary = binary.BinaryHTTPCloudEventConverter.TYPE
TypeStructured = structured.JSONHTTPCloudEventConverter.TYPE

View File

@ -0,0 +1,41 @@
# 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 typing
from cloudevents.sdk.event import base
class Converter(object):
TYPE = None
def __init__(self,
event_class: base.BaseEvent,
supported_media_types: typing.Mapping[str, bool]):
self.event = event_class()
self.supported_media_types = supported_media_types
def can_read(self, media_type: str) -> bool:
return media_type in self.supported_media_types
def can_write(self, media_type: str) -> bool:
return media_type in self.supported_media_types
def read(self, headers: dict, body: typing.IO) -> base.BaseEvent:
raise Exception("not implemented")
def write(self, event: base.BaseEvent,
data_marshaller: typing.Callable) -> (dict, typing.IO):
raise Exception("not implemented")

View File

@ -0,0 +1,54 @@
# 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 typing
from cloudevents.sdk import exceptions
from cloudevents.sdk.converters import base
from cloudevents.sdk.event import base as event_base
from cloudevents.sdk.event import v01
class BinaryHTTPCloudEventConverter(base.Converter):
TYPE = "binary"
def __init__(self, event_class: event_base.BaseEvent,
supported_media_types: typing.Mapping[str, bool]):
if event_class == v01.Event:
raise exceptions.UnsupportedEvent(event_class)
super().__init__(event_class, supported_media_types)
def read(self,
headers: dict, body: typing.IO) -> event_base.BaseEvent:
# we ignore headers, since the whole CE is in request body
event = self.event
event.UnmarshalBinary(headers, body)
return event
def write(self, event: event_base.BaseEvent,
data_marshaller: typing.Callable) -> (dict, typing.IO):
hs, data = event.MarshalBinary()
return hs, data_marshaller(data)
def NewBinaryHTTPCloudEventConverter(
event_class: event_base.BaseEvent) -> BinaryHTTPCloudEventConverter:
media_types = {
"application/json": True,
"application/xml": True,
"application/octet-stream": True,
}
return BinaryHTTPCloudEventConverter(event_class, media_types)

View File

@ -0,0 +1,48 @@
# 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 typing
from cloudevents.sdk.converters import base
from cloudevents.sdk.event import base as event_base
class JSONHTTPCloudEventConverter(base.Converter):
TYPE = "structured"
def __init__(self, event_class: event_base.BaseEvent,
supported_media_types: typing.Mapping[str, bool]):
super().__init__(event_class, supported_media_types)
def read(self, headers: dict,
body: typing.IO) -> event_base.BaseEvent:
# we ignore headers, since the whole CE is in request body
event = self.event
event.UnmarshalJSON(body)
return event
def write(self,
event: event_base.BaseEvent,
data_marshaller: typing.Callable) -> (dict, typing.IO):
return {}, event.MarshalJSON()
def NewJSONHTTPCloudEventConverter(
event_class: event_base.BaseEvent) -> JSONHTTPCloudEventConverter:
media_types = {
"application/cloudevents+json": True,
}
return JSONHTTPCloudEventConverter(event_class, media_types)

View File

View File

@ -0,0 +1,90 @@
# 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 io
import ujson
import typing
class BaseEvent(object):
def Properties(self) -> dict:
props = dict()
for name, value in self.__dict__.items():
if str(name).startswith("ce__"):
props.update(
{
str(name).replace("ce__", ""): value.get()
}
)
return props
def Extensions(self) -> dict:
props = self.Properties()
return props.get("extensions")
def Get(self, key: str) -> (object, bool):
formatted_key = "ce__{0}".format(key.lower())
ok = hasattr(self, formatted_key)
value = getattr(self, formatted_key, None)
if not ok:
exts = self.Extensions()
return exts.get(key), key in exts
return value.get(), ok
def Set(self, key: str, value: object):
formatted_key = "ce__{0}".format(key)
key_exists = hasattr(self, formatted_key)
if key_exists:
attr = getattr(self, formatted_key)
attr.set(value)
setattr(self, formatted_key, attr)
return
exts = self.Extensions()
exts.update({key: value})
self.Set("extensions", exts)
def MarshalJSON(self) -> typing.IO:
return io.StringIO(ujson.dumps(self.Properties()))
def UnmarshalJSON(self, b: typing.IO):
raw_ce = ujson.load(b)
for name, value in raw_ce.items():
self.Set(name, value)
def UnmarshalBinary(self, headers: dict, body: typing.IO):
props = self.Properties()
for key in props:
self.Set(key, headers.get("ce-{0}".format(key)))
data = None
if body:
data = body.read()
self.Set("data", data)
def MarshalBinary(self) -> (dict, object):
headers = {}
props = self.Properties()
for key, value in props.items():
if key not in ["data", "extensions"]:
headers["ce-{0}".format(key)] = value
exts = props.get("extensions")
headers.update(**exts)
data, _ = self.Get("data")
return headers, data

View File

@ -0,0 +1,36 @@
# 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.
class Option(object):
def __init__(self, name, value, is_required):
self.name = name
self.value = value
self.is_required = is_required
def set(self, new_value):
is_none = new_value is None
if self.is_required and is_none:
raise ValueError(
"Attribute value error: '{0}', "
"invalid new value.".format(self.name))
self.value = new_value
def get(self):
return self.value
def required(self):
return self.is_required

View File

@ -0,0 +1,33 @@
# 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.
from cloudevents.sdk.event import opt
from cloudevents.sdk.event import base
class Event(base.BaseEvent):
def __init__(self):
self.ce__specversion = opt.Option("specversion", "0.1", True)
self.ce__type = opt.Option("type", None, True)
self.ce__source = opt.Option("source", None, True)
self.ce__id = opt.Option("id", None, True)
self.ce__time = opt.Option("time", None, True)
self.ce__schemaurl = opt.Option("schemaurl", None, False)
self.ce__contenttype = opt.Option("contenttype", None, False)
self.ce__data = opt.Option("data", None, False)
self.ce__extensions = opt.Option("extensions", dict(), False)
def CloudEventVersion(self) -> str:
return self.ce__specversion.get()

View File

@ -0,0 +1,36 @@
# 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.
from cloudevents.sdk.event import opt
from cloudevents.sdk.event import base
class Event(base.BaseEvent):
def __init__(self):
self.ce__cloudEventsVersion = opt.Option(
"cloudEventsVersion", "0.1", True)
self.ce__eventType = opt.Option("eventType", None, True)
self.ce__eventTypeVersion = opt.Option(
"eventTypeVersion", None, False)
self.ce__source = opt.Option("source", None, True)
self.ce__eventID = opt.Option("eventID", None, True)
self.ce__evenTime = opt.Option("eventTime", None, True)
self.ce__schemaURL = opt.Option("schemaURL", None, False)
self.ce__contentType = opt.Option("contentType", None, False)
self.ce__data = opt.Option("data", None, False)
self.ce__extensions = opt.Option("extensions", dict(), False)
def CloudEventVersion(self) -> str:
return self.ce__cloudEventsVersion.get()

View File

@ -0,0 +1,27 @@
# 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.
class InvalidMimeType(Exception):
def __init__(self, mime_type):
super().__init__(
"Invalid MIME type: {0}".format(mime_type))
class UnsupportedEvent(Exception):
def __init__(self, event_class):
super().__init__("Invalid CloudEvent class: "
"'{0}'".format(event_class))

View File

@ -0,0 +1,57 @@
# 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 typing
from cloudevents.sdk import exceptions
from cloudevents.sdk.converters import base
from cloudevents.sdk.converters import binary
from cloudevents.sdk.converters import structured
from cloudevents.sdk.event import base as event_base
class HTTPMarshaller(object):
def __init__(self, converters: typing.List[base.Converter]):
self.__converters = converters
def FromRequest(self, headers: dict, body: typing.IO):
mimeType = headers.get("Content-Type")
for cnvrtr in self.__converters:
if cnvrtr.can_read(mimeType):
return cnvrtr.read(headers, body)
raise exceptions.InvalidMimeType(mimeType)
def ToRequest(self, event: event_base.BaseEvent,
converter_type: str,
data_marshaller: typing.Callable) -> (dict, typing.IO):
for cnvrtv in self.__converters:
if converter_type == cnvrtv.TYPE:
return cnvrtv.write(event, data_marshaller)
def NewDefaultHTTPMarshaller(
event_class: event_base.BaseEvent) -> HTTPMarshaller:
return HTTPMarshaller([
structured.NewJSONHTTPCloudEventConverter(event_class),
binary.NewBinaryHTTPCloudEventConverter(event_class),
])
def NewHTTPMarshaller(
converters: typing.List[base.Converter]) -> HTTPMarshaller:
return HTTPMarshaller(converters)

View File

View File

@ -0,0 +1,102 @@
# 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 pytest
import io
import ujson
from cloudevents.sdk import exceptions
from cloudevents.sdk import marshaller
from cloudevents.sdk.event import v01
from cloudevents.sdk.event import upstream
from cloudevents.sdk.converters import binary
from cloudevents.sdk.converters import structured
def test_binary_converter_upstream():
headers = {
"ce-specversion": "0.1",
"ce-type": "word.found.exclamation",
"ce-id": "16fb5f0b-211e-1102-3dfe-ea6e2806f124",
"ce-time": "2018-10-23T12:28:23.3464579Z",
"ce-source": "pytest",
"Content-Type": "application/json"
}
m = marshaller.NewHTTPMarshaller(
[
binary.NewBinaryHTTPCloudEventConverter(upstream.Event)
]
)
event = m.FromRequest(headers, None)
assert event is not None
assert event.Get("type") == (headers.get("ce-type"), True)
assert event.Get("id") == (headers.get("ce-id"), True)
def test_structured_converter_upstream():
ce = {
"specversion": "0.1",
"type": "word.found.exclamation",
"id": "16fb5f0b-211e-1102-3dfe-ea6e2806f124",
"time": "2018-10-23T12:28:23.3464579Z",
"source": "pytest",
"contenttype": "application/json"
}
m = marshaller.NewHTTPMarshaller(
[
structured.NewJSONHTTPCloudEventConverter(upstream.Event)
]
)
event = m.FromRequest(
{"Content-Type": "application/cloudevents+json"},
io.StringIO(ujson.dumps(ce))
)
assert event is not None
assert event.Get("type") == (ce.get("type"), True)
assert event.Get("id") == (ce.get("id"), True)
# todo: clarify whether spec 0.1 doesn't support binary format
def test_binary_converter_v01():
pytest.raises(
exceptions.UnsupportedEvent,
binary.NewBinaryHTTPCloudEventConverter,
v01.Event)
def test_structured_converter_v01():
ce = {
"specversion": "0.1",
"type": "word.found.exclamation",
"id": "16fb5f0b-211e-1102-3dfe-ea6e2806f124",
"time": "2018-10-23T12:28:23.3464579Z",
"source": "pytest",
"contenttype": "application/json"
}
m = marshaller.NewHTTPMarshaller(
[
structured.NewJSONHTTPCloudEventConverter(v01.Event)
]
)
event = m.FromRequest(
{"Content-Type": "application/cloudevents+json"},
io.StringIO(ujson.dumps(ce))
)
assert event is not None
assert event.Get("type") == (ce.get("type"), True)
assert event.Get("id") == (ce.get("id"), True)

8
release.sh Normal file
View File

@ -0,0 +1,8 @@
#!/usr/bin/env bash
git checkout -b v${CLOUDEVENTS_SDK_VERSION}-stable
git push origin v${CLOUDEVENTS_SDK_VERSION}-stable
PBR_VERSION=${CLOUDEVENTS_SDK_VERSION} python setup.py sdist bdist_wheel
twine upload dist/cloudevents-${CLOUDEVENTS_SDK_VERSIONN}*
git checkout master

69
release_doc.md Normal file
View File

@ -0,0 +1,69 @@
Release process
===============
Run tests on target brunch
--------------------------
Steps:
tox
Cut off stable branch
---------------------
Steps:
git checkout -b vX.X.X-stable
git push origin vX.X.X-stable
Create GitHub tag
-----------------
Steps:
Releases ---> Draft New Release
Name: CloudEvents Python SDK version X.X.X stable release
Collect changes from previous version
-------------------------------------
Steps:
git log --oneline --decorate
Build distribution package
--------------------------
Steps:
PBR_VERSION=X.X.X python setup.py sdist bdist_wheel
Check install capability for the wheel
--------------------------------------
Steps:
python3.7 -m venv .test_venv
source .test_venv/bin/activate
pip install dist/cloudevents-X.X.X-py3-none-any.whl
Submit release to PYPI
----------------------
Steps:
twine upload dist/cloudevents-X.X.X-py3-none-any.whl
Verify install capability for the wheel
---------------------------------------
Steps:
python3.7 -m venv .test_venv
source .new_venv/bin/activate
pip install cloudevents --upgrade

2
requirement.txt Normal file
View File

@ -0,0 +1,2 @@
pbr!=2.1.0,>=2.0.0 # Apache-2.0
ujson

24
setup.cfg Normal file
View File

@ -0,0 +1,24 @@
[metadata]
name = cloudevents
summary = CloudEvents SDK Python
description-file =
README.md
author = Denis Makogon
author-email = denys.makogon@oracle.com
home-page = https://cloudevents.io/
classifier =
Intended Audience :: Information Technology
Intended Audience :: System Administrators
License :: OSI Approved :: Apache Software License
Operating System :: POSIX :: Linux
Programming Language :: Python :: 3
Programming Language :: Python :: 3.6
Programming Language :: Python :: 3.7
[files]
packages =
cloudevents
[global]
setup-hooks =
pbr.hooks.setup_hook

20
setup.py Normal file
View File

@ -0,0 +1,20 @@
# 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 setuptools
setuptools.setup(
setup_requires=['pbr>=2.0.0'],
pbr=True)

4
test-requirements.txt Normal file
View File

@ -0,0 +1,4 @@
flake8==2.5.0
hacking<0.11,>=0.10.0
pytest==4.0.0
pytest-cov==2.4.0

35
tox.ini Normal file
View File

@ -0,0 +1,35 @@
[tox]
envlist = py{3.6,3.7},pep8
skipsdist = True
[testenv]
basepython =
pep8: python3
py3.6: python3.6
py3.7: python3.7
setenv = VIRTUAL_ENV={envdir}
usedevelop = True
install_command = pip install -U {opts} {packages}
deps = -r{toxinidir}/test-requirements.txt
commands = find . -type f -name "*.pyc" -delete
whitelist_externals = find
rm
go
docker
[testenv:pep8]
commands = flake8
[testenv:venv]
commands = {posargs}
[testenv:py3.6]
commands = pytest -v -s --tb=long --cov=cloudevents {toxinidir}/cloudevents/tests
[testenv:py3.7]
commands = pytest -v -s --tb=long --cov=cloudevents {toxinidir}/cloudevents/tests
[flake8]
ignore = H405,H404,H403,H401,H306
show-source = True
exclude=.venv,.git,.tox,dist,doc,*lib/python*,*egg,build,docs,venv,.venv