def setUp(self): self.cache = MockRedisCacheAdapter() self.channel = 'channel1' self.stream = FilteredSseStream(self.channel, self.cache)
class TestSimpleFilteredSseStream(TestCase): @classmethod def setUpClass(cls): patch_all() def setUp(self): self.cache = MockRedisCacheAdapter() self.channel = 'channel1' self.stream = FilteredSseStream(self.channel, self.cache) def tearDown(self): self.cache.clear() def test_init(self): self.assertEqual(self.stream.channel, self.channel) self.assertEqual(self.stream.cache, self.cache) def test_create_channel_name(self): self.assertEqual(self.stream.create_subchannel_name('a'), '{}.a'.format(self.channel)) self.assertEqual(self.stream.create_subchannel_name(14), '{}.14'.format(self.channel)) def assert_header_in_response(self, response, header, value): header_tuple = next( (header_ for header_ in response.headers if header_[0] == header), None) self.assertIsNotNone(header_tuple) self.assertEqual(header_tuple[1], value) def test_stream_default_headers(self): resp = self.stream.stream(subchannel='a') self.assert_header_in_response(resp, 'Connection', 'keep-alive') self.assert_header_in_response(resp, 'Cache-Control', 'no-cache') self.assert_header_in_response(resp, 'Content-Type', 'text/event-stream; charset=utf-8') def test_stream_custom_headers(self): resp = self.stream.stream(subchannel='a', headers={ 'x-custom': 'yes', 'Cache-Control': 'no-store' }) self.assert_header_in_response(resp, 'Connection', 'keep-alive') self.assert_header_in_response(resp, 'Cache-Control', 'no-store') self.assert_header_in_response(resp, 'Content-Type', 'text/event-stream; charset=utf-8') self.assert_header_in_response(resp, 'x-custom', 'yes') def test_send(self): @self.stream.push('event1') def pusher(a, ev, sub): gevent.sleep(0.1) return {'a': a}, sub, ev subs = ('aaa', 'bbb') result = {sub: [] for sub in subs} def listen(sub): for event in self.stream.send(subchannel=sub): result[sub].append(event) base_args = [('event1', 1), ('event2', 2)] args = { sub: [(event, data + i) for (event, data) in base_args] for i, sub in enumerate(subs) } def publish(sub): for event, data in args[sub]: pusher(data, event, sub) self.stream.unsubscribe(sub) sses = { sub: [SseEvent(event, {'a': arg}) for event, arg in args[sub]] for sub in subs } formatted_sses = { sub: [sse.format(i + 1) for i, sse in enumerate(sse_vals)] for sub, sse_vals in sses.items() } listen_threads = [gevent.spawn(listen, sub) for sub in subs] publish_threads = [gevent.spawn(publish, sub) for sub in subs] gevent.sleep(0.1) gevent.joinall(listen_threads, timeout=2) gevent.joinall(publish_threads, timeout=2) for sub in subs: self.assertListEqual(result[sub], formatted_sses[sub]) def test_send_publish_multiple(self): subs = ('a', 'bbb') @self.stream.push('event1') def pusher(a, ev): gevent.sleep(0.1) return {'a': a}, subs, ev result = {sub: [] for sub in subs} def listen(sub): for event in self.stream.send(subchannel=sub): result[sub].append(event) base_args = [('event1', 1), ('event2', 2)] def publish(): for event, data in base_args: pusher(data, event) for sub in subs: self.stream.unsubscribe(sub) sses = { sub: [SseEvent(event, {'a': arg}) for event, arg in base_args] for sub in subs } formatted_sses = { sub: [sse.format(i + 1) for i, sse in enumerate(sse_vals)] for sub, sse_vals in sses.items() } listen_threads = [gevent.spawn(listen, sub) for sub in subs] publish_thread = gevent.spawn(publish) gevent.sleep(0.1) gevent.joinall(listen_threads, timeout=2) publish_thread.join(timeout=2) for sub in subs: self.assertListEqual(result[sub], formatted_sses[sub]) def test_send_with_retry(self): @self.stream.push('event1') def pusher(a, ev, sub): gevent.sleep(0.1) return {'a': a}, sub, ev subs = ('a', 'b') result = {'a': [], 'b': []} def listen(sub): for event in self.stream.send(subchannel=sub, retry=50): result[sub].append(event) base_args = [('event1', 1), ('event2', 2)] args = { sub: [(event, data + i) for (event, data) in base_args] for i, sub in enumerate(subs) } def publish(sub): for event, data in args[sub]: pusher(data, event, sub) self.stream.unsubscribe(sub) sses = { sub: [SseEvent(event, {'a': arg}) for event, arg in args[sub]] for sub in subs } formatted_sses = { sub: [sse.format(i + 1, retry=50) for i, sse in enumerate(sse_vals)] for sub, sse_vals in sses.items() } listen_threads = [gevent.spawn(listen, sub) for sub in subs] publish_threads = [gevent.spawn(publish, sub) for sub in subs] gevent.sleep(0.1) gevent.joinall(listen_threads, timeout=2) gevent.joinall(publish_threads, timeout=2) for sub in subs: self.assertListEqual(result[sub], formatted_sses[sub])
from datetime import datetime from enum import Enum, unique from flask_jwt_extended import get_jwt_identity from walkoff.messaging import MessageActionEvent from walkoff.security import jwt_required_in_query from walkoff.sse import FilteredSseStream, StreamableBlueprint sse_stream = FilteredSseStream('notifications') notifications_page = StreamableBlueprint('notifications_page', __name__, streams=[sse_stream]) @unique class NotificationSseEvent(Enum): created = 1 read = 2 responded = 3 def format_read_responded_data(message, user): return { 'id': message.id, 'username': user.username, 'timestamp': datetime.utcnow().isoformat() }
from datetime import datetime from uuid import UUID from enum import Enum, unique from flask import current_app, request from walkoff.events import WalkoffEvent from walkoff.executiondb import ActionStatusEnum, WorkflowStatusEnum from walkoff.executiondb.workflowresults import WorkflowStatus from walkoff.helpers import convert_action_argument, utc_as_rfc_datetime from walkoff.security import jwt_required_in_query from walkoff.server.problem import Problem from walkoff.server.returncodes import BAD_REQUEST from walkoff.sse import FilteredSseStream, StreamableBlueprint workflow_stream = FilteredSseStream('workflow_results') action_stream = FilteredSseStream('action_results') action_summary_stream = FilteredSseStream('action_results_summary') workflowresults_page = StreamableBlueprint( 'workflowresults_page', __name__, streams=(workflow_stream, action_stream, action_summary_stream) ) action_summary_keys = ('action_name', 'app_name', 'action_id', 'name', 'timestamp', 'workflow_execution_id') @unique class ActionStreamEvent(Enum): started = 1
import logging from uuid import UUID from flask import request from walkoff.events import WalkoffEvent from walkoff.security import jwt_required_in_query from walkoff.sse import FilteredSseStream, StreamableBlueprint from walkoff.server.problem import Problem from walkoff.server.returncodes import BAD_REQUEST console_stream = FilteredSseStream('console_results') console_page = StreamableBlueprint('console_page', __name__, streams=(console_stream,)) def format_console_data(sender, data): try: level = int(data['level']) except ValueError: level = data['level'] return { 'workflow': sender['name'], 'app_name': data['app_name'], 'action_name': data['action_name'], 'level': logging.getLevelName(level), 'message': data['message'] } @WalkoffEvent.ConsoleLog.connect @console_stream.push('log')
class TestSimpleFilteredSseStream(TestCase): @classmethod def setUpClass(cls): patch_all() def setUp(self): self.cache = MockRedisCacheAdapter() self.channel = 'channel1' self.stream = FilteredSseStream(self.channel, self.cache) def tearDown(self): self.cache.clear() def test_init(self): self.assertEqual(self.stream.channel, self.channel) self.assertEqual(self.stream.cache, self.cache) def test_create_channel_name(self): self.assertEqual(self.stream.create_subchannel_name('a'), '{}.a'.format(self.channel)) self.assertEqual(self.stream.create_subchannel_name(14), '{}.14'.format(self.channel)) def assert_header_in_response(self, response, header, value): header_tuple = next((header_ for header_ in response.headers if header_[0] == header), None) self.assertIsNotNone(header_tuple) self.assertEqual(header_tuple[1], value) def test_stream_default_headers(self): resp = self.stream.stream(subchannel='a') self.assert_header_in_response(resp, 'Connection', 'keep-alive') self.assert_header_in_response(resp, 'Cache-Control', 'no-cache') self.assert_header_in_response(resp, 'Content-Type', 'text/event-stream; charset=utf-8') def test_stream_custom_headers(self): resp = self.stream.stream(subchannel='a', headers={'x-custom': 'yes', 'Cache-Control': 'no-store'}) self.assert_header_in_response(resp, 'Connection', 'keep-alive') self.assert_header_in_response(resp, 'Cache-Control', 'no-store') self.assert_header_in_response(resp, 'Content-Type', 'text/event-stream; charset=utf-8') self.assert_header_in_response(resp, 'x-custom', 'yes') def test_send(self): @self.stream.push('event1') def pusher(a, ev, sub): gevent.sleep(0.1) return {'a': a}, sub, ev subs = ('aaa', 'bbb') result = {sub: [] for sub in subs} def listen(sub): for event in self.stream.send(subchannel=sub): result[sub].append(event) base_args = [('event1', 1), ('event2', 2)] args = {sub: [(event, data + i) for (event, data) in base_args] for i, sub in enumerate(subs)} def publish(sub): for event, data in args[sub]: pusher(data, event, sub) self.stream.unsubscribe(sub) sses = {sub: [SseEvent(event, {'a': arg}) for event, arg in args[sub]] for sub in subs} formatted_sses = {sub: [sse.format(i + 1) for i, sse in enumerate(sse_vals)] for sub, sse_vals in sses.items()} listen_threads = [gevent.spawn(listen, sub) for sub in subs] publish_threads = [gevent.spawn(publish, sub) for sub in subs] gevent.sleep(0.1) gevent.joinall(listen_threads, timeout=2) gevent.joinall(publish_threads, timeout=2) for sub in subs: self.assertListEqual(result[sub], formatted_sses[sub]) def test_send_publish_multiple(self): subs = ('a', 'bbb') @self.stream.push('event1') def pusher(a, ev): gevent.sleep(0.1) return {'a': a}, subs, ev result = {sub: [] for sub in subs} def listen(sub): for event in self.stream.send(subchannel=sub): result[sub].append(event) base_args = [('event1', 1), ('event2', 2)] def publish(): for event, data in base_args: pusher(data, event) for sub in subs: self.stream.unsubscribe(sub) sses = {sub: [SseEvent(event, {'a': arg}) for event, arg in base_args] for sub in subs} formatted_sses = {sub: [sse.format(i + 1) for i, sse in enumerate(sse_vals)] for sub, sse_vals in sses.items()} listen_threads = [gevent.spawn(listen, sub) for sub in subs] publish_thread = gevent.spawn(publish) gevent.sleep(0.1) gevent.joinall(listen_threads, timeout=2) publish_thread.join(timeout=2) for sub in subs: self.assertListEqual(result[sub], formatted_sses[sub]) def test_send_with_retry(self): @self.stream.push('event1') def pusher(a, ev, sub): gevent.sleep(0.1) return {'a': a}, sub, ev subs = ('a', 'b') result = {'a': [], 'b': []} def listen(sub): for event in self.stream.send(subchannel=sub, retry=50): result[sub].append(event) base_args = [('event1', 1), ('event2', 2)] args = {sub: [(event, data + i) for (event, data) in base_args] for i, sub in enumerate(subs)} def publish(sub): for event, data in args[sub]: pusher(data, event, sub) self.stream.unsubscribe(sub) sses = {sub: [SseEvent(event, {'a': arg}) for event, arg in args[sub]] for sub in subs} formatted_sses = {sub: [sse.format(i + 1, retry=50) for i, sse in enumerate(sse_vals)] for sub, sse_vals in sses.items()} listen_threads = [gevent.spawn(listen, sub) for sub in subs] publish_threads = [gevent.spawn(publish, sub) for sub in subs] gevent.sleep(0.1) gevent.joinall(listen_threads, timeout=2) gevent.joinall(publish_threads, timeout=2) for sub in subs: self.assertListEqual(result[sub], formatted_sses[sub])