Skip to content

Commit

Permalink
sync (#1461)
Browse files Browse the repository at this point in the history
* allow free rated / price zero services
* expand IDL generator
  • Loading branch information
oberstet committed Feb 15, 2021
1 parent 6c14e24 commit 3065699
Show file tree
Hide file tree
Showing 8 changed files with 103 additions and 53 deletions.
2 changes: 1 addition & 1 deletion autobahn/_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,4 @@
#
###############################################################################

__version__ = '21.2.2.dev2'
__version__ = '21.2.2.dev3'
4 changes: 3 additions & 1 deletion autobahn/twisted/xbr/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
import uuid

from autobahn.xbr._util import hl
from autobahn.xbr._interfaces import IProvider, ISeller, IConsumer, IBuyer
from autobahn.xbr._interfaces import IProvider, ISeller, IConsumer, IBuyer, IDelegate
from autobahn.xbr import _seller, _buyer, _blockchain

class SimpleBlockchain(_blockchain.SimpleBlockchain):
Expand Down Expand Up @@ -101,5 +101,7 @@ class SimpleBuyer(_buyer.SimpleBuyer):

ISeller.register(SimpleSeller)
IProvider.register(SimpleSeller)
IDelegate.register(SimpleSeller)
IBuyer.register(SimpleBuyer)
IConsumer.register(SimpleBuyer)
IDelegate.register(SimpleBuyer)
1 change: 1 addition & 0 deletions autobahn/xbr/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,7 @@ def account_from_seedphrase(seephrase, index=0):
'IConsumer',
'ISeller',
'IBuyer',
'IDelegate',

'FbsSchema',
'FbsService',
Expand Down
42 changes: 31 additions & 11 deletions autobahn/xbr/_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -1036,15 +1036,15 @@ def _main():
if is_first_by_category:
is_first_by_category_modules[(modulename, category)] = True

# render object type template into python code section
# render template into python code section
if args.language == 'python':
# render obj|enum|service.py.jinja2 template
tmpl = env.get_template('{}.py.jinja2'.format(category))
code = tmpl.render(metadata=metadata, FbsType=FbsType, render_imports=is_first, is_first_by_category=is_first_by_category)
code = tmpl.render(repo=repo, metadata=metadata, FbsType=FbsType, render_imports=is_first, is_first_by_category=is_first_by_category)

# render test_obj|enum|service.py.jinja2 template
test_tmpl = env.get_template('test_{}.py.jinja2'.format(category))
test_code = test_tmpl.render(metadata=metadata, FbsType=FbsType, render_imports=is_first, is_first_by_category=is_first_by_category)
test_code = test_tmpl.render(repo=repo, metadata=metadata, FbsType=FbsType, render_imports=is_first, is_first_by_category=is_first_by_category)
elif args.language == 'json':
code = json.dumps(metadata.marshal(),
separators=(', ', ': '),
Expand All @@ -1068,6 +1068,7 @@ def _main():
# write out code modules
#
i = 0
initialized = set()
for code_file, code_sections in code_modules.items():
code = '\n\n\n'.join(code_sections)
if code_file:
Expand All @@ -1085,8 +1086,9 @@ def _main():
_modulename = '.'.join(code_file_dir[:i + 1])[1:]
with open(fn, 'wb') as f:
tmpl = env.get_template('module.py.jinja2')
init_code = tmpl.render(modulename=_modulename)
init_code = tmpl.render(repo=repo, modulename=_modulename)
f.write(init_code.encode('utf8'))
initialized.add(fn)

if args.language == 'python':
if code_file:
Expand All @@ -1101,15 +1103,28 @@ def _main():
else:
code_file_name = 'init.json'
test_code_file_name = None
else:
code_file_name = None
test_code_file_name = None

fn = os.path.join(*(code_file_dir + [code_file_name]))
fn = os.path.join(args.output, fn)
# write out code modules
#
if code_file_name:
data = code.encode('utf8')

data = code.encode('utf8')
with open(fn, 'ab') as fd:
fd.write(data)
fn = os.path.join(*(code_file_dir + [code_file_name]))
fn = os.path.join(args.output, fn)

print('Ok, written {} bytes to {}'.format(len(data), fn))
if fn not in initialized and os.path.exists(fn):
os.remove(fn)
with open(fn, 'wb') as fd:
fd.write('# Copyright (c) ...'.encode('utf8'))
initialized.add(fn)

with open(fn, 'ab') as fd:
fd.write(data)

print('Ok, written {} bytes to {}'.format(len(data), fn))

# write out unit test code modules
#
Expand All @@ -1121,11 +1136,16 @@ def _main():
fn = os.path.join(*(code_file_dir + [test_code_file_name]))
fn = os.path.join(args.output, fn)

if fn not in initialized and os.path.exists(fn):
os.remove(fn)
with open(fn, 'wb') as fd:
fd.write('# Copyright (c) ...'.encode('utf8'))
initialized.add(fn)

with open(fn, 'ab') as fd:
fd.write(data)

print('Ok, written {} bytes to {}'.format(len(data), fn))

else:
if args.command is None or args.command == 'noop':
print('no command given. select from: {}'.format(', '.join(_COMMANDS)))
Expand Down
15 changes: 15 additions & 0 deletions autobahn/xbr/_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,21 @@ def __init__(self,
self._attrs = attrs
self._docs = docs

def map(self, language: str) -> str:
if language == 'python':
klass = self._name.split('.')[-1]
return klass
else:
raise NotImplementedError()

def map_import(self, language: str) -> str:
if language == 'python':
base = self._name.split('.')[-2]
klass = self._name.split('.')[-1]
return 'from {} import {}'.format(base, klass)
else:
raise NotImplementedError()

@property
def name(self):
return self._name
Expand Down
4 changes: 2 additions & 2 deletions autobahn/xbr/_seller.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ def __init__(self, api_id, price, interval=None, count=None, on_rotate=None):
:type on_rotate: callable
"""
assert type(api_id) == bytes and len(api_id) == 16
assert type(price) == int and price > 0
assert type(price) == int and price >= 0
assert interval is None or (type(interval) == int and interval > 0)
assert count is None or (type(count) == int and count > 0)
assert (interval is None and count is not None) or (interval is not None and count is None)
Expand Down Expand Up @@ -292,7 +292,7 @@ def add(self, api_id, prefix, price, interval=None, count=None, categories=None)
:type count: int
"""
assert type(api_id) == bytes and len(api_id) == 16 and api_id not in self._keys
assert type(price) == int and price > 0
assert type(price) == int and price >= 0
assert interval is None or (type(interval) == int and interval > 0)
assert count is None or (type(count) == int and count > 0)
assert (interval is None and count is not None) or (interval is not None and count is None)
Expand Down
10 changes: 7 additions & 3 deletions autobahn/xbr/templates/obj.py.jinja2
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,18 @@
{% if render_imports %}
import uuid
import pprint
from typing import Dict, List, Optional
from typing import Dict, List, Optional, TypeVar

import flatbuffers
from flatbuffers.compat import import_numpy
np = import_numpy()


{% endif %}
# https://stackoverflow.com/a/46064289/884770
T_{{ metadata.classname }} = TypeVar('T_{{ metadata.classname }}', bound='{{ metadata.classname }}')


class {{ metadata.classname }}(object):
"""
{{ metadata.docs }}
Expand Down Expand Up @@ -122,7 +126,7 @@ class {{ metadata.classname }}(object):
{% endfor %}

@staticmethod
def parse(data: Dict) -> object:
def parse(data: Dict) -> T_{{ metadata.classname }}:
"""
Parse generic, native language object into a typed, native language object.

Expand Down Expand Up @@ -198,7 +202,7 @@ class {{ metadata.classname }}(object):
return '\n{}\n'.format(pprint.pformat(self.marshal()))

@staticmethod
def cast(buf: bytes, offset: int=0) -> object:
def cast(buf: bytes, offset: int=0) -> T_{{ metadata.classname }}:
"""
Cast a FlatBuffers raw input buffer as a typed object of this class.

Expand Down
78 changes: 43 additions & 35 deletions autobahn/xbr/templates/service.py.jinja2
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@
## service types (aka "APIs")
##
import abc
from autobahn.wamp.types import PublishOptions, SubscribeOptions
from pprint import pformat

from txaio.interfaces import ILogger
from autobahn.wamp.types import PublishOptions, SubscribeOptions, EventDetails
from autobahn.wamp.request import Publication
from autobahn.wamp.interfaces import ISession
from autobahn.xbr import IDelegate
Expand All @@ -19,117 +22,122 @@ class {{ metadata.classname }}(object):
__slots__ = [
'log',
'_x_api_id',
'_x_prefix',
'_x_session',
'_x_delegate',
]

def __init__(self, log=None):
def __init__(self, prefix: str, log: Optional[ILogger]=None):
"""

:param prefix: The URI prefix under which this API will be instantiated under on the realm joined.
:param log: If provided, log to this logger, else create a new one internally.
"""
if log:
self.log = log
else:
import txaio
self.log = txaio.make_logger()
self._x_api_id = uuid.UUID('{{ metadata.attrs.uuid }}').bytes
self._x_api_id = uuid.UUID('{{ metadata.attrs.uuid }}')
self._x_prefix = prefix
self._x_session = None
self._x_delegate = None

@property
def api(self) -> Optional[uuid.UUID]:
"""
WAMP session this API is attached to.
"""
return self._x_api_id

@property
def prefix(self) -> Optional[str]:
"""
WAMP URI prefix under which this API is instantiated.
"""
return self._x_prefix

@property
def session(self) -> Optional[ISession]:
"""
WAMP session this API is attached to.
"""
return self._x_session

@property
def delegate(self) -> Optional[IDelegate]:
"""
XBR (buyer/seller) delegate this API is attached to.
"""
return self._x_delegate

def prefix(self) -> Optional[str]:
"""
WAMP URI prefix under which this API is instantiated.
"""
return self._x_prefix

def is_attched(self) -> bool:
@property
def is_attached(self) -> bool:
"""
Flag indicating whether this API instance is currently attached to a session/delegate.
"""
if self._x_session is None and self._x_seller is None and self._x_prefix is None:
return False
elif self._x_session is not None and self._x_seller is not None and self._x_prefix is not None:
return True
else:
assert False, 'logic error - should not arrive here'
return self._x_session is not None and self._x_delegate is not None

async def attach(self, session: ISession, delegate: IDelegate, prefix: str):
async def attach(self, session, delegate):
"""
Attach this API instance with the given session and delegate, and under the given WAMP URI prefix.

:param session: WAMP session this API instance is attached to.
:param delegate: XBR (buyer/seller) delegate used by this API instance.
:param prefix: WAMP URI prefix under which this API instance is attached to.
"""
assert self._x_session is None
assert self._x_seller is None
assert self._x_prefix is None
assert not self.is_attached

self._x_session = session
self._x_delegate = delegate
self._x_prefix = prefix
subscriptions = []
{% for call_name in metadata.calls_by_id %}
{% if metadata.calls[call_name].attrs['type'] == 'topic' %}

async def do_receive_{{ call_name }}(key_id, enc_ser, ciphertext, details=None):
print('Received event {}, encrypted with key_id={}'.format(details.publication, uuid.UUID(bytes=key_id)))
try:
payload = await self._x_delegate.unwrap(key_id, enc_ser, ciphertext)
obj = HomeDevice.parse(payload)
obj = {{ repo.objs[metadata.calls[call_name].request.name].map('python') }}.parse(payload)
except:
self.log.failure()
else:
print('Unencrypted event payload: {}'.format(pformat(obj)))
self.receive_{{ call_name }}(obj)

topic = '{}.{{ call_name }}'.format(self._x_prefix)
await self.subscribe(do_receive_{{ call_name }}, topic, options=SubscribeOptions(details=True))
sub = await self._x_session.subscribe(do_receive_{{ call_name }}, topic, options=SubscribeOptions(details=True))
subscriptions.append(sub)
{% endif %}
{% endfor %}

for sub in subscriptions:
self.log.info('Subscription {} created for "{}"'.format(sub.id, sub.topic))

async def detach(self):
"""
Detach this API instance from the session and delegate.
"""
assert self._x_session is not None
assert self._x_delegate is not None
assert self._x_prefix is not None
assert self.is_attached
if self._x_session.is_attached():
await self._x_session.leave()
self._x_session = None
self._x_delegate = None
self._x_prefix = None

{% for call_name in metadata.calls_by_id %}
{% if metadata.calls[call_name].attrs['type'] == 'topic' %}
async def publish_{{ call_name }}(self, device: HomeDevice) -> Publication:
async def publish_{{ call_name }}(self, evt: {{ repo.objs[metadata.calls[call_name].request.name].map('python') }}, options: Optional[PublishOptions] = None) -> Publication:
"""
Publish - {{ metadata.calls[call_name].docs }}
"""
assert self._x_session is not None and self._x_session.is_attached()
assert self._x_seller is not None
assert self.is_attached

topic = '{}.{{ call_name }}'.format(self._x_prefix)
payload = device.marshal()
payload = evt.marshal()
key_id, enc_ser, ciphertext = await self._x_delegate.wrap(self._x_api_id, topic, payload)
pub = await self._x_session.publish(topic, key_id, enc_ser, ciphertext,
options=PublishOptions(acknowledge=True))
pub = await self._x_session.publish(topic, key_id, enc_ser, ciphertext, options=options)
return pub

def receive_{{ call_name }}(self, device: HomeDevice):
def receive_{{ call_name }}(self, evt: {{ repo.objs[metadata.calls[call_name].request.name].map('python') }}, details: Optional[EventDetails] = None):
"""
Receive - {{ metadata.calls[call_name].docs }}
"""
Expand Down

0 comments on commit 3065699

Please sign in to comment.