Initial commit
This commit is contained in:
commit
e6cdf8127f
|
@ -0,0 +1,8 @@
|
|||
*.pyc
|
||||
.coverage
|
||||
htmlcov/
|
||||
build/
|
||||
dist/
|
||||
*.egg-info/
|
||||
pyenv/
|
||||
/preserves
|
|
@ -0,0 +1,16 @@
|
|||
test:
|
||||
@echo no tests yet, lol
|
||||
|
||||
clean:
|
||||
rm -rf htmlcov
|
||||
find . -iname __pycache__ -o -iname '*.pyc' | xargs rm -rf
|
||||
rm -f .coverage
|
||||
rm -rf preserves.egg-info build dist
|
||||
|
||||
# sudo apt install python3-wheel twine
|
||||
publish: clean
|
||||
python3 setup.py sdist bdist_wheel
|
||||
twine upload dist/*
|
||||
|
||||
veryclean: clean
|
||||
rm -rf pyenv
|
|
@ -0,0 +1,60 @@
|
|||
import sys
|
||||
import asyncio
|
||||
import random
|
||||
import threading
|
||||
import syndicate.mini.core as S
|
||||
|
||||
Present = S.Record.makeConstructor('Present', 'who')
|
||||
Says = S.Record.makeConstructor('Says', 'who what')
|
||||
|
||||
if len(sys.argv) == 3:
|
||||
conn = S.TcpConnection(sys.argv[1], int(sys.argv[2]))
|
||||
elif len(sys.argv) == 2:
|
||||
conn = S.WebsocketConnection(sys.argv[1])
|
||||
elif len(sys.argv) == 1:
|
||||
conn = S.WebsocketConnection('ws://localhost:8000/broker')
|
||||
else:
|
||||
sys.stderr.write(b'Usage: chat.py [ HOST PORT | WEBSOCKETURL ]\n')
|
||||
sys.exit(1)
|
||||
|
||||
## Courtesy of http://listofrandomnames.com/ :-)
|
||||
names = ['Daria', 'Kendra', 'Danny', 'Rufus', 'Diana', 'Arnetta', 'Dominick', 'Melonie', 'Regan',
|
||||
'Glenda', 'Janet', 'Luci', 'Ronnie', 'Vita', 'Amie', 'Stefani', 'Catherine', 'Grady',
|
||||
'Terrance', 'Rey', 'Fay', 'Shantae', 'Carlota', 'Judi', 'Crissy', 'Tasha', 'Jordan',
|
||||
'Rolande', 'Buster', 'Diamond', 'Dallas', 'Lissa', 'Yang', 'Charlena', 'Brooke', 'Haydee',
|
||||
'Griselda', 'Kasie', 'Clara', 'Claudie', 'Darell', 'Emery', 'Barbera', 'Chong', 'Karin',
|
||||
'Veronica', 'Karly', 'Shaunda', 'Nigel', 'Cleo']
|
||||
|
||||
me = random.choice(names) + '_' + str(random.randint(10, 1000))
|
||||
|
||||
S.Endpoint(conn, Present(me))
|
||||
|
||||
S.Endpoint(conn, S.Observe(Present(S.CAPTURE)),
|
||||
on_add=lambda who: print(who, 'joined'),
|
||||
on_del=lambda who: print(who, 'left'))
|
||||
|
||||
S.Endpoint(conn, S.Observe(Says(S.CAPTURE, S.CAPTURE)),
|
||||
on_msg=lambda who, what: print(who, 'said', repr(what)))
|
||||
|
||||
async def reconnect(loop):
|
||||
while True:
|
||||
await conn.main(loop)
|
||||
if not conn: break
|
||||
print('-'*50, 'Disconnected')
|
||||
await asyncio.sleep(2)
|
||||
if not conn: break
|
||||
|
||||
def accept_input(loop):
|
||||
global conn
|
||||
while True:
|
||||
line = sys.stdin.readline()
|
||||
if not line:
|
||||
conn.destroy()
|
||||
conn = None
|
||||
break
|
||||
conn.send(Says(me, line.strip()))
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
threading.Thread(target=lambda: accept_input(loop)).start()
|
||||
loop.run_until_complete(reconnect(loop))
|
||||
loop.close()
|
|
@ -0,0 +1,3 @@
|
|||
[ -d pyenv ] || virtualenv -p python3 pyenv
|
||||
. pyenv/bin/activate
|
||||
pip install -r requirements.txt
|
|
@ -0,0 +1 @@
|
|||
websockets
|
|
@ -0,0 +1,24 @@
|
|||
try:
|
||||
from setuptools import setup
|
||||
except ImportError:
|
||||
from distutils.core import setup
|
||||
|
||||
setup(
|
||||
name="mini-syndicate",
|
||||
version="0.0.0",
|
||||
author="Tony Garnock-Jones",
|
||||
author_email="tonyg@leastfixedpoint.com",
|
||||
license="GNU General Public License v3 or later (GPLv3+)",
|
||||
classifiers=[
|
||||
"Development Status :: 3 - Alpha",
|
||||
"Intended Audience :: Developers",
|
||||
"Topic :: Software Development :: Libraries :: Python Modules",
|
||||
"License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)",
|
||||
"Programming Language :: Python :: 3",
|
||||
],
|
||||
packages=["syndicate"],
|
||||
url="https://github.com/syndicate-lang/mini-syndicate-py",
|
||||
description="Syndicate-like library for integrating Python with the Syndicate ecosystem",
|
||||
install_requires=['websockets', 'preserves'],
|
||||
python_requires=">=3.6, <4",
|
||||
)
|
|
@ -0,0 +1 @@
|
|||
__path__ = __import__('pkgutil').extend_path(__path__, __name__)
|
|
@ -0,0 +1 @@
|
|||
__path__ = __import__('pkgutil').extend_path(__path__, __name__)
|
|
@ -0,0 +1,225 @@
|
|||
import asyncio
|
||||
import secrets
|
||||
import logging
|
||||
import websockets
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
import syndicate.mini.protocol as protocol
|
||||
|
||||
from syndicate.mini.protocol import Capture, Discard, Observe
|
||||
CAPTURE = Capture(Discard())
|
||||
|
||||
from preserves import *
|
||||
|
||||
_instance_id = secrets.token_urlsafe(8)
|
||||
_uuid_counter = 0
|
||||
|
||||
def uuid(prefix='__@syndicate'):
|
||||
global _uuid_counter
|
||||
c = _uuid_counter
|
||||
_uuid_counter = c + 1
|
||||
return prefix + '_' + _instance_id + '_' + str(c)
|
||||
|
||||
def _ignore(*args, **kwargs):
|
||||
pass
|
||||
|
||||
class Endpoint(object):
|
||||
def __init__(self, conn, assertion, id=None, on_add=None, on_del=None, on_msg=None):
|
||||
self.conn = conn
|
||||
self.assertion = assertion
|
||||
self.id = id or uuid('sub' if Observe.isClassOf(assertion) else 'pub')
|
||||
self.on_add = on_add or _ignore
|
||||
self.on_del = on_del or _ignore
|
||||
self.on_msg = on_msg or _ignore
|
||||
self.cache = set()
|
||||
self.conn._update_endpoint(self)
|
||||
|
||||
def set(self, new_assertion):
|
||||
self.assertion = new_assertion
|
||||
if self.conn:
|
||||
self.conn._update_endpoint(self)
|
||||
|
||||
def send(self, message):
|
||||
'''Shortcut to Connection.send.'''
|
||||
if self.conn:
|
||||
self.conn.send(message)
|
||||
|
||||
def destroy(self):
|
||||
if self.conn:
|
||||
self.conn._clear_endpoint(self)
|
||||
self.conn = None
|
||||
|
||||
def _reset(self):
|
||||
for captures in set(self.cache):
|
||||
self._del(captures)
|
||||
|
||||
def _add(self, captures):
|
||||
if captures not in self.cache:
|
||||
self.cache.add(captures)
|
||||
self.on_add(*captures)
|
||||
|
||||
def _del(self, captures):
|
||||
if captures in self.cache:
|
||||
self.cache.discard(captures)
|
||||
self.on_del(*captures)
|
||||
|
||||
def _msg(self, captures):
|
||||
self.on_msg(*captures)
|
||||
|
||||
class DummyEndpoint(object):
|
||||
def _add(self, captures): pass
|
||||
def _del(self, captures): pass
|
||||
def _msg(self, captures): pass
|
||||
|
||||
_dummy_endpoint = DummyEndpoint()
|
||||
|
||||
class Connection(object):
|
||||
def __init__(self):
|
||||
self.endpoints = {}
|
||||
|
||||
def _each_endpoint(self):
|
||||
return list(self.endpoints.values())
|
||||
|
||||
def destroy(self):
|
||||
for ep in self._each_endpoint():
|
||||
ep.destroy()
|
||||
self._disconnect()
|
||||
|
||||
def _encode(self, event):
|
||||
e = protocol.Encoder()
|
||||
e.append(event)
|
||||
return e.contents()
|
||||
|
||||
def _update_endpoint(self, ep):
|
||||
self.endpoints[ep.id] = ep
|
||||
self._send(self._encode(protocol.Assert(ep.id, ep.assertion)))
|
||||
|
||||
def _clear_endpoint(self, ep):
|
||||
if ep.id in self.endpoints:
|
||||
del self.endpoints[ep.id]
|
||||
self._send(self._encode(protocol.Clear(ep.id)))
|
||||
|
||||
def send(self, message):
|
||||
self._send(self._encode(protocol.Message(message)))
|
||||
|
||||
def _on_disconnected(self):
|
||||
for ep in self._each_endpoint():
|
||||
ep._reset()
|
||||
self._disconnect()
|
||||
|
||||
def _on_connected(self):
|
||||
for ep in self._each_endpoint():
|
||||
self._update_endpoint(ep)
|
||||
|
||||
def _lookup(self, endpointId):
|
||||
return self.endpoints.get(endpointId, _dummy_endpoint)
|
||||
|
||||
def _on_event(self, v):
|
||||
if protocol.Add.isClassOf(v): return self._lookup(v[0])._add(v[1])
|
||||
if protocol.Del.isClassOf(v): return self._lookup(v[0])._del(v[1])
|
||||
if protocol.Msg.isClassOf(v): return self._lookup(v[0])._msg(v[1])
|
||||
|
||||
def _send(self, bs):
|
||||
raise Exception('subclassresponsibility')
|
||||
|
||||
def _disconnect(self):
|
||||
raise Exception('subclassresponsibility')
|
||||
|
||||
class TcpConnection(Connection, asyncio.Protocol):
|
||||
def __init__(self, host, port):
|
||||
super().__init__()
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.decoder = None
|
||||
self.stop_signal = None
|
||||
self.transport = None
|
||||
|
||||
def connection_lost(self, exc):
|
||||
self._on_disconnected()
|
||||
|
||||
def connection_made(self, transport):
|
||||
self.transport = transport
|
||||
self._on_connected()
|
||||
|
||||
def data_received(self, chunk):
|
||||
self.decoder.extend(chunk)
|
||||
while True:
|
||||
v = self.decoder.try_next()
|
||||
if v is None: break
|
||||
self._on_event(v)
|
||||
|
||||
def _send(self, bs):
|
||||
if self.transport:
|
||||
self.transport.write(bs)
|
||||
|
||||
def _disconnect(self):
|
||||
if self.stop_signal:
|
||||
self.stop_signal.set_result(True)
|
||||
|
||||
async def main(self, loop):
|
||||
if self.transport is not None:
|
||||
raise Exception('Cannot run connection twice!')
|
||||
|
||||
self.decoder = protocol.Decoder()
|
||||
self.stop_signal = loop.create_future()
|
||||
try:
|
||||
_transport, _protocol = await loop.create_connection(
|
||||
lambda: self,
|
||||
self.host,
|
||||
self.port)
|
||||
except OSError:
|
||||
log.error('Could not connect to broker', exc_info=True)
|
||||
return False
|
||||
|
||||
try:
|
||||
await self.stop_signal
|
||||
return True
|
||||
finally:
|
||||
self.transport.close()
|
||||
self.transport = None
|
||||
self.stop_signal = None
|
||||
self.decoder = None
|
||||
|
||||
class WebsocketConnection(Connection):
|
||||
def __init__(self, url):
|
||||
super().__init__()
|
||||
self.url = url
|
||||
self.loop = None
|
||||
self.ws = None
|
||||
|
||||
def _send(self, bs):
|
||||
if self.ws:
|
||||
self.loop.call_soon_threadsafe(lambda: self.loop.create_task(self.ws.send(bs)))
|
||||
|
||||
def _disconnect(self):
|
||||
if self.ws:
|
||||
self.loop.call_soon_threadsafe(lambda: self.loop.create_task(self.ws.close()))
|
||||
|
||||
async def main(self, loop):
|
||||
if self.ws is not None:
|
||||
raise Exception('Cannot run connection twice!')
|
||||
|
||||
self.loop = loop
|
||||
|
||||
try:
|
||||
async with websockets.connect(self.url) as ws:
|
||||
self.ws = ws
|
||||
self._on_connected()
|
||||
try:
|
||||
while True:
|
||||
chunk = await loop.create_task(ws.recv())
|
||||
self._on_event(protocol.Decoder(chunk).next())
|
||||
except websockets.exceptions.ConnectionClosed:
|
||||
pass
|
||||
except OSError:
|
||||
log.error('Could not connect to broker', exc_info=True)
|
||||
return False
|
||||
finally:
|
||||
self._on_disconnected()
|
||||
|
||||
if self.ws:
|
||||
await self.ws.close()
|
||||
self.loop = None
|
||||
self.ws = None
|
||||
return True
|
|
@ -0,0 +1,32 @@
|
|||
import preserves
|
||||
from preserves import Record, Symbol
|
||||
|
||||
## Client -> Broker
|
||||
Assert = Record.makeConstructor('Assert', 'endpointName assertion')
|
||||
Clear = Record.makeConstructor('Clear', 'endpointName')
|
||||
Message = Record.makeConstructor('Message', 'body')
|
||||
|
||||
## Broker -> Client
|
||||
Add = Record.makeConstructor('Add', 'endpointName captures')
|
||||
Del = Record.makeConstructor('Del', 'endpointName captures')
|
||||
Msg = Record.makeConstructor('Msg', 'endpointName captures')
|
||||
|
||||
## Standard Syndicate constructors
|
||||
Observe = Record.makeConstructor('observe', 'specification')
|
||||
Capture = Record.makeConstructor('capture', 'specification')
|
||||
Discard = Record.makeConstructor('discard', '')
|
||||
|
||||
class Decoder(preserves.Decoder):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(Decoder, self).__init__(*args, **kwargs)
|
||||
_init_shortforms(self)
|
||||
|
||||
class Encoder(preserves.Encoder):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(Encoder, self).__init__(*args, **kwargs)
|
||||
_init_shortforms(self)
|
||||
|
||||
def _init_shortforms(c):
|
||||
c.set_shortform(0, Discard.constructorInfo.key)
|
||||
c.set_shortform(1, Capture.constructorInfo.key)
|
||||
c.set_shortform(2, Observe.constructorInfo.key)
|
Loading…
Reference in New Issue