diff --git a/.gitignore b/.gitignore index 3c3629e..87523d0 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,8 @@ +# Node.js node_modules + +# Python +*.py[cod] +*.egg-info/ +.eggs/ +.tox/ diff --git a/python/Pipfile b/python/Pipfile new file mode 100644 index 0000000..7c1f24c --- /dev/null +++ b/python/Pipfile @@ -0,0 +1,15 @@ +[[source]] +name = "pypi" +url = "https://pypi.org/simple" +verify_ssl = true + +[dev-packages] +tox = "*" +pytest = "*" +responses = "*" + +[packages] +requests = "*" + +[requires] +python_version = "3.7" diff --git a/python/Pipfile.lock b/python/Pipfile.lock new file mode 100644 index 0000000..ed60fcd --- /dev/null +++ b/python/Pipfile.lock @@ -0,0 +1,217 @@ +{ + "_meta": { + "hash": { + "sha256": "19b3f45b17e953d1629ea67609b87d0372a5ab2352e7d284192679f107f7e464" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.7" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "certifi": { + "hashes": [ + "sha256:e4f3620cfea4f83eedc95b24abd9cd56f3c4b146dd0177e83a21b4eb49e21e50", + "sha256:fd7c7c74727ddcf00e9acd26bba8da604ffec95bf1c2144e67aff7a8b50e6cef" + ], + "version": "==2019.9.11" + }, + "chardet": { + "hashes": [ + "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", + "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + ], + "version": "==3.0.4" + }, + "idna": { + "hashes": [ + "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", + "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" + ], + "version": "==2.8" + }, + "requests": { + "hashes": [ + "sha256:11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4", + "sha256:9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31" + ], + "index": "pypi", + "version": "==2.22.0" + }, + "urllib3": { + "hashes": [ + "sha256:3de946ffbed6e6746608990594d08faac602528ac7015ac28d33cee6a45b7398", + "sha256:9a107b99a5393caf59c7aa3c1249c16e6879447533d0887f4336dde834c7be86" + ], + "version": "==1.25.6" + } + }, + "develop": { + "atomicwrites": { + "hashes": [ + "sha256:03472c30eb2c5d1ba9227e4c2ca66ab8287fbfbbda3888aa93dc2e28fc6811b4", + "sha256:75a9445bac02d8d058d5e1fe689654ba5a6556a1dfd8ce6ec55a0ed79866cfa6" + ], + "version": "==1.3.0" + }, + "attrs": { + "hashes": [ + "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c", + "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72" + ], + "version": "==19.3.0" + }, + "certifi": { + "hashes": [ + "sha256:e4f3620cfea4f83eedc95b24abd9cd56f3c4b146dd0177e83a21b4eb49e21e50", + "sha256:fd7c7c74727ddcf00e9acd26bba8da604ffec95bf1c2144e67aff7a8b50e6cef" + ], + "version": "==2019.9.11" + }, + "chardet": { + "hashes": [ + "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", + "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + ], + "version": "==3.0.4" + }, + "filelock": { + "hashes": [ + "sha256:18d82244ee114f543149c66a6e0c14e9c4f8a1044b5cdaadd0f82159d6a6ff59", + "sha256:929b7d63ec5b7d6b71b0fa5ac14e030b3f70b75747cef1b10da9b879fef15836" + ], + "version": "==3.0.12" + }, + "idna": { + "hashes": [ + "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", + "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" + ], + "version": "==2.8" + }, + "importlib-metadata": { + "hashes": [ + "sha256:aa18d7378b00b40847790e7c27e11673d7fed219354109d0e7b9e5b25dc3ad26", + "sha256:d5f18a79777f3aa179c145737780282e27b508fc8fd688cb17c7a813e8bd39af" + ], + "markers": "python_version < '3.8'", + "version": "==0.23" + }, + "more-itertools": { + "hashes": [ + "sha256:409cd48d4db7052af495b09dec721011634af3753ae1ef92d2b32f73a745f832", + "sha256:92b8c4b06dac4f0611c0729b2f2ede52b2e1bac1ab48f089c7ddc12e26bb60c4" + ], + "version": "==7.2.0" + }, + "packaging": { + "hashes": [ + "sha256:28b924174df7a2fa32c1953825ff29c61e2f5e082343165438812f00d3a7fc47", + "sha256:d9551545c6d761f3def1677baf08ab2a3ca17c56879e70fecba2fc4dde4ed108" + ], + "version": "==19.2" + }, + "pluggy": { + "hashes": [ + "sha256:0db4b7601aae1d35b4a033282da476845aa19185c1e6964b25cf324b5e4ec3e6", + "sha256:fa5fa1622fa6dd5c030e9cad086fa19ef6a0cf6d7a2d12318e10cb49d6d68f34" + ], + "version": "==0.13.0" + }, + "py": { + "hashes": [ + "sha256:64f65755aee5b381cea27766a3a147c3f15b9b6b9ac88676de66ba2ae36793fa", + "sha256:dc639b046a6e2cff5bbe40194ad65936d6ba360b52b3c3fe1d08a82dd50b5e53" + ], + "version": "==1.8.0" + }, + "pyparsing": { + "hashes": [ + "sha256:6f98a7b9397e206d78cc01df10131398f1c8b8510a2f4d97d9abd82e1aacdd80", + "sha256:d9338df12903bbf5d65a0e4e87c2161968b10d2e489652bb47001d82a9b028b4" + ], + "version": "==2.4.2" + }, + "pytest": { + "hashes": [ + "sha256:7e4800063ccfc306a53c461442526c5571e1462f61583506ce97e4da6a1d88c8", + "sha256:ca563435f4941d0cb34767301c27bc65c510cb82e90b9ecf9cb52dc2c63caaa0" + ], + "index": "pypi", + "version": "==5.2.1" + }, + "requests": { + "hashes": [ + "sha256:11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4", + "sha256:9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31" + ], + "index": "pypi", + "version": "==2.22.0" + }, + "responses": { + "hashes": [ + "sha256:502d9c0c8008439cfcdef7e251f507fcfdd503b56e8c0c87c3c3e3393953f790", + "sha256:97193c0183d63fba8cd3a041c75464e4b09ea0aff6328800d1546598567dde0b" + ], + "index": "pypi", + "version": "==0.10.6" + }, + "six": { + "hashes": [ + "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", + "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" + ], + "version": "==1.12.0" + }, + "toml": { + "hashes": [ + "sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c", + "sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e" + ], + "version": "==0.10.0" + }, + "tox": { + "hashes": [ + "sha256:0bc216b6a2e6afe764476b4a07edf2c1dab99ed82bb146a1130b2e828f5bff5e", + "sha256:c4f6b319c20ba4913dbfe71ebfd14ff95d1853c4231493608182f66e566ecfe1" + ], + "index": "pypi", + "version": "==3.14.0" + }, + "urllib3": { + "hashes": [ + "sha256:3de946ffbed6e6746608990594d08faac602528ac7015ac28d33cee6a45b7398", + "sha256:9a107b99a5393caf59c7aa3c1249c16e6879447533d0887f4336dde834c7be86" + ], + "version": "==1.25.6" + }, + "virtualenv": { + "hashes": [ + "sha256:3e3597e89c73df9313f5566e8fc582bd7037938d15b05329c232ec57a11a7ad5", + "sha256:5d370508bf32e522d79096e8cbea3499d47e624ac7e11e9089f9397a0b3318df" + ], + "version": "==16.7.6" + }, + "wcwidth": { + "hashes": [ + "sha256:3df37372226d6e63e1b1e1eda15c594bca98a22d33a23832a90998faa96bc65e", + "sha256:f4ebe71925af7b40a864553f761ed559b43544f8f71746c2d756c7fe788ade7c" + ], + "version": "==0.1.7" + }, + "zipp": { + "hashes": [ + "sha256:3718b1cbcd963c7d4c5511a8240812904164b7f381b647143a89d3b98f9bcd8e", + "sha256:f06903e9f1f43b12d371004b4ac7b06ab39a44adc747266928ae6debfa7b3335" + ], + "version": "==0.6.0" + } + } +} diff --git a/python/README.md b/python/README.md index c3754ae..acd4d72 100644 --- a/python/README.md +++ b/python/README.md @@ -1,13 +1,14 @@ # sdk:python +## Usage + ```python -from pipedream import sdk as pd +from pipedream_sdk import send_event -pd.send_event(API_KEY, {'hello': 'world'}) +send_event({'hello': 'world'}) # with exports -pd.send_event( - API_KEY, +send_event( {'hello': 'world'}, exports={ 'event': {'hello': 'world!'}, @@ -16,9 +17,23 @@ pd.send_event( ) ``` -If you do not provide an `event` export it will be set to the `raw_event` -(first argument). `event` and `raw_event` MUST be a dict or the SDK request -will be invalid. +Importing `send_event` directly will create an instance of `PipedreamSdkClient` +behind the scenes, configured through environment variables. You may also create +the client explicitly: + +```python +from pipedream_sdk import PipedreamSdkClient + +pd = PipedreamSdkClient( + api_key=YOUR_API_KEY, + sdk_protocol='https', +) +pd.send_event({'hello': 'world'}) +``` + +This may be useful in cases where multiple events need to be pushed to Pipedream +using differing API keys, or where some events should be sent as over HTTPS while +others should be sent without SSL. ## Development diff --git a/python/pipedream_sdk/__init__.py b/python/pipedream_sdk/__init__.py new file mode 100644 index 0000000..0fb66c4 --- /dev/null +++ b/python/pipedream_sdk/__init__.py @@ -0,0 +1,16 @@ +from .client import PipedreamSdkClient + + +_CLIENT = None + + +def _get_client(): + global _CLIENT + + if _CLIENT is None: + _CLIENT = PipedreamSdkClient() + return _CLIENT + + +def send_event(*args, **kwargs): + _get_client().send_event(*args, **Jkwargs) diff --git a/python/pipedream_sdk/about.py b/python/pipedream_sdk/about.py new file mode 100644 index 0000000..93557a5 --- /dev/null +++ b/python/pipedream_sdk/about.py @@ -0,0 +1 @@ +version = '0.0.1dev0' diff --git a/python/pipedream_sdk/client.py b/python/pipedream_sdk/client.py new file mode 100644 index 0000000..af20ed2 --- /dev/null +++ b/python/pipedream_sdk/client.py @@ -0,0 +1,63 @@ +import hashlib +import json +import os + +import requests + +from .about import version + + +class PipedreamSdkClient: + def __init__(self, api_key=None, api_secret=None, sdk_host=None, sdk_protocol=None): + self._sdk_version = '0.3.0' + self._api_key = api_key or os.getenv('PD_SDK_API_KEY') + self._api_secret = api_secret or os.getenv('PD_SDK_SECRET_KEY') + self._sdk_host = ( + sdk_host + or os.getenv('PD_SDK_HOST') + or 'sdk.m.pipedream.net' + ) + self._sdk_protocol = ( + sdk_protocol + or os.getenv('PD_SDK_PROTO') + or 'https' + ) + + def send_event(self, raw_event, exports=None, deployment=None): + """Send an event to the PipedreamHQ SDK + + :return: None + """ + # Manually create the payload so a signature can be generated + event = {'raw_event': raw_event} + data = json.dumps(event) + headers = { + 'content-type': 'application/json', + 'user-agent': f'pipedream-sdk:python/{version}', + 'x-pd-sdk-version': self._sdk_version, + } + + if self._api_secret: + headers['x-pd-sig'] = self._sign(data) + # TODO: warn if no secret is present + + #TODO: raise exception if POST fails + response = requests.post(self._request_uri(deployment), data=data, headers=headers) + import pdb; pdb.set_trace() + + def _request_uri(self, deployment): + uri_parts = [ + f'{self._sdk_protocol}://{self._sdk_host}', + 'pipelines', + self._api_key, + ] + if deployment: + uri_parts += ['deployments', deployment] + uri_parts.append('events') + return '/'.join(uri_parts) + + def _sign(self, data): + sig = hashlib.sha256(self._api_secret.encode('utf-8')) + sig.update(data.encode('utf-8')) + return sig.hexdigest() + diff --git a/python/pipedream_sdk/config.py b/python/pipedream_sdk/config.py new file mode 100644 index 0000000..4afc02e --- /dev/null +++ b/python/pipedream_sdk/config.py @@ -0,0 +1,8 @@ +import os + +client_version = '0.0.1dev0' + +sdk_version = '0.3.0' +hostname = os.getenv('PD_SDK_HOST') or 'sdk.m.pipedream.net' +protocol = os.getenv('PD_SDK_PROTO') or 'https' +user_agent = f'pipedream-sdk:python/{client_version}' diff --git a/python/setup.py b/python/setup.py new file mode 100644 index 0000000..4ffbc35 --- /dev/null +++ b/python/setup.py @@ -0,0 +1,34 @@ +import sys + +from setuptools import setup +from setuptools.command.test import test as TestCommand + + +class PyTest(TestCommand): + def run_tests(self): + import pytest + + errno = pytest.main() + sys.exit(errno) + + +setup( + name='Pipedream SDK', + version='0.0.1dev0', + description='The Pipedream SDK', + url='https://github.com/PipedreamHQ/sdk', + classifiers=( + 'Programming Language :: Python', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: Implementation :: CPython', + 'Programming Language :: Python :: Implementation :: PyPy', + ), + install_requires=( + 'requests>=2.22', + ), + tests_require=( + 'pytest>=5.2' + ), + cmdclass={'test': PyTest}, +) diff --git a/python/test/test_client.py b/python/test/test_client.py new file mode 100644 index 0000000..5119fbf --- /dev/null +++ b/python/test/test_client.py @@ -0,0 +1,36 @@ +import pytest + +from pipedream_sdk import PipedreamSdkClient as Client + + +class TestClient: + @pytest.fixture + def env_vars(self, monkeypatch): + monkeypatch.setenv('PD_SDK_HOST', 'example.com') + monkeypatch.setenv('PD_SDK_PROTO', 'telnet') + monkeypatch.setenv('PD_SDK_API_KEY', 'not-a-real-key') + + def test_config(self): + client = Client() + + assert client._api_key is None + assert client._sdk_host == 'sdk.m.pipedream.net' + assert client._sdk_protocol == 'https' + + def test_config_from_env_vars(self, env_vars): + client = Client() + + assert client._api_key == 'not-a-real-key' + assert client._sdk_host == 'example.com' + assert client._sdk_protocol == 'telnet' + + def test_config_from_params(self, env_vars): + client = Client( + api_key='explicit-key', + sdk_host='explicit.example.com', + sdk_protocol='gopher', + ) + + assert client._api_key == 'explicit-key' + assert client._sdk_host == 'explicit.example.com' + assert client._sdk_protocol == 'gopher' diff --git a/python/test/test_init.py b/python/test/test_init.py new file mode 100644 index 0000000..c1a8084 --- /dev/null +++ b/python/test/test_init.py @@ -0,0 +1,11 @@ +import pipedream_sdk as sdk + + +# TODO: This should really only test that an instance of the client was created +def test_client_config(): + sdk._CLIENT = None + client = sdk._get_client() + + assert client._api_key is None + assert client._sdk_host == 'sdk.m.pipedream.net' + assert client._sdk_protocol == 'https' diff --git a/python/tox.ini b/python/tox.ini new file mode 100644 index 0000000..ba15f1e --- /dev/null +++ b/python/tox.ini @@ -0,0 +1,6 @@ +[tox] +envlist = py37 + +[testenv] +commands = + python setup.py test