From 5ab882a88a1d656a794a2475cdd58b5b10c154ab Mon Sep 17 00:00:00 2001 From: James Riehl Date: Wed, 23 Nov 2022 11:49:50 +0000 Subject: [PATCH 01/41] feat: add internal agent protocol --- src/nexus/agent.py | 82 +++++++++++++++---------------------------- src/nexus/protocol.py | 8 +++-- 2 files changed, 33 insertions(+), 57 deletions(-) diff --git a/src/nexus/agent.py b/src/nexus/agent.py index 46fa2c59..3dd8bdab 100644 --- a/src/nexus/agent.py +++ b/src/nexus/agent.py @@ -3,8 +3,6 @@ import logging from typing import Optional, List, Set, Tuple, Any, Union -from apispec import APISpec - from cosmpy.aerial.wallet import LocalWallet, PrivateKey from nexus.asgi import ASGIServer @@ -12,7 +10,7 @@ from nexus.crypto import Identity, derive_key_from_seed from nexus.dispatch import Sink, dispatcher from nexus.models import Model -from nexus.protocol import Protocol, OPENAPI_VERSION +from nexus.protocol import Protocol from nexus.resolver import Resolver, AlmanacResolver from nexus.storage import KeyValueStore from nexus.network import get_ledger, get_reg_contract @@ -61,7 +59,7 @@ def __init__( PrivateKey(derive_key_from_seed(seed, LEDGER_PREFIX, 0)), prefix=LEDGER_PREFIX, ) - self._endpoint = endpoint if endpoint is not None else "" + self._endpoint = endpoint if endpoint is not None else "123" self._ledger = get_ledger() self._reg_contract = get_reg_contract() self._storage = KeyValueStore(self.address[0:16]) @@ -82,15 +80,14 @@ def __init__( self._message_queue = asyncio.Queue() self._version = version or "0.1.0" - self.spec = APISpec( - title=name, - version=self._version, - openapi_version=OPENAPI_VERSION, - ) + # initialize the internal agent protocol + self._protocol = Protocol(name=self._name, version=self._version) # register with the dispatcher self._dispatcher.register(self.address, self) + self._create_interval_tasks() + # start the background message queue processor task = self._loop.create_task(self._process_message_queue()) self._background_tasks.add(task) @@ -127,7 +124,8 @@ async def register(self, ctx: Context): agent_balance = ctx.ledger.query_bank_balance(ctx.wallet) if agent_balance < REGISTRATION_FEE: - raise Exception(f"Insufficient funds to register {self._name}") + logging.exception(f"Insufficient funds to register {self._name}") + return msg = { "register": { @@ -180,60 +178,34 @@ def handler(*args, **kwargs): def on_message( self, model: Model, replies: Optional[Union[Model, Set[Model]]] = None ): - def decorator_on_message(func: MessageCallback): - @functools.wraps(func) - def handler(*args, **kwargs): - return func(*args, **kwargs) - - self._add_message_handler(model, func, replies) - - return handler - - return decorator_on_message - - def _add_message_handler(self, model, func, replies): - schema_digest = Model.build_schema_digest(model) - - # update the model database - self._models[schema_digest] = model - self._message_handlers[schema_digest] = func - if replies is not None: - if not isinstance(replies, set): - replies = {replies} - self._replies[schema_digest] = { - Model.build_schema_digest(reply) for reply in replies - } - - self.spec.path( - path=model.__name__, - operations=dict( - post=dict(replies=[reply.__name__ for reply in replies]) - ), - ) + return self._protocol.on_message(model, replies) def include(self, protocol: Protocol): for func, period in protocol.intervals: - task = self._loop.create_task(_run_interval(func, self._ctx, period)) - self._background_tasks.add(task) - task.add_done_callback(self._background_tasks.discard) + self._protocol.intervals.append((func, period)) for schema_digest in protocol.models: - if schema_digest in self._models: + if schema_digest in self._protocol.models: raise RuntimeError("Unable to register duplicate model") - if schema_digest in self._message_handlers: + if schema_digest in self._protocol.message_handlers: raise RuntimeError("Unable to register duplicate message handler") if schema_digest not in protocol.message_handlers: raise RuntimeError("Unable to lookup up message handler in protocol") # include the message handlers from the protocol - self._models[schema_digest] = protocol.models[schema_digest] - self._replies[schema_digest] = protocol.replies[schema_digest] - self._message_handlers[schema_digest] = protocol.message_handlers[ - schema_digest - ] + self._protocol.add_message_handler( + protocol.models[schema_digest], + protocol.message_handlers[schema_digest], + set(protocol.replies[schema_digest].values()), + ) + + def _create_interval_tasks(self): + for func, period in self._protocol.intervals: + task = self._loop.create_task(_run_interval(func, self._ctx, period)) + self._background_tasks.add(task) + task.add_done_callback(self._background_tasks.discard) async def handle_message(self, sender, schema_digest: str, message: Any): - # schema_digest = _build_model_digest(message) await self._message_queue.put((schema_digest, sender, message)) def run(self): @@ -250,7 +222,7 @@ async def _process_message_queue(self): schema_digest, sender, message = await self._message_queue.get() # lookup the model definition - model_class = self._models.get(schema_digest) + model_class = self._protocol.models.get(schema_digest) if model_class is None: continue @@ -265,12 +237,14 @@ async def _process_message_queue(self): self._identity, self._wallet, self._ledger, - self._replies, + self._protocol.replies, MsgDigest(message=message, schema_digest=schema_digest), ) # attempt to find the handler - handler: MessageCallback = self._message_handlers.get(schema_digest) + handler: MessageCallback = self._protocol.message_handlers.get( + schema_digest + ) if handler is not None: await handler(context, sender, recovered) diff --git a/src/nexus/protocol.py b/src/nexus/protocol.py index 6e1d6309..9ee4f0bb 100644 --- a/src/nexus/protocol.py +++ b/src/nexus/protocol.py @@ -62,13 +62,15 @@ def decorator_on_message(func: MessageCallback): def handler(*args, **kwargs): return func(*args, **kwargs) - self._add_message_handler(model, func, replies) + self.add_message_handler(model, func, replies) return handler return decorator_on_message - def _add_message_handler(self, model, func, replies): + def add_message_handler( + self, model: Model, func: MessageCallback, replies: Union[Model, Set[Model]] + ): schema_digest = Model.build_schema_digest(model) # update the model database @@ -78,7 +80,7 @@ def _add_message_handler(self, model, func, replies): if not isinstance(replies, set): replies = {replies} self._replies[schema_digest] = { - Model.build_schema_digest(reply) for reply in replies + Model.build_schema_digest(reply): reply for reply in replies } self.spec.path( From 58e22a6390900546b63ecab5b0c1df9c176bdf64 Mon Sep 17 00:00:00 2001 From: James Riehl Date: Wed, 23 Nov 2022 13:26:43 +0000 Subject: [PATCH 02/41] feat: compress protocol digests --- src/nexus/agent.py | 7 ++++++- src/nexus/protocol.py | 22 ++++++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/src/nexus/agent.py b/src/nexus/agent.py index 3dd8bdab..85ecd24e 100644 --- a/src/nexus/agent.py +++ b/src/nexus/agent.py @@ -83,6 +83,11 @@ def __init__( # initialize the internal agent protocol self._protocol = Protocol(name=self._name, version=self._version) + # keep track of supported protocols + self.protocols = { + f"{self._protocol.name}:{self._protocol.version}": self._protocol.schema_digest + } + # register with the dispatcher self._dispatcher.register(self.address, self) @@ -131,7 +136,7 @@ async def register(self, ctx: Context): "register": { "record": { "Service": { - "protocols": list(self._models.keys()), + "protocols": list(self.protocols.values()), "endpoints": [{"url": self._endpoint, "weight": 1}], } } diff --git a/src/nexus/protocol.py b/src/nexus/protocol.py index 9ee4f0bb..3c46668c 100644 --- a/src/nexus/protocol.py +++ b/src/nexus/protocol.py @@ -1,4 +1,5 @@ import functools +import hashlib from typing import Set, Optional, Union from apispec import APISpec @@ -18,6 +19,7 @@ def __init__(self, name: Optional[str] = None, version: Optional[str] = None): self._replies = {} self._name = name or "" self._version = version or "0.1.0" + self._schema_digest = "" self.spec = APISpec( title=self._name, @@ -41,6 +43,18 @@ def replies(self): def message_handlers(self): return self._message_handlers + @property + def name(self): + return self._name + + @property + def version(self): + return self._version + + @property + def schema_digest(self): + return self._schema_digest + def on_interval(self, period: float): def decorator_on_interval(func: IntervalCallback): @functools.wraps(func) @@ -89,3 +103,11 @@ def add_message_handler( post=dict(replies=[reply.__name__ for reply in replies]) ), ) + self._update_schema_digest() + + def _update_schema_digest(self): + sorted_schema_digests = sorted(list(self._models.keys())) + hasher = hashlib.sha256() + for digest in sorted_schema_digests: + hasher.update(bytes.fromhex(digest)) + self._schema_digest = hasher.digest().hex() From 05a1137af65cf5d35c1bf0e4be4f9bda4e28e444 Mon Sep 17 00:00:00 2001 From: Alejandro-Morales Date: Wed, 23 Nov 2022 08:01:46 -0600 Subject: [PATCH 03/41] fix: example 06 --- examples/06-msg-verification/main.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/examples/06-msg-verification/main.py b/examples/06-msg-verification/main.py index b7abd710..02bc8ee7 100644 --- a/examples/06-msg-verification/main.py +++ b/examples/06-msg-verification/main.py @@ -18,13 +18,6 @@ def encode(message: str) -> bytes: alice = Agent(name="alice", seed="alice recovery password") bob = Agent(name="bob", seed="bob recovery password") -assert ( - alice.address == "agent1qg985el6kquqw3zdnq7tvpsz2n0srxa8e8eyp0nwpagp6yyg2ayus294dux" -) -assert ( - bob.address == "agent1qgqdlpds2w7rs032jgylcka8m5d663nvt6p5dmmffpmanljldchssyjhn9r" -) - @alice.on_interval(period=3.0) async def send_message(ctx: Context): From e1ddfd9aa34f24518b91c377a7d2ac74666734e5 Mon Sep 17 00:00:00 2001 From: James Riehl Date: Wed, 23 Nov 2022 16:43:48 +0000 Subject: [PATCH 04/41] feat: add protocol id --- src/nexus/agent.py | 5 ++--- src/nexus/protocol.py | 5 +++++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/nexus/agent.py b/src/nexus/agent.py index 85ecd24e..9ce81f76 100644 --- a/src/nexus/agent.py +++ b/src/nexus/agent.py @@ -84,9 +84,7 @@ def __init__( self._protocol = Protocol(name=self._name, version=self._version) # keep track of supported protocols - self.protocols = { - f"{self._protocol.name}:{self._protocol.version}": self._protocol.schema_digest - } + self.protocols = {self._protocol.id: self._protocol.schema_digest} # register with the dispatcher self._dispatcher.register(self.address, self) @@ -203,6 +201,7 @@ def include(self, protocol: Protocol): protocol.message_handlers[schema_digest], set(protocol.replies[schema_digest].values()), ) + self.protocols[protocol.id] = protocol.schema_digest def _create_interval_tasks(self): for func, period in self._protocol.intervals: diff --git a/src/nexus/protocol.py b/src/nexus/protocol.py index 3c46668c..56d9a1e5 100644 --- a/src/nexus/protocol.py +++ b/src/nexus/protocol.py @@ -19,6 +19,7 @@ def __init__(self, name: Optional[str] = None, version: Optional[str] = None): self._replies = {} self._name = name or "" self._version = version or "0.1.0" + self._id = f"{self._name}:{self._version}" self._schema_digest = "" self.spec = APISpec( @@ -51,6 +52,10 @@ def name(self): def version(self): return self._version + @property + def id(self): # pylint: disable=C0103 + return self._id + @property def schema_digest(self): return self._schema_digest From fd098f19b0e3ff46905be400c8466c002a3d3fe2 Mon Sep 17 00:00:00 2001 From: James Riehl Date: Thu, 24 Nov 2022 10:44:15 +0000 Subject: [PATCH 05/41] fix: update internal protocol digest and some names --- src/nexus/agent.py | 7 +++++-- src/nexus/protocol.py | 15 ++++++++------- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/src/nexus/agent.py b/src/nexus/agent.py index 9ce81f76..482d5b2d 100644 --- a/src/nexus/agent.py +++ b/src/nexus/agent.py @@ -84,7 +84,7 @@ def __init__( self._protocol = Protocol(name=self._name, version=self._version) # keep track of supported protocols - self.protocols = {self._protocol.id: self._protocol.schema_digest} + self.protocols = {self._protocol.canonical_name: self._protocol.digest} # register with the dispatcher self._dispatcher.register(self.address, self) @@ -201,7 +201,10 @@ def include(self, protocol: Protocol): protocol.message_handlers[schema_digest], set(protocol.replies[schema_digest].values()), ) - self.protocols[protocol.id] = protocol.schema_digest + self.protocols[protocol.canonical_name] = protocol.digest + + # Update the internal protocol digest in the list of supported protocols + self.protocols[self._protocol.canonical_name] = self._protocol.digest def _create_interval_tasks(self): for func, period in self._protocol.intervals: diff --git a/src/nexus/protocol.py b/src/nexus/protocol.py index 56d9a1e5..26f73837 100644 --- a/src/nexus/protocol.py +++ b/src/nexus/protocol.py @@ -19,8 +19,8 @@ def __init__(self, name: Optional[str] = None, version: Optional[str] = None): self._replies = {} self._name = name or "" self._version = version or "0.1.0" - self._id = f"{self._name}:{self._version}" - self._schema_digest = "" + self._canonical_name = f"{self._name}:{self._version}" + self._digest = "" self.spec = APISpec( title=self._name, @@ -53,12 +53,13 @@ def version(self): return self._version @property - def id(self): # pylint: disable=C0103 - return self._id + def canonical_name(self): + return self._canonical_name @property - def schema_digest(self): - return self._schema_digest + def digest(self): + assert self._digest != "", "Protocol digest empty" + return self._digest def on_interval(self, period: float): def decorator_on_interval(func: IntervalCallback): @@ -115,4 +116,4 @@ def _update_schema_digest(self): hasher = hashlib.sha256() for digest in sorted_schema_digests: hasher.update(bytes.fromhex(digest)) - self._schema_digest = hasher.digest().hex() + self._digest = hasher.digest().hex() From ff38ea3f637b61fde33315efe82c347c6ad8461d Mon Sep 17 00:00:00 2001 From: James Riehl Date: Thu, 24 Nov 2022 14:46:26 +0000 Subject: [PATCH 06/41] fix: init internal protocol empty --- src/nexus/agent.py | 2 +- src/nexus/protocol.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/nexus/agent.py b/src/nexus/agent.py index fd6a641e..e6fcbba5 100644 --- a/src/nexus/agent.py +++ b/src/nexus/agent.py @@ -84,7 +84,7 @@ def __init__( self._protocol = Protocol(name=self._name, version=self._version) # keep track of supported protocols - self.protocols = {self._protocol.canonical_name: self._protocol.digest} + self.protocols = {} # register with the dispatcher self._dispatcher.register(self.address, self) diff --git a/src/nexus/protocol.py b/src/nexus/protocol.py index 26f73837..2e447cd3 100644 --- a/src/nexus/protocol.py +++ b/src/nexus/protocol.py @@ -58,7 +58,7 @@ def canonical_name(self): @property def digest(self): - assert self._digest != "", "Protocol digest empty" + # assert self._digest != "", "Protocol digest empty" return self._digest def on_interval(self, period: float): From 2f8f7d6bcd5d0fdb98374f47255aeab280e9ed46 Mon Sep 17 00:00:00 2001 From: James Riehl Date: Thu, 24 Nov 2022 14:47:50 +0000 Subject: [PATCH 07/41] feat: add cleaning demo --- examples/10-cleaning-demo/cleaner.py | 37 +++++++ .../10-cleaning-demo/protocols/__init__.py | 0 .../10-cleaning-demo/protocols/cleaning.py | 100 ++++++++++++++++++ examples/10-cleaning-demo/user.py | 73 +++++++++++++ 4 files changed, 210 insertions(+) create mode 100644 examples/10-cleaning-demo/cleaner.py create mode 100644 examples/10-cleaning-demo/protocols/__init__.py create mode 100644 examples/10-cleaning-demo/protocols/cleaning.py create mode 100644 examples/10-cleaning-demo/user.py diff --git a/examples/10-cleaning-demo/cleaner.py b/examples/10-cleaning-demo/cleaner.py new file mode 100644 index 00000000..02f66fe7 --- /dev/null +++ b/examples/10-cleaning-demo/cleaner.py @@ -0,0 +1,37 @@ +from protocols.cleaning import cleaning_proto, Availability, Service + +from nexus import Agent +from nexus.setup import fund_agent_if_low + + +cleaner = Agent( + name="cleaner", + port=8001, + seed="cleaner secret seed phrase", + endpoint="http://127.0.0.1:8001/submit", +) + +fund_agent_if_low(cleaner.wallet.address()) + +print(cleaner.address) + +# build the restaurant agent from stock protocols +cleaner.include(cleaning_proto) + +availability = Availability( + address=25, + max_distance=10, + time_start=10, + time_end=18, + services=[Service.Floor, Service.Window, Service.Laundry], + min_hourly_price=12, +) +markup = 1.1 + +cleaner._storage.set( + "availability", availability.dict() +) # pylint: disable=protected-access +cleaner._storage.set("markup", markup) + +if __name__ == "__main__": + cleaner.run() diff --git a/examples/10-cleaning-demo/protocols/__init__.py b/examples/10-cleaning-demo/protocols/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/examples/10-cleaning-demo/protocols/cleaning.py b/examples/10-cleaning-demo/protocols/cleaning.py new file mode 100644 index 00000000..7e91fdee --- /dev/null +++ b/examples/10-cleaning-demo/protocols/cleaning.py @@ -0,0 +1,100 @@ +from enum import Enum +from typing import List + +from nexus import Context, Model, Protocol + + +PROTOCOL_NAME = "cleaning" +PROTOCOL_VERSION = "0.1.0" + + +class Service(str, Enum): + Floor = "floor" + Window = "window" + Laundry = "laundry" + Iron = "iron" + Bathroom = "bathroom" + + +class Availability(Model): + address: int + max_distance: int + time_start: int + time_end: int + services: List[Service] + min_hourly_price: float + + +class ServiceRequest(Model): + address: int + time_start: int + duration: int + services: List[Service] + max_price: float + + +class ServiceResponse(Model): + accept: bool + price: float + + +class ServiceBooking(Model): + address: str + time_start: int + duration: int + services: List[Service] + price: float + + +class BookingResponse(Model): + success: bool + + +cleaning_proto = Protocol(name=PROTOCOL_NAME, version=PROTOCOL_VERSION) + + +def in_service_region(address: int, availability: Availability) -> bool: + return abs(availability.address - address) <= availability.max_distance + + +@cleaning_proto.on_message(model=ServiceRequest, replies=ServiceResponse) +async def handle_query_request(ctx: Context, sender: str, msg: ServiceRequest): + + availability = Availability(**ctx.storage.get("availability")) + markup = float(ctx.storage.get("markup")) + + if ( + set(msg.services) <= set(availability.services) + and in_service_region(msg.address, availability) + and availability.time_start <= msg.time_start + and availability.time_end >= msg.time_start + msg.duration + and availability.min_hourly_price * msg.duration < msg.max_price + ): + accept = True + price = markup * availability.min_hourly_price * msg.duration + else: + accept = False + price = 0 + + print(f"Query: {msg}. Availability: {availability}.") + + await ctx.send(sender, ServiceResponse(accept=accept, price=price)) + + +@cleaning_proto.on_message(model=ServiceBooking, replies=BookingResponse) +async def handle_book_request(ctx: Context, sender: str, msg: ServiceBooking): + + availability = Availability(**ctx.storage.get("availability")) + + if ( + set(msg.services) <= set(availability.services) + and availability.time_start <= msg.time_start + and availability.time_end >= msg.time_start + msg.duration + and msg.price <= availability.min_hourly_price * msg.duration + ): + success = True + else: + success = False + + # send the response + await ctx.send(sender, BookingResponse(success=success)) diff --git a/examples/10-cleaning-demo/user.py b/examples/10-cleaning-demo/user.py new file mode 100644 index 00000000..ae34ff3c --- /dev/null +++ b/examples/10-cleaning-demo/user.py @@ -0,0 +1,73 @@ +from protocols.cleaning import ( + ServiceBooking, + BookingResponse, + Service, + ServiceRequest, + ServiceResponse, +) + +from nexus import Agent, Context +from nexus.setup import fund_agent_if_low + + +CLEANER_ADDRESS = "agent1q0g3v3masp6fg2dpfxtfxu9ysuzeusgpdmcz5whv30jm7m7u2vt2zms6p2z" + +user = Agent( + name="user", + port=8000, + seed="user secret seed phrase", + endpoint="http://127.0.0.1:8000/submit", +) + +fund_agent_if_low(user.wallet.address()) + +request = ServiceRequest( + address=17, + time_start=12, + duration=4, + services=[Service.Window, Service.Laundry], + max_price=60, +) + +markdown = 0.8 + + +@user.on_interval(period=3.0) +async def interval(ctx: Context): + ctx.storage.set("markdown", markdown) + completed = ctx.storage.get("completed") + + if not completed: + await ctx.send(CLEANER_ADDRESS, request) + + +@user.on_message(ServiceResponse, replies=ServiceBooking) +async def handle_query_response(ctx: Context, sender: str, msg: ServiceResponse): + markdown = ctx.storage.get("markdown") + if msg.accept: + print("Cleaner is available, attempting to book now") + booking = ServiceBooking( + address=request.address, + time_start=request.time_start, + duration=request.duration, + services=request.services, + price=markdown * msg.price, + ) + await ctx.send(sender, booking) + else: + print("Cleaner is not available - nothing more to do") + ctx.storage.set("completed", True) + + +@user.on_message(BookingResponse, replies=set()) +async def handle_book_response(ctx: Context, _sender: str, msg: BookingResponse): + if msg.success: + print("Booking was successful") + else: + print("Booking was UNSUCCESSFUL") + + ctx.storage.set("completed", True) + + +if __name__ == "__main__": + user.run() From a69aa61b9f8e40b3eef18a8fd28536b726aefc08 Mon Sep 17 00:00:00 2001 From: James Riehl Date: Thu, 24 Nov 2022 14:54:48 +0000 Subject: [PATCH 08/41] chore: fix linting errors --- examples/10-cleaning-demo/cleaner.py | 10 +++++----- examples/10-cleaning-demo/protocols/cleaning.py | 17 +++++++---------- examples/10-cleaning-demo/user.py | 6 +++--- src/nexus/protocol.py | 2 +- 4 files changed, 16 insertions(+), 19 deletions(-) diff --git a/examples/10-cleaning-demo/cleaner.py b/examples/10-cleaning-demo/cleaner.py index 02f66fe7..965db777 100644 --- a/examples/10-cleaning-demo/cleaner.py +++ b/examples/10-cleaning-demo/cleaner.py @@ -23,15 +23,15 @@ max_distance=10, time_start=10, time_end=18, - services=[Service.Floor, Service.Window, Service.Laundry], + services=[Service.FLOOR, Service.WINDOW, Service.LAUNDRY], min_hourly_price=12, ) -markup = 1.1 +MARKUP = 1.1 -cleaner._storage.set( +cleaner._storage.set( # pylint: disable=protected-access "availability", availability.dict() -) # pylint: disable=protected-access -cleaner._storage.set("markup", markup) +) +cleaner._storage.set("markup", MARKUP) # pylint: disable=protected-access if __name__ == "__main__": cleaner.run() diff --git a/examples/10-cleaning-demo/protocols/cleaning.py b/examples/10-cleaning-demo/protocols/cleaning.py index 7e91fdee..164528aa 100644 --- a/examples/10-cleaning-demo/protocols/cleaning.py +++ b/examples/10-cleaning-demo/protocols/cleaning.py @@ -9,11 +9,11 @@ class Service(str, Enum): - Floor = "floor" - Window = "window" - Laundry = "laundry" - Iron = "iron" - Bathroom = "bathroom" + FLOOR = "floor" + WINDOW = "window" + LAUNDRY = "laundry" + IRON = "iron" + BATHROOM = "bathroom" class Availability(Model): @@ -86,15 +86,12 @@ async def handle_book_request(ctx: Context, sender: str, msg: ServiceBooking): availability = Availability(**ctx.storage.get("availability")) - if ( + success = ( set(msg.services) <= set(availability.services) and availability.time_start <= msg.time_start and availability.time_end >= msg.time_start + msg.duration and msg.price <= availability.min_hourly_price * msg.duration - ): - success = True - else: - success = False + ) # send the response await ctx.send(sender, BookingResponse(success=success)) diff --git a/examples/10-cleaning-demo/user.py b/examples/10-cleaning-demo/user.py index ae34ff3c..70cd292d 100644 --- a/examples/10-cleaning-demo/user.py +++ b/examples/10-cleaning-demo/user.py @@ -25,16 +25,16 @@ address=17, time_start=12, duration=4, - services=[Service.Window, Service.Laundry], + services=[Service.WINDOW, Service.LAUNDRY], max_price=60, ) -markdown = 0.8 +MARKDOWN = 0.8 @user.on_interval(period=3.0) async def interval(ctx: Context): - ctx.storage.set("markdown", markdown) + ctx.storage.set("markdown", MARKDOWN) completed = ctx.storage.get("completed") if not completed: diff --git a/src/nexus/protocol.py b/src/nexus/protocol.py index 2e447cd3..26f73837 100644 --- a/src/nexus/protocol.py +++ b/src/nexus/protocol.py @@ -58,7 +58,7 @@ def canonical_name(self): @property def digest(self): - # assert self._digest != "", "Protocol digest empty" + assert self._digest != "", "Protocol digest empty" return self._digest def on_interval(self, period: float): From bfe9935c2a04aa5228170083c14aeddff13fa471 Mon Sep 17 00:00:00 2001 From: James Riehl Date: Thu, 24 Nov 2022 16:10:50 +0000 Subject: [PATCH 09/41] chore: uncomment empty digest assertion --- src/nexus/protocol.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/nexus/protocol.py b/src/nexus/protocol.py index 2e447cd3..26f73837 100644 --- a/src/nexus/protocol.py +++ b/src/nexus/protocol.py @@ -58,7 +58,7 @@ def canonical_name(self): @property def digest(self): - # assert self._digest != "", "Protocol digest empty" + assert self._digest != "", "Protocol digest empty" return self._digest def on_interval(self, period: float): From 283aa9083d7ab806526b28f4d1e8317ef1a9149f Mon Sep 17 00:00:00 2001 From: James Riehl Date: Thu, 24 Nov 2022 17:34:48 +0000 Subject: [PATCH 10/41] feat: interval protocol only contains agent registrations --- src/nexus/agent.py | 54 ++++++++++++++----------------------------- src/nexus/protocol.py | 8 +++---- 2 files changed, 21 insertions(+), 41 deletions(-) diff --git a/src/nexus/agent.py b/src/nexus/agent.py index e6fcbba5..574e4ff8 100644 --- a/src/nexus/agent.py +++ b/src/nexus/agent.py @@ -89,8 +89,6 @@ def __init__( # register with the dispatcher self._dispatcher.register(self.address, self) - self._create_interval_tasks() - # start the background message queue processor task = self._loop.create_task(self._process_message_queue()) self._background_tasks.add(task) @@ -180,19 +178,7 @@ def get_registration_sequence(self) -> int: return sequence def on_interval(self, period: float): - def decorator_on_interval(func: IntervalCallback): - @functools.wraps(func) - def handler(*args, **kwargs): - return func(*args, **kwargs) - - # register the interval with the agent - task = self._loop.create_task(_run_interval(func, self._ctx, period)) - self._background_tasks.add(task) - task.add_done_callback(self._background_tasks.discard) - - return handler - - return decorator_on_interval + return self._protocol.on_interval(period) def on_message( self, model: Model, replies: Optional[Union[Model, Set[Model]]] = None @@ -201,37 +187,33 @@ def on_message( def include(self, protocol: Protocol): for func, period in protocol.intervals: - self._protocol.intervals.append((func, period)) + task = self._loop.create_task(_run_interval(func, self._ctx, period)) + self._background_tasks.add(task) + task.add_done_callback(self._background_tasks.discard) for schema_digest in protocol.models: - if schema_digest in self._protocol.models: + if schema_digest in self._models: raise RuntimeError("Unable to register duplicate model") - if schema_digest in self._protocol.message_handlers: + if schema_digest in self._message_handlers: raise RuntimeError("Unable to register duplicate message handler") if schema_digest not in protocol.message_handlers: raise RuntimeError("Unable to lookup up message handler in protocol") # include the message handlers from the protocol - self._protocol.add_message_handler( - protocol.models[schema_digest], - protocol.message_handlers[schema_digest], - set(protocol.replies[schema_digest].values()), - ) + self._models[schema_digest] = protocol.models[schema_digest] + self._message_handlers[schema_digest] = protocol.message_handlers[ + schema_digest + ] + self._replies[schema_digest] = protocol.replies[schema_digest] self.protocols[protocol.canonical_name] = protocol.digest - # Update the internal protocol digest in the list of supported protocols - self.protocols[self._protocol.canonical_name] = self._protocol.digest - - def _create_interval_tasks(self): - for func, period in self._protocol.intervals: - task = self._loop.create_task(_run_interval(func, self._ctx, period)) - self._background_tasks.add(task) - task.add_done_callback(self._background_tasks.discard) - async def handle_message(self, sender, schema_digest: str, message: Any): await self._message_queue.put((schema_digest, sender, message)) def run(self): + # register the internal agent protocol + self.include(self._protocol) + # start the contract registration update loop self._loop.create_task( _run_interval(self.register, self._ctx, REG_UPDATE_INTERVAL_SECONDS) @@ -245,7 +227,7 @@ async def _process_message_queue(self): schema_digest, sender, message = await self._message_queue.get() # lookup the model definition - model_class = self._protocol.models.get(schema_digest) + model_class = self._models.get(schema_digest) if model_class is None: continue @@ -260,14 +242,12 @@ async def _process_message_queue(self): self._identity, self._wallet, self._ledger, - self._protocol.replies, + self._replies, MsgDigest(message=message, schema_digest=schema_digest), ) # attempt to find the handler - handler: MessageCallback = self._protocol.message_handlers.get( - schema_digest - ) + handler: MessageCallback = self._message_handlers.get(schema_digest) if handler is not None: await handler(context, sender, recovered) diff --git a/src/nexus/protocol.py b/src/nexus/protocol.py index 26f73837..bde30c7f 100644 --- a/src/nexus/protocol.py +++ b/src/nexus/protocol.py @@ -91,15 +91,15 @@ def handler(*args, **kwargs): def add_message_handler( self, model: Model, func: MessageCallback, replies: Union[Model, Set[Model]] ): - schema_digest = Model.build_schema_digest(model) + model_digest = Model.build_schema_digest(model) # update the model database - self._models[schema_digest] = model - self._message_handlers[schema_digest] = func + self._models[model_digest] = model + self._message_handlers[model_digest] = func if replies is not None: if not isinstance(replies, set): replies = {replies} - self._replies[schema_digest] = { + self._replies[model_digest] = { Model.build_schema_digest(reply): reply for reply in replies } From 8690ce9dce1d47ff6ab24d8e8886e6de93a802cf Mon Sep 17 00:00:00 2001 From: James Riehl Date: Fri, 25 Nov 2022 10:48:43 +0000 Subject: [PATCH 11/41] feat: add interval messages to protocol --- src/nexus/agent.py | 6 +++++- src/nexus/protocol.py | 35 ++++++++++++++++++++++++++++++----- 2 files changed, 35 insertions(+), 6 deletions(-) diff --git a/src/nexus/agent.py b/src/nexus/agent.py index 574e4ff8..d71e36f5 100644 --- a/src/nexus/agent.py +++ b/src/nexus/agent.py @@ -210,7 +210,7 @@ def include(self, protocol: Protocol): async def handle_message(self, sender, schema_digest: str, message: Any): await self._message_queue.put((schema_digest, sender, message)) - def run(self): + def setup(self): # register the internal agent protocol self.include(self._protocol) @@ -219,6 +219,8 @@ def run(self): _run_interval(self.register, self._ctx, REG_UPDATE_INTERVAL_SECONDS) ) + def run(self): + self.setup() self._loop.run_until_complete(self._server.serve()) async def _process_message_queue(self): @@ -264,4 +266,6 @@ def add(self, agent: Agent): self._agents.append(agent) def run(self): + for agent in self._agents: + agent.setup() self._loop.run_until_complete(self._server.serve()) diff --git a/src/nexus/protocol.py b/src/nexus/protocol.py index bde30c7f..e03818b5 100644 --- a/src/nexus/protocol.py +++ b/src/nexus/protocol.py @@ -14,6 +14,7 @@ class Protocol: def __init__(self, name: Optional[str] = None, version: Optional[str] = None): self._intervals = [] + self._interval_messages = {} self._message_handlers = {} self._models = {} self._replies = {} @@ -61,19 +62,39 @@ def digest(self): assert self._digest != "", "Protocol digest empty" return self._digest - def on_interval(self, period: float): + def on_interval( + self, period: float, messages: Optional[Union[Model, Set[Model]]] = None + ): def decorator_on_interval(func: IntervalCallback): @functools.wraps(func) def handler(*args, **kwargs): return func(*args, **kwargs) - # store the interval handler for later - self._intervals.append((func, period)) + self.add_interval_handler(period, func, messages) return handler return decorator_on_interval + def add_interval_handler( + self, + period: float, + func: IntervalCallback, + messages: Optional[Union[Model, Set[Model]]], + ): + + # store the interval handler for later + self._intervals.append((func, period)) + if messages is not None: + if not isinstance(messages, set): + messages = {messages} + for message in messages: + message_digest = Model.build_schema_digest(message) + self._interval_messages[message_digest] = message + + self.spec.path(path=message.__name__, operations=message.dict()) + self._update_schema_digest() + def on_message( self, model: Model, replies: Optional[Union[Model, Set[Model]]] = None ): @@ -89,7 +110,10 @@ def handler(*args, **kwargs): return decorator_on_message def add_message_handler( - self, model: Model, func: MessageCallback, replies: Union[Model, Set[Model]] + self, + model: Model, + func: MessageCallback, + replies: Optional[Union[Model, Set[Model]]], ): model_digest = Model.build_schema_digest(model) @@ -112,7 +136,8 @@ def add_message_handler( self._update_schema_digest() def _update_schema_digest(self): - sorted_schema_digests = sorted(list(self._models.keys())) + all_models = self._models | self._interval_messages + sorted_schema_digests = sorted(list(all_models.keys())) hasher = hashlib.sha256() for digest in sorted_schema_digests: hasher.update(bytes.fromhex(digest)) From 9a02d2f4aea6ef67918863fdd24e5b8efb605516 Mon Sep 17 00:00:00 2001 From: James Riehl Date: Fri, 25 Nov 2022 10:54:14 +0000 Subject: [PATCH 12/41] fix: add messages to agent decorator --- examples/04-booking-protocol/main.py | 2 +- examples/05-booking-protocol-split/main.py | 2 +- src/nexus/agent.py | 6 ++++-- src/nexus/protocol.py | 2 +- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/examples/04-booking-protocol/main.py b/examples/04-booking-protocol/main.py index 375447cd..75e8e69c 100644 --- a/examples/04-booking-protocol/main.py +++ b/examples/04-booking-protocol/main.py @@ -28,7 +28,7 @@ class BookTableResponse(Model): restuarant = Agent(name="restuarant") -@user.on_interval(period=3.0) +@user.on_interval(period=3.0, messages=QueryTableRequest) async def interval(ctx: Context): started = ctx.storage.get("started") diff --git a/examples/05-booking-protocol-split/main.py b/examples/05-booking-protocol-split/main.py index 41e402f5..d0df7261 100644 --- a/examples/05-booking-protocol-split/main.py +++ b/examples/05-booking-protocol-split/main.py @@ -16,7 +16,7 @@ user = Agent(name="user") -@user.on_interval(period=3.0) +@user.on_interval(period=3.0, messages=QueryTableRequest) async def interval(ctx: Context): started = ctx.storage.get("started") diff --git a/src/nexus/agent.py b/src/nexus/agent.py index d71e36f5..3d16c675 100644 --- a/src/nexus/agent.py +++ b/src/nexus/agent.py @@ -177,8 +177,10 @@ def get_registration_sequence(self) -> int: return sequence - def on_interval(self, period: float): - return self._protocol.on_interval(period) + def on_interval( + self, period: float, messages: Optional[Union[Model, Set[Model]]] = None + ): + return self._protocol.on_interval(period, messages) def on_message( self, model: Model, replies: Optional[Union[Model, Set[Model]]] = None diff --git a/src/nexus/protocol.py b/src/nexus/protocol.py index e03818b5..c1ba0178 100644 --- a/src/nexus/protocol.py +++ b/src/nexus/protocol.py @@ -92,7 +92,7 @@ def add_interval_handler( message_digest = Model.build_schema_digest(message) self._interval_messages[message_digest] = message - self.spec.path(path=message.__name__, operations=message.dict()) + self.spec.path(path=message.__name__, operations={}) self._update_schema_digest() def on_message( From fcc2ba32ed54539f6fead66eff5fc393b9a78dbd Mon Sep 17 00:00:00 2001 From: James Riehl Date: Fri, 25 Nov 2022 12:00:21 +0000 Subject: [PATCH 13/41] chore: fix types for set union --- src/nexus/protocol.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/nexus/protocol.py b/src/nexus/protocol.py index c1ba0178..33829289 100644 --- a/src/nexus/protocol.py +++ b/src/nexus/protocol.py @@ -136,8 +136,10 @@ def add_message_handler( self._update_schema_digest() def _update_schema_digest(self): - all_models = self._models | self._interval_messages - sorted_schema_digests = sorted(list(all_models.keys())) + all_model_digests = set(self._models.keys()) | set( + self._interval_messages.keys() + ) + sorted_schema_digests = sorted(list(all_model_digests)) hasher = hashlib.sha256() for digest in sorted_schema_digests: hasher.update(bytes.fromhex(digest)) From ce78d4ef7c9cea31be4ff00581d040b57b293f32 Mon Sep 17 00:00:00 2001 From: James Riehl Date: Fri, 25 Nov 2022 13:19:12 +0000 Subject: [PATCH 14/41] chore: make private methods private --- src/nexus/protocol.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/nexus/protocol.py b/src/nexus/protocol.py index 33829289..66fb765c 100644 --- a/src/nexus/protocol.py +++ b/src/nexus/protocol.py @@ -70,13 +70,13 @@ def decorator_on_interval(func: IntervalCallback): def handler(*args, **kwargs): return func(*args, **kwargs) - self.add_interval_handler(period, func, messages) + self._add_interval_handler(period, func, messages) return handler return decorator_on_interval - def add_interval_handler( + def _add_interval_handler( self, period: float, func: IntervalCallback, @@ -103,13 +103,13 @@ def decorator_on_message(func: MessageCallback): def handler(*args, **kwargs): return func(*args, **kwargs) - self.add_message_handler(model, func, replies) + self._add_message_handler(model, func, replies) return handler return decorator_on_message - def add_message_handler( + def _add_message_handler( self, model: Model, func: MessageCallback, From d872e6a6766d53d50dd0ce74b76a8312072c9caf Mon Sep 17 00:00:00 2001 From: James Riehl Date: Fri, 25 Nov 2022 13:48:35 +0000 Subject: [PATCH 15/41] feat: validate outgoing interval messages --- examples/09-booking-protocol-demo/user.py | 2 +- src/nexus/agent.py | 18 +++++++++++++++--- src/nexus/context.py | 10 ++++++++++ src/nexus/protocol.py | 11 +++++++---- 4 files changed, 33 insertions(+), 8 deletions(-) diff --git a/examples/09-booking-protocol-demo/user.py b/examples/09-booking-protocol-demo/user.py index 7066e998..10c80d75 100644 --- a/examples/09-booking-protocol-demo/user.py +++ b/examples/09-booking-protocol-demo/user.py @@ -26,7 +26,7 @@ ) -@user.on_interval(period=3.0) +@user.on_interval(period=3.0, messages=QueryTableRequest) async def interval(ctx: Context): completed = ctx.storage.get("completed") diff --git a/src/nexus/agent.py b/src/nexus/agent.py index 3d16c675..828fcdb5 100644 --- a/src/nexus/agent.py +++ b/src/nexus/agent.py @@ -65,6 +65,7 @@ def __init__( self._storage = KeyValueStore(self.address[0:16]) self._models = {} self._replies = {} + self._interval_messages = {} self._message_handlers = {} self._inbox = {} self._ctx = Context( @@ -193,6 +194,11 @@ def include(self, protocol: Protocol): self._background_tasks.add(task) task.add_done_callback(self._background_tasks.discard) + for schema_digest in protocol.interval_messages: + self._interval_messages[schema_digest] = protocol.interval_messages[ + schema_digest + ] + for schema_digest in protocol.models: if schema_digest in self._models: raise RuntimeError("Unable to register duplicate model") @@ -206,7 +212,10 @@ def include(self, protocol: Protocol): self._message_handlers[schema_digest] = protocol.message_handlers[ schema_digest ] - self._replies[schema_digest] = protocol.replies[schema_digest] + if schema_digest in protocol.replies: + self._replies[schema_digest] = protocol.replies[schema_digest] + + if protocol.digest is not None: self.protocols[protocol.canonical_name] = protocol.digest async def handle_message(self, sender, schema_digest: str, message: Any): @@ -246,8 +255,11 @@ async def _process_message_queue(self): self._identity, self._wallet, self._ledger, - self._replies, - MsgDigest(message=message, schema_digest=schema_digest), + replies=self._replies, + interval_messages=self._interval_messages, + message_received=MsgDigest( + message=message, schema_digest=schema_digest + ), ) # attempt to find the handler diff --git a/src/nexus/context.py b/src/nexus/context.py index 7ead4dff..bd127af7 100644 --- a/src/nexus/context.py +++ b/src/nexus/context.py @@ -35,6 +35,7 @@ def __init__( wallet: LocalWallet, ledger: LedgerClient, replies: Optional[Dict[str, Set[str]]] = None, + interval_messages: Optional[Dict[str, Set[str]]] = None, message_received: Optional[MsgDigest] = None, ): self.storage = storage @@ -45,6 +46,7 @@ def __init__( self._resolver = resolve self._identity = identity self._replies = replies + self._interval_messages = interval_messages self._message_received = message_received @property @@ -73,6 +75,14 @@ async def send(self, destination: str, message: Model): ) return + # check if this message is a valid interval message + if self._message_received is None and self._interval_messages is not None: + if schema_digest not in self._interval_messages: + logging.exception( + f"Outgoing message {message} is not a valid interval message" + ) + return + # handle local dispatch of messages if dispatcher.contains(destination): await dispatcher.dispatch( diff --git a/src/nexus/protocol.py b/src/nexus/protocol.py index 66fb765c..9c4af0cb 100644 --- a/src/nexus/protocol.py +++ b/src/nexus/protocol.py @@ -41,6 +41,10 @@ def models(self): def replies(self): return self._replies + @property + def interval_messages(self): + return self._interval_messages + @property def message_handlers(self): return self._message_handlers @@ -59,7 +63,6 @@ def canonical_name(self): @property def digest(self): - assert self._digest != "", "Protocol digest empty" return self._digest def on_interval( @@ -93,7 +96,7 @@ def _add_interval_handler( self._interval_messages[message_digest] = message self.spec.path(path=message.__name__, operations={}) - self._update_schema_digest() + self._update_digest() def on_message( self, model: Model, replies: Optional[Union[Model, Set[Model]]] = None @@ -133,9 +136,9 @@ def _add_message_handler( post=dict(replies=[reply.__name__ for reply in replies]) ), ) - self._update_schema_digest() + self._update_digest() - def _update_schema_digest(self): + def _update_digest(self): all_model_digests = set(self._models.keys()) | set( self._interval_messages.keys() ) From 6e1dab71d903517a601c6ca0ed101289cd734a1d Mon Sep 17 00:00:00 2001 From: James Riehl Date: Fri, 25 Nov 2022 15:00:37 +0000 Subject: [PATCH 16/41] chore: update demo and contract --- examples/10-cleaning-demo/protocols/cleaning.py | 9 +++++++-- examples/10-cleaning-demo/user.py | 3 ++- src/nexus/config.py | 2 +- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/examples/10-cleaning-demo/protocols/cleaning.py b/examples/10-cleaning-demo/protocols/cleaning.py index 164528aa..aa987708 100644 --- a/examples/10-cleaning-demo/protocols/cleaning.py +++ b/examples/10-cleaning-demo/protocols/cleaning.py @@ -72,11 +72,11 @@ async def handle_query_request(ctx: Context, sender: str, msg: ServiceRequest): ): accept = True price = markup * availability.min_hourly_price * msg.duration + print(f"I am available! Proposing price: {price}.") else: accept = False price = 0 - - print(f"Query: {msg}. Availability: {availability}.") + print("I am not available. Declining request.") await ctx.send(sender, ServiceResponse(accept=accept, price=price)) @@ -93,5 +93,10 @@ async def handle_book_request(ctx: Context, sender: str, msg: ServiceBooking): and msg.price <= availability.min_hourly_price * msg.duration ) + if success: + availability.time_start = msg.time_start + msg.duration + ctx.storage.set("availability", availability.dict()) + print(f"Accepted task and updated availability.") + # send the response await ctx.send(sender, BookingResponse(success=success)) diff --git a/examples/10-cleaning-demo/user.py b/examples/10-cleaning-demo/user.py index 70cd292d..83e921a6 100644 --- a/examples/10-cleaning-demo/user.py +++ b/examples/10-cleaning-demo/user.py @@ -32,12 +32,13 @@ MARKDOWN = 0.8 -@user.on_interval(period=3.0) +@user.on_interval(period=3.0, messages=ServiceRequest) async def interval(ctx: Context): ctx.storage.set("markdown", MARKDOWN) completed = ctx.storage.get("completed") if not completed: + print(f"Requesting cleaning service: {request}") await ctx.send(CLEANER_ADDRESS, request) diff --git a/src/nexus/config.py b/src/nexus/config.py index 18a4da8c..422b438a 100644 --- a/src/nexus/config.py +++ b/src/nexus/config.py @@ -11,7 +11,7 @@ class AgentNetwork(Enum): AGENT_PREFIX = "agent" LEDGER_PREFIX = "fetch" -CONTRACT_ALMANAC = "fetch1fvdmhy3y4hae35qsxgxjfy3nafvqe8grzclpeauvv2n3pt9l0uqqg375h8" +CONTRACT_ALMANAC = "fetch1tjagw8g8nn4cwuw00cf0m5tl4l6wfw9c0ue507fhx9e3yrsck8zs0l3q4w" REGISTRATION_FEE = 500000000000000000 REGISTRATION_DENOM = "atestfet" REG_UPDATE_INTERVAL_SECONDS = 60 From a01fe5de15204e36b5fbeee36a3fd108127aec21 Mon Sep 17 00:00:00 2001 From: James Riehl Date: Fri, 25 Nov 2022 15:48:47 +0000 Subject: [PATCH 17/41] fix: include interval messages in default agent context --- src/nexus/agent.py | 2 ++ src/nexus/config.py | 2 +- src/nexus/context.py | 5 +++-- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/nexus/agent.py b/src/nexus/agent.py index 828fcdb5..bd704b0f 100644 --- a/src/nexus/agent.py +++ b/src/nexus/agent.py @@ -76,6 +76,8 @@ def __init__( self._identity, self._wallet, self._ledger, + replies=self._replies, + interval_messages=self._interval_messages, ) self._dispatcher = dispatcher self._message_queue = asyncio.Queue() diff --git a/src/nexus/config.py b/src/nexus/config.py index 18a4da8c..422b438a 100644 --- a/src/nexus/config.py +++ b/src/nexus/config.py @@ -11,7 +11,7 @@ class AgentNetwork(Enum): AGENT_PREFIX = "agent" LEDGER_PREFIX = "fetch" -CONTRACT_ALMANAC = "fetch1fvdmhy3y4hae35qsxgxjfy3nafvqe8grzclpeauvv2n3pt9l0uqqg375h8" +CONTRACT_ALMANAC = "fetch1tjagw8g8nn4cwuw00cf0m5tl4l6wfw9c0ue507fhx9e3yrsck8zs0l3q4w" REGISTRATION_FEE = 500000000000000000 REGISTRATION_DENOM = "atestfet" REG_UPDATE_INTERVAL_SECONDS = 60 diff --git a/src/nexus/context.py b/src/nexus/context.py index bd127af7..48917300 100644 --- a/src/nexus/context.py +++ b/src/nexus/context.py @@ -71,7 +71,8 @@ async def send(self, destination: str, message: Model): # ensure the reply is valid if schema_digest not in self._replies[received.schema_digest]: logging.exception( - f"Outgoing message {message} is not a valid reply to {received.message}" + f"Outgoing message {type(message)} " + f"is not a valid reply to {received.message}" ) return @@ -79,7 +80,7 @@ async def send(self, destination: str, message: Model): if self._message_received is None and self._interval_messages is not None: if schema_digest not in self._interval_messages: logging.exception( - f"Outgoing message {message} is not a valid interval message" + f"Outgoing message {type(message)} is not a valid interval message" ) return From 2b346175e49e923d22ea3762d0c7f141dd33c587 Mon Sep 17 00:00:00 2001 From: James Riehl Date: Tue, 29 Nov 2022 11:22:12 +0000 Subject: [PATCH 18/41] fix: message check and agent addresses --- examples/07-remote-agents/agent1.py | 4 ++-- examples/07-remote-agents/agent2.py | 4 ++-- src/nexus/context.py | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/examples/07-remote-agents/agent1.py b/examples/07-remote-agents/agent1.py index 4490ab73..a8c633d0 100644 --- a/examples/07-remote-agents/agent1.py +++ b/examples/07-remote-agents/agent1.py @@ -6,8 +6,8 @@ class Message(Model): message: str -AGENT1_ADDRESS = "agent1qwpdqyxwdxmx9ar8gmp02x7n85pjucams0q0m2l45zlmykrzvnpnj5l06gf" -AGENT2_ADDRESS = "agent1q2sec3utj4a8xl8le8x2dy90f33fnlunaatxamjpepz0zk99qqttj97526g" +AGENT1_ADDRESS = "agent1qv2l7qzcd2g2rcv2p93tqflrcaq5dk7c2xc7fcnfq3s37zgkhxjmq5mfyvz" +AGENT2_ADDRESS = "agent1qv73me5ql7kl30t0grehalj0aau0l4hpthp4m5q9v4qk2hz8h63vzpgyadp" agent = Agent( name="alice", diff --git a/examples/07-remote-agents/agent2.py b/examples/07-remote-agents/agent2.py index 9cff3a0b..527e4ad6 100644 --- a/examples/07-remote-agents/agent2.py +++ b/examples/07-remote-agents/agent2.py @@ -6,8 +6,8 @@ class Message(Model): message: str -AGENT1_ADDRESS = "agent1qwpdqyxwdxmx9ar8gmp02x7n85pjucams0q0m2l45zlmykrzvnpnj5l06gf" -AGENT2_ADDRESS = "agent1q2sec3utj4a8xl8le8x2dy90f33fnlunaatxamjpepz0zk99qqttj97526g" +AGENT1_ADDRESS = "agent1qv2l7qzcd2g2rcv2p93tqflrcaq5dk7c2xc7fcnfq3s37zgkhxjmq5mfyvz" +AGENT2_ADDRESS = "agent1qv73me5ql7kl30t0grehalj0aau0l4hpthp4m5q9v4qk2hz8h63vzpgyadp" agent = Agent( name="bob", diff --git a/src/nexus/context.py b/src/nexus/context.py index 48917300..d995d5ab 100644 --- a/src/nexus/context.py +++ b/src/nexus/context.py @@ -65,7 +65,7 @@ async def send(self, destination: str, message: Model): schema_digest = Model.build_schema_digest(message) # check if this message is a reply - if self._message_received is not None and self._replies is not None: + if self._message_received is not None and self._replies: received = self._message_received if received.schema_digest in self._replies: # ensure the reply is valid @@ -77,7 +77,7 @@ async def send(self, destination: str, message: Model): return # check if this message is a valid interval message - if self._message_received is None and self._interval_messages is not None: + if self._message_received is None and self._interval_messages: if schema_digest not in self._interval_messages: logging.exception( f"Outgoing message {type(message)} is not a valid interval message" From 15ea924e8b1692b506e7cbecadc995d648b5816a Mon Sep 17 00:00:00 2001 From: James Riehl Date: Tue, 29 Nov 2022 13:57:53 +0000 Subject: [PATCH 19/41] feat: start tortoise integration --- .../protocols/cleaning/__init__.py | 0 .../{cleaning.py => cleaning/models.py} | 17 ++-- poetry.lock | 79 ++++++++++++++++++- pyproject.toml | 1 + 4 files changed, 89 insertions(+), 8 deletions(-) create mode 100644 examples/10-cleaning-demo/protocols/cleaning/__init__.py rename examples/10-cleaning-demo/protocols/{cleaning.py => cleaning/models.py} (86%) diff --git a/examples/10-cleaning-demo/protocols/cleaning/__init__.py b/examples/10-cleaning-demo/protocols/cleaning/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/examples/10-cleaning-demo/protocols/cleaning.py b/examples/10-cleaning-demo/protocols/cleaning/models.py similarity index 86% rename from examples/10-cleaning-demo/protocols/cleaning.py rename to examples/10-cleaning-demo/protocols/cleaning/models.py index aa987708..afb3a663 100644 --- a/examples/10-cleaning-demo/protocols/cleaning.py +++ b/examples/10-cleaning-demo/protocols/cleaning/models.py @@ -1,6 +1,9 @@ from enum import Enum from typing import List +from tortoise import fields, models +from tortoise.contrib.postgres.fields import ArrayField + from nexus import Context, Model, Protocol @@ -16,13 +19,13 @@ class Service(str, Enum): BATHROOM = "bathroom" -class Availability(Model): - address: int - max_distance: int - time_start: int - time_end: int - services: List[Service] - min_hourly_price: float +class Availability(models.Model): + address = fields.IntField(default=0) + max_distance = fields.IntField(default=10) + time_start: fields.DatetimeField() + time_end: fields.DatetimeField(default=24) + services: ArrayField(element_type=str) + min_hourly_price: fields.FloatField(default=0.0) class ServiceRequest(Model): diff --git a/poetry.lock b/poetry.lock index 832ce6a0..521f3483 100644 --- a/poetry.lock +++ b/poetry.lock @@ -29,6 +29,17 @@ python-versions = ">=3.7" [package.dependencies] frozenlist = ">=1.1.0" +[[package]] +name = "aiosqlite" +version = "0.17.0" +description = "asyncio bridge to the standard sqlite3 module" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +typing_extensions = ">=3.7.2" + [[package]] name = "apispec" version = "6.0.2" @@ -452,6 +463,14 @@ category = "dev" optional = false python-versions = "*" +[[package]] +name = "iso8601" +version = "1.1.0" +description = "Simple module to parse ISO 8601 dates" +category = "main" +optional = false +python-versions = ">=3.6.2,<4.0" + [[package]] name = "isort" version = "5.10.1" @@ -698,6 +717,14 @@ python-versions = ">=3.6.8" [package.extras] diagrams = ["jinja2", "railroad-diagrams"] +[[package]] +name = "pypika-tortoise" +version = "0.1.6" +description = "Forked from pypika and streamline just for tortoise-orm" +category = "main" +optional = false +python-versions = ">=3.7,<4.0" + [[package]] name = "pyrsistent" version = "0.19.2" @@ -726,6 +753,14 @@ tomli = ">=1.0.0" [package.extras] testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] +[[package]] +name = "pytz" +version = "2022.6" +description = "World timezone definitions, modern and historical" +category = "main" +optional = false +python-versions = "*" + [[package]] name = "requests" version = "2.28.1" @@ -779,6 +814,28 @@ category = "dev" optional = false python-versions = ">=3.6,<4.0" +[[package]] +name = "tortoise-orm" +version = "0.19.2" +description = "Easy async ORM for python, built with relations in mind" +category = "main" +optional = false +python-versions = ">=3.7,<4.0" + +[package.dependencies] +aiosqlite = ">=0.16.0,<0.18.0" +iso8601 = ">=1.0.2,<2.0.0" +pypika-tortoise = ">=0.1.6,<0.2.0" +pytz = "*" + +[package.extras] +accel = ["ciso8601", "orjson", "uvloop"] +aiomysql = ["aiomysql"] +asyncmy = ["asyncmy (>=0.2.5,<0.3.0)"] +asyncodbc = ["asyncodbc (>=0.1.1,<0.2.0)"] +asyncpg = ["asyncpg"] +psycopg = ["psycopg[binary,pool]"] + [[package]] name = "typing-extensions" version = "4.3.0" @@ -869,7 +926,7 @@ testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools" [metadata] lock-version = "1.1" python-versions = "^3.8" -content-hash = "806d7c2d416c9b5209f806483bf96a49f75ec4afb0c9272af8f6db461180265b" +content-hash = "2f0e71a168e2d79d932ac249162e8be9752d68340c2d278076da35042b17b86e" [metadata.files] aiohttp = [ @@ -965,6 +1022,10 @@ aiosignal = [ {file = "aiosignal-1.3.1-py3-none-any.whl", hash = "sha256:f8376fb07dd1e86a584e4fcdec80b36b7f81aac666ebc724e2c090300dd83b17"}, {file = "aiosignal-1.3.1.tar.gz", hash = "sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc"}, ] +aiosqlite = [ + {file = "aiosqlite-0.17.0-py3-none-any.whl", hash = "sha256:6c49dc6d3405929b1d08eeccc72306d3677503cc5e5e43771efc1e00232e8231"}, + {file = "aiosqlite-0.17.0.tar.gz", hash = "sha256:f0e6acc24bc4864149267ac82fb46dfb3be4455f99fe21df82609cc6e6baee51"}, +] apispec = [ {file = "apispec-6.0.2-py3-none-any.whl", hash = "sha256:d97f0ae9c65133185b9ed9c5be1a434eb85627dfa33c4c53cabda122256c1b67"}, {file = "apispec-6.0.2.tar.gz", hash = "sha256:e76d80b739edef4be213092a6384ad7fd933ba7d64f6d5a0aff8d4da1bef7887"}, @@ -1381,6 +1442,10 @@ iniconfig = [ {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, ] +iso8601 = [ + {file = "iso8601-1.1.0-py3-none-any.whl", hash = "sha256:8400e90141bf792bce2634df533dc57e3bee19ea120a87bebcd3da89a58ad73f"}, + {file = "iso8601-1.1.0.tar.gz", hash = "sha256:32811e7b81deee2063ea6d2e94f8819a86d1f3811e49d23623a41fa832bef03f"}, +] isort = [ {file = "isort-5.10.1-py3-none-any.whl", hash = "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7"}, {file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"}, @@ -1788,6 +1853,10 @@ pyparsing = [ {file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"}, {file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"}, ] +pypika-tortoise = [ + {file = "pypika-tortoise-0.1.6.tar.gz", hash = "sha256:d802868f479a708e3263724c7b5719a26ad79399b2a70cea065f4a4cadbebf36"}, + {file = "pypika_tortoise-0.1.6-py3-none-any.whl", hash = "sha256:2d68bbb7e377673743cff42aa1059f3a80228d411fbcae591e4465e173109fd8"}, +] pyrsistent = [ {file = "pyrsistent-0.19.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d6982b5a0237e1b7d876b60265564648a69b14017f3b5f908c5be2de3f9abb7a"}, {file = "pyrsistent-0.19.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:187d5730b0507d9285a96fca9716310d572e5464cadd19f22b63a6976254d77a"}, @@ -1816,6 +1885,10 @@ pytest = [ {file = "pytest-7.1.3-py3-none-any.whl", hash = "sha256:1377bda3466d70b55e3f5cecfa55bb7cfcf219c7964629b967c37cf0bda818b7"}, {file = "pytest-7.1.3.tar.gz", hash = "sha256:4f365fec2dff9c1162f834d9f18af1ba13062db0c708bf7b946f8a5c76180c39"}, ] +pytz = [ + {file = "pytz-2022.6-py2.py3-none-any.whl", hash = "sha256:222439474e9c98fced559f1709d89e6c9cbf8d79c794ff3eb9f8800064291427"}, + {file = "pytz-2022.6.tar.gz", hash = "sha256:e89512406b793ca39f5971bc999cc538ce125c0e51c27941bef4568b460095e2"}, +] requests = [ {file = "requests-2.28.1-py3-none-any.whl", hash = "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349"}, {file = "requests-2.28.1.tar.gz", hash = "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983"}, @@ -1836,6 +1909,10 @@ tomlkit = [ {file = "tomlkit-0.11.5-py3-none-any.whl", hash = "sha256:f2ef9da9cef846ee027947dc99a45d6b68a63b0ebc21944649505bf2e8bc5fe7"}, {file = "tomlkit-0.11.5.tar.gz", hash = "sha256:571854ebbb5eac89abcb4a2e47d7ea27b89bf29e09c35395da6f03dd4ae23d1c"}, ] +tortoise-orm = [ + {file = "tortoise-orm-0.19.2.tar.gz", hash = "sha256:bff4d79abfca7fb805972bb2438e8e0cd2e6590bc2cfd7593a803518a027bbf0"}, + {file = "tortoise_orm-0.19.2-py3-none-any.whl", hash = "sha256:a99b8c9f42d5cd49493c70471b5c9a5df8ecf49cc624f2f41b9dc75bba993ac5"}, +] typing-extensions = [ {file = "typing_extensions-4.3.0-py3-none-any.whl", hash = "sha256:25642c956049920a5aa49edcdd6ab1e06d7e5d467fc00e0506c44ac86fbfca02"}, {file = "typing_extensions-4.3.0.tar.gz", hash = "sha256:e6d2677a32f47fc7eb2795db1dd15c1f34eff616bcaf2cfb5e997f854fa1c4a6"}, diff --git a/pyproject.toml b/pyproject.toml index 6d595254..5b926fb3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,6 +14,7 @@ apispec = "^6.0.2" uvicorn = "^0.19.0" aiohttp = "^3.8.3" cosmpy = "^0.6.0" +tortoise-orm = "^0.19.2" [tool.poetry.dev-dependencies] black = "^22.8.0" From ca161fa003ea6cb75a9a16ca6cb691ad2763add3 Mon Sep 17 00:00:00 2001 From: James Riehl Date: Fri, 2 Dec 2022 10:23:28 +0000 Subject: [PATCH 20/41] feat: add tortoise orm --- examples/10-cleaning-demo/cleaner.py | 41 ++++-- .../protocols/cleaning/__init__.py | 92 ++++++++++++++ .../protocols/cleaning/models.py | 120 ++++-------------- examples/10-cleaning-demo/user.py | 5 +- src/nexus/agent.py | 5 - 5 files changed, 149 insertions(+), 114 deletions(-) diff --git a/examples/10-cleaning-demo/cleaner.py b/examples/10-cleaning-demo/cleaner.py index 965db777..4703bc55 100644 --- a/examples/10-cleaning-demo/cleaner.py +++ b/examples/10-cleaning-demo/cleaner.py @@ -1,4 +1,7 @@ -from protocols.cleaning import cleaning_proto, Availability, Service +from tortoise import Tortoise, run_async + +from protocols.cleaning import cleaning_proto, Service +from protocols.cleaning.models import Availability, Provider, ServiceType from nexus import Agent from nexus.setup import fund_agent_if_low @@ -18,20 +21,30 @@ # build the restaurant agent from stock protocols cleaner.include(cleaning_proto) -availability = Availability( - address=25, - max_distance=10, - time_start=10, - time_end=18, - services=[Service.FLOOR, Service.WINDOW, Service.LAUNDRY], - min_hourly_price=12, -) -MARKUP = 1.1 -cleaner._storage.set( # pylint: disable=protected-access - "availability", availability.dict() -) -cleaner._storage.set("markup", MARKUP) # pylint: disable=protected-access +async def init_db(): + await Tortoise.init(db_url="sqlite://db.sqlite3", modules={"models": ["__main__"]}) + await Tortoise.generate_schemas() + + provider = await Provider.create(name=cleaner.name, address=12) + + floor = await Service.create(type=ServiceType.FLOOR) + window = await Service.create(type=ServiceType.WINDOW) + laundry = await Service.create(type=ServiceType.LAUNDRY) + + await provider.services.add(floor) + await provider.services.add(window) + await provider.services.add(laundry) + + await Availability.create( + provider=provider, + time_start=10, + time_end=22, + max_distance=10, + min_hourly_price=5, + ) + if __name__ == "__main__": + run_async(init_db()) cleaner.run() diff --git a/examples/10-cleaning-demo/protocols/cleaning/__init__.py b/examples/10-cleaning-demo/protocols/cleaning/__init__.py index e69de29b..deb922d4 100644 --- a/examples/10-cleaning-demo/protocols/cleaning/__init__.py +++ b/examples/10-cleaning-demo/protocols/cleaning/__init__.py @@ -0,0 +1,92 @@ +from typing import List + +from nexus import Context, Model, Protocol + +from .models import Provider, Availability + + +PROTOCOL_NAME = "cleaning" +PROTOCOL_VERSION = "0.1.0" + + +class ServiceRequest(Model): + address: int + time_start: int + duration: int + services: List[int] + max_price: float + + +class ServiceResponse(Model): + accept: bool + price: float + + +class ServiceBooking(Model): + address: str + time_start: int + duration: int + services: List[int] + price: float + + +class BookingResponse(Model): + success: bool + + +cleaning_proto = Protocol(name=PROTOCOL_NAME, version=PROTOCOL_VERSION) + + +def in_service_region( + address: int, availability: Availability, provider: Provider +) -> bool: + return abs(provider.address - address) <= availability.max_distance + + +@cleaning_proto.on_message(model=ServiceRequest, replies=ServiceResponse) +async def handle_query_request(ctx: Context, sender: str, msg: ServiceRequest): + + provider = await Provider.filter(name=ctx.name).first() + availability = await Availability.get(provider=provider) + services = [int(service.type) for service in await provider.services] + markup = provider.markup + + if ( + set(msg.services) <= set(services) + and in_service_region(msg.address, availability, provider) + and availability.time_start <= msg.time_start + and availability.time_end >= msg.time_start + msg.duration + and availability.min_hourly_price * msg.duration < msg.max_price + ): + accept = True + price = markup * availability.min_hourly_price * msg.duration + print(f"I am available! Proposing price: {price}.") + else: + accept = False + price = 0 + print("I am not available. Declining request.") + + await ctx.send(sender, ServiceResponse(accept=accept, price=price)) + + +@cleaning_proto.on_message(model=ServiceBooking, replies=BookingResponse) +async def handle_book_request(ctx: Context, sender: str, msg: ServiceBooking): + + provider = await Provider.get(name=ctx.name) + availability = await Availability.get(provider=provider) + services = [int(service.type) for service in await provider.services] + + success = ( + set(msg.services) <= set(services) + and availability.time_start <= msg.time_start + and availability.time_end >= msg.time_start + msg.duration + and msg.price <= availability.min_hourly_price * msg.duration + ) + + if success: + availability.time_start = msg.time_start + msg.duration + await availability.save() + print("Accepted task and updated availability.") + + # send the response + await ctx.send(sender, BookingResponse(success=success)) diff --git a/examples/10-cleaning-demo/protocols/cleaning/models.py b/examples/10-cleaning-demo/protocols/cleaning/models.py index afb3a663..39e31f6c 100644 --- a/examples/10-cleaning-demo/protocols/cleaning/models.py +++ b/examples/10-cleaning-demo/protocols/cleaning/models.py @@ -1,105 +1,41 @@ from enum import Enum -from typing import List from tortoise import fields, models -from tortoise.contrib.postgres.fields import ArrayField -from nexus import Context, Model, Protocol +class ServiceType(int, Enum): + FLOOR = 1 + WINDOW = 2 + LAUNDRY = 3 + IRON = 4 + BATHROOM = 5 -PROTOCOL_NAME = "cleaning" -PROTOCOL_VERSION = "0.1.0" +class User(models.Model): + id = fields.IntField(pk=True) + name = fields.CharField(max_length=64) + created_at = fields.DatetimeField(auto_now_add=True) -class Service(str, Enum): - FLOOR = "floor" - WINDOW = "window" - LAUNDRY = "laundry" - IRON = "iron" - BATHROOM = "bathroom" +class Service(models.Model): + id = fields.IntField(pk=True) + type = fields.IntEnumField(ServiceType) -class Availability(models.Model): - address = fields.IntField(default=0) - max_distance = fields.IntField(default=10) - time_start: fields.DatetimeField() - time_end: fields.DatetimeField(default=24) - services: ArrayField(element_type=str) - min_hourly_price: fields.FloatField(default=0.0) - - -class ServiceRequest(Model): - address: int - time_start: int - duration: int - services: List[Service] - max_price: float - - -class ServiceResponse(Model): - accept: bool - price: float - - -class ServiceBooking(Model): - address: str - time_start: int - duration: int - services: List[Service] - price: float - - -class BookingResponse(Model): - success: bool - - -cleaning_proto = Protocol(name=PROTOCOL_NAME, version=PROTOCOL_VERSION) +class Provider(models.Model): + id = fields.IntField(pk=True) + name = fields.CharField(max_length=64) + address = fields.IntField(default=0) + created_at = fields.DatetimeField(auto_now_add=True) + availability = fields.ReverseRelation["Availability"] + services = fields.ManyToManyField("models.Service") + markup = fields.FloatField(default=1.1) -def in_service_region(address: int, availability: Availability) -> bool: - return abs(availability.address - address) <= availability.max_distance - - -@cleaning_proto.on_message(model=ServiceRequest, replies=ServiceResponse) -async def handle_query_request(ctx: Context, sender: str, msg: ServiceRequest): - - availability = Availability(**ctx.storage.get("availability")) - markup = float(ctx.storage.get("markup")) - - if ( - set(msg.services) <= set(availability.services) - and in_service_region(msg.address, availability) - and availability.time_start <= msg.time_start - and availability.time_end >= msg.time_start + msg.duration - and availability.min_hourly_price * msg.duration < msg.max_price - ): - accept = True - price = markup * availability.min_hourly_price * msg.duration - print(f"I am available! Proposing price: {price}.") - else: - accept = False - price = 0 - print("I am not available. Declining request.") - - await ctx.send(sender, ServiceResponse(accept=accept, price=price)) - - -@cleaning_proto.on_message(model=ServiceBooking, replies=BookingResponse) -async def handle_book_request(ctx: Context, sender: str, msg: ServiceBooking): - - availability = Availability(**ctx.storage.get("availability")) - - success = ( - set(msg.services) <= set(availability.services) - and availability.time_start <= msg.time_start - and availability.time_end >= msg.time_start + msg.duration - and msg.price <= availability.min_hourly_price * msg.duration - ) - - if success: - availability.time_start = msg.time_start + msg.duration - ctx.storage.set("availability", availability.dict()) - print(f"Accepted task and updated availability.") - # send the response - await ctx.send(sender, BookingResponse(success=success)) +class Availability(models.Model): + id = fields.IntField(pk=True) + provider = fields.OneToOneField("models.Provider", related_name="availability") + max_distance = fields.IntField(default=10) + time_start = fields.IntField(default=0) + time_end = fields.IntField(default=24) + min_hourly_price = fields.FloatField(default=0.0) diff --git a/examples/10-cleaning-demo/user.py b/examples/10-cleaning-demo/user.py index 83e921a6..81f7b1bd 100644 --- a/examples/10-cleaning-demo/user.py +++ b/examples/10-cleaning-demo/user.py @@ -1,11 +1,10 @@ from protocols.cleaning import ( ServiceBooking, BookingResponse, - Service, ServiceRequest, ServiceResponse, ) - +from protocols.cleaning.models import ServiceType from nexus import Agent, Context from nexus.setup import fund_agent_if_low @@ -25,7 +24,7 @@ address=17, time_start=12, duration=4, - services=[Service.WINDOW, Service.LAUNDRY], + services=[ServiceType.WINDOW, ServiceType.LAUNDRY], max_price=60, ) diff --git a/src/nexus/agent.py b/src/nexus/agent.py index efb224bb..9ed9a068 100644 --- a/src/nexus/agent.py +++ b/src/nexus/agent.py @@ -141,15 +141,10 @@ async def register(self, ctx: Context): agent_balance = ctx.ledger.query_bank_balance(ctx.wallet) if agent_balance < REGISTRATION_FEE: -<<<<<<< HEAD - logging.exception(f"Insufficient funds to register {self._name}") - return -======= logging.exception( f"Insufficient funds to register {self._name}\ \nFund using wallet address: {self.wallet.address()}" ) ->>>>>>> 8040377795c2145cce80bb98c863733038f1df7c signature = self.sign_registration() From e94020b64ba926ecce321539419861d700461634 Mon Sep 17 00:00:00 2001 From: James Riehl Date: Fri, 2 Dec 2022 10:34:04 +0000 Subject: [PATCH 21/41] chore: fix import --- examples/10-cleaning-demo/cleaner.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/10-cleaning-demo/cleaner.py b/examples/10-cleaning-demo/cleaner.py index 4703bc55..a5b955a1 100644 --- a/examples/10-cleaning-demo/cleaner.py +++ b/examples/10-cleaning-demo/cleaner.py @@ -1,7 +1,7 @@ from tortoise import Tortoise, run_async -from protocols.cleaning import cleaning_proto, Service -from protocols.cleaning.models import Availability, Provider, ServiceType +from protocols.cleaning import cleaning_proto +from protocols.cleaning.models import Availability, Provider, Service, ServiceType from nexus import Agent from nexus.setup import fund_agent_if_low From 8a85706f069a861687f6bb22b8bdeb82d2429428 Mon Sep 17 00:00:00 2001 From: James Riehl Date: Fri, 2 Dec 2022 14:34:37 +0000 Subject: [PATCH 22/41] chore: move tortoise to orm dependency group --- examples/10-cleaning-demo/cleaner.py | 4 +- poetry.lock | 393 +++++++++++++-------------- pyproject.toml | 4 +- 3 files changed, 195 insertions(+), 206 deletions(-) diff --git a/examples/10-cleaning-demo/cleaner.py b/examples/10-cleaning-demo/cleaner.py index a5b955a1..2306f388 100644 --- a/examples/10-cleaning-demo/cleaner.py +++ b/examples/10-cleaning-demo/cleaner.py @@ -16,9 +16,9 @@ fund_agent_if_low(cleaner.wallet.address()) -print(cleaner.address) +print("Agent address:", cleaner.address) -# build the restaurant agent from stock protocols +# build the cleaning service agent from the cleaning protocol cleaner.include(cleaning_proto) diff --git a/poetry.lock b/poetry.lock index 521f3483..a426f9d4 100644 --- a/poetry.lock +++ b/poetry.lock @@ -33,7 +33,7 @@ frozenlist = ">=1.1.0" name = "aiosqlite" version = "0.17.0" description = "asyncio bridge to the standard sqlite3 module" -category = "main" +category = "dev" optional = false python-versions = ">=3.6" @@ -70,7 +70,7 @@ python-versions = "*" [[package]] name = "astroid" -version = "2.12.10" +version = "2.12.13" description = "An abstract syntax tree for Python with inference support." category = "dev" optional = false @@ -137,11 +137,11 @@ develop = ["coverage (>=5.3)", "flake8 (>=3.8)", "isort (>=5.8)", "mypy (>=0.900 [[package]] name = "black" -version = "22.8.0" +version = "22.10.0" description = "The uncompromising code formatter." category = "dev" optional = false -python-versions = ">=3.6.2" +python-versions = ">=3.7" [package.dependencies] click = ">=8.0.0" @@ -178,7 +178,7 @@ python-versions = "~=3.7" [[package]] name = "cbor2" -version = "5.4.3" +version = "5.4.5" description = "CBOR (de)serializer with extensive tag support" category = "main" optional = false @@ -243,15 +243,15 @@ cffi = ">=1.3.0" [[package]] name = "colorama" -version = "0.4.5" +version = "0.4.6" description = "Cross-platform colored terminal text." category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" [[package]] name = "cosmpy" -version = "0.6.0" +version = "0.6.2" description = "A library for interacting with the cosmos networks" category = "main" optional = false @@ -264,7 +264,7 @@ blspy = "*" ecdsa = "*" google-api-python-client = "*" grpcio = "1.47.0" -jsonschema = ">=4.16.0,<5.0.0" +jsonschema = ">=3.2.0,<5" protobuf = ">=3.19.4,<4" requests = "*" @@ -278,11 +278,11 @@ python-versions = "*" [[package]] name = "dill" -version = "0.3.5.1" +version = "0.3.6" description = "serialize all of python" category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, !=3.6.*" +python-versions = ">=3.7" [package.extras] graph = ["objgraph (>=1.7.2)"] @@ -310,6 +310,17 @@ category = "main" optional = false python-versions = "*" +[[package]] +name = "exceptiongroup" +version = "1.0.4" +description = "Backport of PEP 654 (exception groups)" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.extras] +test = ["pytest (>=6)"] + [[package]] name = "frozenlist" version = "1.3.3" @@ -320,26 +331,26 @@ python-versions = ">=3.7" [[package]] name = "google-api-core" -version = "2.10.2" +version = "2.11.0" description = "Google API client core library" category = "main" optional = false python-versions = ">=3.7" [package.dependencies] -google-auth = ">=1.25.0,<3.0dev" +google-auth = ">=2.14.1,<3.0dev" googleapis-common-protos = ">=1.56.2,<2.0dev" protobuf = ">=3.19.5,<3.20.0 || >3.20.0,<3.20.1 || >3.20.1,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<5.0.0dev" requests = ">=2.18.0,<3.0.0dev" [package.extras] -grpc = ["grpcio (>=1.33.2,<2.0dev)", "grpcio-status (>=1.33.2,<2.0dev)"] +grpc = ["grpcio (>=1.33.2,<2.0dev)", "grpcio (>=1.49.1,<2.0dev)", "grpcio-status (>=1.33.2,<2.0dev)", "grpcio-status (>=1.49.1,<2.0dev)"] grpcgcp = ["grpcio-gcp (>=0.2.2,<1.0dev)"] grpcio-gcp = ["grpcio-gcp (>=0.2.2,<1.0dev)"] [[package]] name = "google-api-python-client" -version = "2.66.0" +version = "2.68.0" description = "Google API Client Library for Python" category = "main" optional = false @@ -354,7 +365,7 @@ uritemplate = ">=3.0.1,<5" [[package]] name = "google-auth" -version = "2.14.1" +version = "2.15.0" description = "Google Authentication Library" category = "main" optional = false @@ -467,7 +478,7 @@ python-versions = "*" name = "iso8601" version = "1.1.0" description = "Simple module to parse ISO 8601 dates" -category = "main" +category = "dev" optional = false python-versions = ">=3.6.2,<4.0" @@ -487,7 +498,7 @@ requirements_deprecated_finder = ["pip-api", "pipreqs"] [[package]] name = "jsonschema" -version = "4.17.0" +version = "4.17.3" description = "An implementation of JSON Schema validation for Python" category = "main" optional = false @@ -505,11 +516,11 @@ format-nongpl = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339- [[package]] name = "lazy-object-proxy" -version = "1.7.1" +version = "1.8.0" description = "A fast and thorough lazy object proxy." category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [[package]] name = "mccabe" @@ -556,7 +567,7 @@ pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" [[package]] name = "pathspec" -version = "0.10.1" +version = "0.10.2" description = "Utility library for gitignore style pattern matching of file paths." category = "dev" optional = false @@ -572,15 +583,15 @@ python-versions = ">=3.6" [[package]] name = "platformdirs" -version = "2.5.2" -description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +version = "2.5.4" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." category = "dev" optional = false python-versions = ">=3.7" [package.extras] -docs = ["furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx (>=4)", "sphinx-autodoc-typehints (>=1.12)"] -test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"] +docs = ["furo (>=2022.9.29)", "proselint (>=0.13)", "sphinx (>=5.3)", "sphinx-autodoc-typehints (>=1.19.4)"] +test = ["appdirs (==1.4.4)", "pytest (>=7.2)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"] [[package]] name = "pluggy" @@ -602,14 +613,6 @@ category = "main" optional = false python-versions = ">=3.7" -[[package]] -name = "py" -version = "1.11.0" -description = "library with cross-python path, ini-parsing, io, code, log facilities" -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" - [[package]] name = "py-sr25519-bindings" version = "0.2.0" @@ -647,7 +650,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "pycryptodome" -version = "3.15.0" +version = "3.16.0" description = "Cryptographic library for Python" category = "main" optional = false @@ -670,14 +673,14 @@ email = ["email-validator (>=1.0.3)"] [[package]] name = "pylint" -version = "2.15.3" +version = "2.15.7" description = "python code static checker" category = "dev" optional = false python-versions = ">=3.7.2" [package.dependencies] -astroid = ">=2.12.10,<=2.14.0-dev0" +astroid = ">=2.12.13,<=2.14.0-dev0" colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} dill = ">=0.2" isort = ">=4.2.5,<6" @@ -721,7 +724,7 @@ diagrams = ["jinja2", "railroad-diagrams"] name = "pypika-tortoise" version = "0.1.6" description = "Forked from pypika and streamline just for tortoise-orm" -category = "main" +category = "dev" optional = false python-versions = ">=3.7,<4.0" @@ -735,7 +738,7 @@ python-versions = ">=3.7" [[package]] name = "pytest" -version = "7.1.3" +version = "7.2.0" description = "pytest: simple powerful testing with Python" category = "dev" optional = false @@ -744,11 +747,11 @@ python-versions = ">=3.7" [package.dependencies] attrs = ">=19.2.0" colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} iniconfig = "*" packaging = "*" pluggy = ">=0.12,<2.0" -py = ">=1.8.2" -tomli = ">=1.0.0" +tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} [package.extras] testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] @@ -757,7 +760,7 @@ testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2. name = "pytz" version = "2022.6" description = "World timezone definitions, modern and historical" -category = "main" +category = "dev" optional = false python-versions = "*" @@ -808,17 +811,17 @@ python-versions = ">=3.7" [[package]] name = "tomlkit" -version = "0.11.5" +version = "0.11.6" description = "Style preserving TOML library" category = "dev" optional = false -python-versions = ">=3.6,<4.0" +python-versions = ">=3.6" [[package]] name = "tortoise-orm" version = "0.19.2" description = "Easy async ORM for python, built with relations in mind" -category = "main" +category = "dev" optional = false python-versions = ">=3.7,<4.0" @@ -838,7 +841,7 @@ psycopg = ["psycopg[binary,pool]"] [[package]] name = "typing-extensions" -version = "4.3.0" +version = "4.4.0" description = "Backported and Experimental Type Hints for Python 3.7+" category = "main" optional = false @@ -854,11 +857,11 @@ python-versions = ">=3.6" [[package]] name = "urllib3" -version = "1.26.12" +version = "1.26.13" description = "HTTP library with thread-safe connection pooling, file post, and more." category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, <4" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" [package.extras] brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] @@ -913,7 +916,7 @@ multidict = ">=4.0" [[package]] name = "zipp" -version = "3.10.0" +version = "3.11.0" description = "Backport of pathlib-compatible object wrapper for zip files" category = "main" optional = false @@ -926,7 +929,7 @@ testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools" [metadata] lock-version = "1.1" python-versions = "^3.8" -content-hash = "2f0e71a168e2d79d932ac249162e8be9752d68340c2d278076da35042b17b86e" +content-hash = "6028d1e1e755220a4e8949d97d17f6c6a3a7579b2e7ec0d1d38bf8bd4f8359c8" [metadata.files] aiohttp = [ @@ -1035,8 +1038,8 @@ asn1crypto = [ {file = "asn1crypto-1.5.1.tar.gz", hash = "sha256:13ae38502be632115abf8a24cbe5f4da52e3b5231990aff31123c805306ccb9c"}, ] astroid = [ - {file = "astroid-2.12.10-py3-none-any.whl", hash = "sha256:997e0c735df60d4a4caff27080a3afc51f9bdd693d3572a4a0b7090b645c36c5"}, - {file = "astroid-2.12.10.tar.gz", hash = "sha256:81f870105d892e73bf535da77a8261aa5bde838fa4ed12bb2f435291a098c581"}, + {file = "astroid-2.12.13-py3-none-any.whl", hash = "sha256:10e0ad5f7b79c435179d0d0f0df69998c4eef4597534aae44910db060baeb907"}, + {file = "astroid-2.12.13.tar.gz", hash = "sha256:1493fe8bd3dfd73dc35bd53c9d5b6e49ead98497c47b2307662556a5692d29d7"}, ] async-timeout = [ {file = "async-timeout-4.0.2.tar.gz", hash = "sha256:2163e1640ddb52b7a8c80d0a67a08587e5d245cc9c553a74a847056bc2976b15"}, @@ -1055,29 +1058,27 @@ bip-utils = [ {file = "bip_utils-2.7.0.tar.gz", hash = "sha256:bc6302840a95695609e215ad362ddb42d70d472b3cb1494d1fb2112d08c1c707"}, ] black = [ - {file = "black-22.8.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ce957f1d6b78a8a231b18e0dd2d94a33d2ba738cd88a7fe64f53f659eea49fdd"}, - {file = "black-22.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5107ea36b2b61917956d018bd25129baf9ad1125e39324a9b18248d362156a27"}, - {file = "black-22.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e8166b7bfe5dcb56d325385bd1d1e0f635f24aae14b3ae437102dedc0c186747"}, - {file = "black-22.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd82842bb272297503cbec1a2600b6bfb338dae017186f8f215c8958f8acf869"}, - {file = "black-22.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:d839150f61d09e7217f52917259831fe2b689f5c8e5e32611736351b89bb2a90"}, - {file = "black-22.8.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:a05da0430bd5ced89176db098567973be52ce175a55677436a271102d7eaa3fe"}, - {file = "black-22.8.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a098a69a02596e1f2a58a2a1c8d5a05d5a74461af552b371e82f9fa4ada8342"}, - {file = "black-22.8.0-cp36-cp36m-win_amd64.whl", hash = "sha256:5594efbdc35426e35a7defa1ea1a1cb97c7dbd34c0e49af7fb593a36bd45edab"}, - {file = "black-22.8.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a983526af1bea1e4cf6768e649990f28ee4f4137266921c2c3cee8116ae42ec3"}, - {file = "black-22.8.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b2c25f8dea5e8444bdc6788a2f543e1fb01494e144480bc17f806178378005e"}, - {file = "black-22.8.0-cp37-cp37m-win_amd64.whl", hash = "sha256:78dd85caaab7c3153054756b9fe8c611efa63d9e7aecfa33e533060cb14b6d16"}, - {file = "black-22.8.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:cea1b2542d4e2c02c332e83150e41e3ca80dc0fb8de20df3c5e98e242156222c"}, - {file = "black-22.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5b879eb439094751185d1cfdca43023bc6786bd3c60372462b6f051efa6281a5"}, - {file = "black-22.8.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0a12e4e1353819af41df998b02c6742643cfef58282915f781d0e4dd7a200411"}, - {file = "black-22.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c3a73f66b6d5ba7288cd5d6dad9b4c9b43f4e8a4b789a94bf5abfb878c663eb3"}, - {file = "black-22.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:e981e20ec152dfb3e77418fb616077937378b322d7b26aa1ff87717fb18b4875"}, - {file = "black-22.8.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8ce13ffed7e66dda0da3e0b2eb1bdfc83f5812f66e09aca2b0978593ed636b6c"}, - {file = "black-22.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:32a4b17f644fc288c6ee2bafdf5e3b045f4eff84693ac069d87b1a347d861497"}, - {file = "black-22.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0ad827325a3a634bae88ae7747db1a395d5ee02cf05d9aa7a9bd77dfb10e940c"}, - {file = "black-22.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:53198e28a1fb865e9fe97f88220da2e44df6da82b18833b588b1883b16bb5d41"}, - {file = "black-22.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:bc4d4123830a2d190e9cc42a2e43570f82ace35c3aeb26a512a2102bce5af7ec"}, - {file = "black-22.8.0-py3-none-any.whl", hash = "sha256:d2c21d439b2baf7aa80d6dd4e3659259be64c6f49dfd0f32091063db0e006db4"}, - {file = "black-22.8.0.tar.gz", hash = "sha256:792f7eb540ba9a17e8656538701d3eb1afcb134e3b45b71f20b25c77a8db7e6e"}, + {file = "black-22.10.0-1fixedarch-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:5cc42ca67989e9c3cf859e84c2bf014f6633db63d1cbdf8fdb666dcd9e77e3fa"}, + {file = "black-22.10.0-1fixedarch-cp311-cp311-macosx_11_0_x86_64.whl", hash = "sha256:5d8f74030e67087b219b032aa33a919fae8806d49c867846bfacde57f43972ef"}, + {file = "black-22.10.0-1fixedarch-cp37-cp37m-macosx_10_16_x86_64.whl", hash = "sha256:197df8509263b0b8614e1df1756b1dd41be6738eed2ba9e9769f3880c2b9d7b6"}, + {file = "black-22.10.0-1fixedarch-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:2644b5d63633702bc2c5f3754b1b475378fbbfb481f62319388235d0cd104c2d"}, + {file = "black-22.10.0-1fixedarch-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:e41a86c6c650bcecc6633ee3180d80a025db041a8e2398dcc059b3afa8382cd4"}, + {file = "black-22.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2039230db3c6c639bd84efe3292ec7b06e9214a2992cd9beb293d639c6402edb"}, + {file = "black-22.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14ff67aec0a47c424bc99b71005202045dc09270da44a27848d534600ac64fc7"}, + {file = "black-22.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:819dc789f4498ecc91438a7de64427c73b45035e2e3680c92e18795a839ebb66"}, + {file = "black-22.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5b9b29da4f564ba8787c119f37d174f2b69cdfdf9015b7d8c5c16121ddc054ae"}, + {file = "black-22.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8b49776299fece66bffaafe357d929ca9451450f5466e997a7285ab0fe28e3b"}, + {file = "black-22.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:21199526696b8f09c3997e2b4db8d0b108d801a348414264d2eb8eb2532e540d"}, + {file = "black-22.10.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e464456d24e23d11fced2bc8c47ef66d471f845c7b7a42f3bd77bf3d1789650"}, + {file = "black-22.10.0-cp37-cp37m-win_amd64.whl", hash = "sha256:9311e99228ae10023300ecac05be5a296f60d2fd10fff31cf5c1fa4ca4b1988d"}, + {file = "black-22.10.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:fba8a281e570adafb79f7755ac8721b6cf1bbf691186a287e990c7929c7692ff"}, + {file = "black-22.10.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:915ace4ff03fdfff953962fa672d44be269deb2eaf88499a0f8805221bc68c87"}, + {file = "black-22.10.0-cp38-cp38-win_amd64.whl", hash = "sha256:444ebfb4e441254e87bad00c661fe32df9969b2bf224373a448d8aca2132b395"}, + {file = "black-22.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:974308c58d057a651d182208a484ce80a26dac0caef2895836a92dd6ebd725e0"}, + {file = "black-22.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72ef3925f30e12a184889aac03d77d031056860ccae8a1e519f6cbb742736383"}, + {file = "black-22.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:432247333090c8c5366e69627ccb363bc58514ae3e63f7fc75c54b1ea80fa7de"}, + {file = "black-22.10.0-py3-none-any.whl", hash = "sha256:c957b2b4ea88587b46cf49d1dc17681c1e672864fd7af32fc1e9664d572b3458"}, + {file = "black-22.10.0.tar.gz", hash = "sha256:f513588da599943e0cde4e32cc9879e825d58720d6557062d1098c5ad80080e1"}, ] blspy = [ {file = "blspy-1.0.16-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:5c4d31f19df5e6bd2bb84c3f21a43ef3cc80ff9414bb2541a8e49dad111aead6"}, @@ -1110,34 +1111,42 @@ cachetools = [ {file = "cachetools-5.2.0.tar.gz", hash = "sha256:6a94c6402995a99c3970cc7e4884bb60b4a8639938157eeed436098bf9831757"}, ] cbor2 = [ - {file = "cbor2-5.4.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8a643b19ace1584043bbf4e2d0b4fae8bebd6b6ffab14ea6478d3ff07f58e854"}, - {file = "cbor2-5.4.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e10f2f4fcf5ab6a8b24d22f7109f48cad8143f669795899370170d7b36ed309f"}, - {file = "cbor2-5.4.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2de925608dc6d73cd1aab08800bff38f71f90459c15db3a71a67023b0fc697da"}, - {file = "cbor2-5.4.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:62fc15bfe187e4994c457e6055687514c417d6099de62dd33ae766561f05847e"}, - {file = "cbor2-5.4.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:3843a9bb970343e9c896aa71a34fa80983cd0ddec6eacdb2284b5e83f4ee7511"}, - {file = "cbor2-5.4.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b35c5d4d14fe804f718d5a5968a528970d2a7046aa87045538f189a98e5c7055"}, - {file = "cbor2-5.4.3-cp310-cp310-win_amd64.whl", hash = "sha256:0a3a1b2f6b83ab4ce806df48360cc16d34cd315f17549dbda9fdd371bea04497"}, - {file = "cbor2-5.4.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4b09ff6148a8cd529512479a1d6521fb7687fb03b448973933c3b03711d00bfc"}, - {file = "cbor2-5.4.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d21ccd1ec802e88dba1c373724a09538a0237116ab589c5301ca4c59478f7c10"}, - {file = "cbor2-5.4.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c07975f956baddb8dfeca4966f1871fd2482cb36af24c461f763732a44675225"}, - {file = "cbor2-5.4.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:9538ab1b4207e76ee02a52362d77e312921ec1dc75b6fb42182887d87d0ca53e"}, - {file = "cbor2-5.4.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:cbca58220f52fd50d8985e4079e10c71196d538fb6685f157f608a29253409a4"}, - {file = "cbor2-5.4.3-cp37-cp37m-win_amd64.whl", hash = "sha256:c617c7f94936d65ed9c8e99c6c03e3dc83313d69c6bfea810014ec658e9b1a9d"}, - {file = "cbor2-5.4.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:70789805b9aebd215626188aa05bb09908ed51e3268d4db5ae6a08276efdbcb1"}, - {file = "cbor2-5.4.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0e4ae67a697c664b579b87c4ef9d60e26c146b95bff443a9a38abb16f6981ff0"}, - {file = "cbor2-5.4.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab6c934806759d453a9bb5318f2703c831e736be005ac35d5bd5cf2093ba57b1"}, - {file = "cbor2-5.4.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:981b9ffc4f2947a0f030e71ce5eac31334bc81369dd57c6c1273c94c6cdb0b5a"}, - {file = "cbor2-5.4.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:cbe7cdeed26cd8ec2dcfed2b8876bc137ad8b9e0abb07aa5fb05770148a4b5c7"}, - {file = "cbor2-5.4.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6bc8c5606aa0ae510bdb3c7d987f92df39ef87d09e0f0588a4d1daffd3cb0453"}, - {file = "cbor2-5.4.3-cp38-cp38-win_amd64.whl", hash = "sha256:5c50da4702ac5ca3a8e7cb9f34f62b4ea91bc81b76c2fba03888b366da299cd8"}, - {file = "cbor2-5.4.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:37ae0ce5afe864d1a1c5b05becaf8aaca7b7131cb7b0b935d7e79b29fb1cea28"}, - {file = "cbor2-5.4.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2f30f7ef329ea6ec630ceabe5a539fed407b9c81e27e2322644e3efbbd1b2a76"}, - {file = "cbor2-5.4.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d549abea7115c8a0d7c61a31a895c031f902a7b4c875f9efd8ce41e466baf83a"}, - {file = "cbor2-5.4.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fab0e00c28305db59f7005150447d08dd13da6a82695a2132c28beba590fd2c"}, - {file = "cbor2-5.4.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:20291dad09cf9c4e5f434d376dd9d60f5ab5e066b308005f50e7c5e22e504214"}, - {file = "cbor2-5.4.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5aaf3406c9d661d11f87e792edb9a38561dba1441afba7fb883d6d963e67f32c"}, - {file = "cbor2-5.4.3-cp39-cp39-win_amd64.whl", hash = "sha256:4e8590193fcbbb9477010ca0f094f6540a5e723965c90eea7a37edbe75f0ec4d"}, - {file = "cbor2-5.4.3.tar.gz", hash = "sha256:62b863c5ee6ced4032afe948f3c1484f375550995d3b8498145237fe28e546c2"}, + {file = "cbor2-5.4.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a758aa5c87f94dca0f3a502cb67678cc460535667d51418d6290877873c5de9f"}, + {file = "cbor2-5.4.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e457221fab640ae26318542fdb3da582b0674bdb456b19262323f1bd363058b9"}, + {file = "cbor2-5.4.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:53709a78e99c52aa4f993e9fe7c18e3a177447deec3141f8072f0fb375eed862"}, + {file = "cbor2-5.4.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ae88ec3cf72a2517b7b6e86660c7bf56a6004049f3130e3fb47c53d078fdd30b"}, + {file = "cbor2-5.4.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:26421eb4d0951caa243177721c4e0fe96aeb7ec47d33ea3b632901142ce1868f"}, + {file = "cbor2-5.4.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fb2ee29a49de559890b33e3f70069bdc17995bf024a856cd6e3e7ee7e5dd6f4c"}, + {file = "cbor2-5.4.5-cp310-cp310-win_amd64.whl", hash = "sha256:f0175b4bc620a3de03dbfeaceb6007e8df6feafbc6294f489058659d6a512376"}, + {file = "cbor2-5.4.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0213a16c5dea25416809dc234d54b1d6c519562dce344eed947c47cc0124b4db"}, + {file = "cbor2-5.4.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:319dc8d5e9721065c1d037bed1dee0602ad1c407fbf913a3364de6c5490aa657"}, + {file = "cbor2-5.4.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e25bd6cb6ffda5f3d140d64ffa7f24f01fd9db634f3c12e74593a8144e2581b5"}, + {file = "cbor2-5.4.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac8083295d25ec1f5523e5f1ef313fdaac2cf663ff061418eb68def6af3615a4"}, + {file = "cbor2-5.4.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e7fb7bd57c7f19304510a4a5e94178af96f26586c44f6e958651c01cd1345739"}, + {file = "cbor2-5.4.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:19a124aff6e4ebf1901553fcc1db6a3c23401955844b2e1421b632c0efcabeb5"}, + {file = "cbor2-5.4.5-cp311-cp311-win_amd64.whl", hash = "sha256:d380bcc07b2a88351b154eadd6d96be416aa25e852d85531eb99a9e502c3d8b3"}, + {file = "cbor2-5.4.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a42a9f86660c1256dc431389cb0240d60341f96f3c10ee89dbf27ef2bfaff3f3"}, + {file = "cbor2-5.4.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ddb7efa0e1f68add317e3dda96fd10d7a327237ec601d9906a30194cf92e8d8"}, + {file = "cbor2-5.4.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a98e1268e1630bd6017d49fe8ec0cc54bd5228b47d0e243e770bf56b63519f12"}, + {file = "cbor2-5.4.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:147855bc3fd3d63418f689053b28f71d53695cb20687163efea733f3ade107a6"}, + {file = "cbor2-5.4.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:41cc81651d46872f61661295d09e85088808dd370a479f8f2a090832f6199bae"}, + {file = "cbor2-5.4.5-cp37-cp37m-win_amd64.whl", hash = "sha256:20655a950686d53d4f4ad39e29a2953f7ab0ab278c977ff4adabca09961aca82"}, + {file = "cbor2-5.4.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:385ec264fb21fd9b6a0274a8c8407f67621a0f67a29bc71647ee15731ff8d0f1"}, + {file = "cbor2-5.4.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:443b01997d3a6a9f6652e4759cf4c169a5f3b726f54fefea0272b189e9a327c7"}, + {file = "cbor2-5.4.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a088d913b049b91435d36ef815098d15ac634ac25bfc38154733d6c522a089b6"}, + {file = "cbor2-5.4.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5cf264a14f5702f3fff8909e06d3064fb9a835b62acb65c6ee219d1e89404235"}, + {file = "cbor2-5.4.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:d7668b9ba429f2b38572007a0ff78e87179070267e34454f982c99b4ccb6dadd"}, + {file = "cbor2-5.4.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:606c0d3dca772e21711403d0f8648c6d99b45e468e4b44ebfbfad81ef76d09f9"}, + {file = "cbor2-5.4.5-cp38-cp38-win_amd64.whl", hash = "sha256:e8b8bd6db3da237c245c5bb7f31413badec98dfb0e82c59172cb51b246859bd4"}, + {file = "cbor2-5.4.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:144f4afb75eeeb12e218cdb8ec3c00545aa825ab3b2009957463aa469966e7c5"}, + {file = "cbor2-5.4.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:37b931e5b91514125b4e5807a5ff487df008caea9265294a74688a87ee1e221f"}, + {file = "cbor2-5.4.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e319504b4886ab840eaab7e9e890a6b1a588c4d1e098fb9991ddb685a9cec7d0"}, + {file = "cbor2-5.4.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:761870c22ad5aec18e77ff50d86a0c9fcb4581c7467606c362920a6bde1f1cf0"}, + {file = "cbor2-5.4.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:844ccfe201604827b214645b1a232516c55ea4cc015c5341f19b6a7a2d484ed5"}, + {file = "cbor2-5.4.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:13dcb33217a21a1b2b82b4b9e798ffb92d2679173994a070882ab93c0f736db9"}, + {file = "cbor2-5.4.5-cp39-cp39-win_amd64.whl", hash = "sha256:ca5a76dd55a2af718a7887ea48e04618b17deaf37ac3a986d1fa7d494c5069b2"}, + {file = "cbor2-5.4.5-py3-none-any.whl", hash = "sha256:deb271768011f6d31ac9ee77577f9af9c8d71db0178c51ea9e6546fd6cff9a87"}, + {file = "cbor2-5.4.5.tar.gz", hash = "sha256:2256fdcc11613b0297a4b844e268d20db4f7d4be79d2002f51613bd8105366ef"}, ] certifi = [ {file = "certifi-2022.9.24-py3-none-any.whl", hash = "sha256:90c1a32f1d68f940488354e36370f6cca89f0f106db09518524c88d6ed83f382"}, @@ -1254,12 +1263,12 @@ coincurve = [ {file = "coincurve-17.0.0.tar.gz", hash = "sha256:68da55aff898702952fda3ee04fd6ed60bb6b91f919c69270786ed766b548b93"}, ] colorama = [ - {file = "colorama-0.4.5-py2.py3-none-any.whl", hash = "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da"}, - {file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"}, + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] cosmpy = [ - {file = "cosmpy-0.6.0-py3-none-any.whl", hash = "sha256:fce96685e4fd85642ed48e448eb8f4a38871af506d1f150d8fd464f3947ce502"}, - {file = "cosmpy-0.6.0.tar.gz", hash = "sha256:5d93847e92a30b724778f6263df7df8c9a193d5b819d25ece07aa85517436dd1"}, + {file = "cosmpy-0.6.2-py3-none-any.whl", hash = "sha256:537a262df4d72346e751c97480b122cbcbcc66be32ef321a1a95c2b1d8628ff1"}, + {file = "cosmpy-0.6.2.tar.gz", hash = "sha256:a7c92b13a34bfa4b5287e56c3d4c7cb85cc3e8d008965da3fb18711c46c18ab6"}, ] crcmod = [ {file = "crcmod-1.7.tar.gz", hash = "sha256:dc7051a0db5f2bd48665a990d3ec1cc305a466a77358ca4492826f41f283601e"}, @@ -1268,8 +1277,8 @@ crcmod = [ {file = "crcmod-1.7.win32-py3.1.msi", hash = "sha256:50586ab48981f11e5b117523d97bb70864a2a1af246cf6e4f5c4a21ef4611cd1"}, ] dill = [ - {file = "dill-0.3.5.1-py2.py3-none-any.whl", hash = "sha256:33501d03270bbe410c72639b350e941882a8b0fd55357580fbc873fba0c59302"}, - {file = "dill-0.3.5.1.tar.gz", hash = "sha256:d75e41f3eff1eee599d738e76ba8f4ad98ea229db8b085318aa2b3333a208c86"}, + {file = "dill-0.3.6-py3-none-any.whl", hash = "sha256:a07ffd2351b8c678dfc4a856a3005f8067aea51d6ba6c700796a4d9e280f39f0"}, + {file = "dill-0.3.6.tar.gz", hash = "sha256:e5db55f3687856d8fbdab002ed78544e1c4559a130302693d839dfe8f93f2373"}, ] ecdsa = [ {file = "ecdsa-0.18.0-py2.py3-none-any.whl", hash = "sha256:80600258e7ed2f16b9aa1d7c295bd70194109ad5a30fdee0eaeefef1d4c559dd"}, @@ -1278,6 +1287,10 @@ ecdsa = [ ed25519-blake2b = [ {file = "ed25519-blake2b-1.4.tar.gz", hash = "sha256:d1a1cb9032ec307ce95b41c619440fd4d3fcecc18f224035cc7d6dc7a7d8ef40"}, ] +exceptiongroup = [ + {file = "exceptiongroup-1.0.4-py3-none-any.whl", hash = "sha256:542adf9dea4055530d6e1279602fa5cb11dab2395fa650b8674eaec35fc4a828"}, + {file = "exceptiongroup-1.0.4.tar.gz", hash = "sha256:bd14967b79cd9bdb54d97323216f8fdf533e278df937aa2a90089e7d6e06e5ec"}, +] frozenlist = [ {file = "frozenlist-1.3.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ff8bf625fe85e119553b5383ba0fb6aa3d0ec2ae980295aaefa552374926b3f4"}, {file = "frozenlist-1.3.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dfbac4c2dfcc082fcf8d942d1e49b6aa0766c19d3358bd86e2000bf0fa4a9cf0"}, @@ -1355,16 +1368,16 @@ frozenlist = [ {file = "frozenlist-1.3.3.tar.gz", hash = "sha256:58bcc55721e8a90b88332d6cd441261ebb22342e238296bb330968952fbb3a6a"}, ] google-api-core = [ - {file = "google-api-core-2.10.2.tar.gz", hash = "sha256:10c06f7739fe57781f87523375e8e1a3a4674bf6392cd6131a3222182b971320"}, - {file = "google_api_core-2.10.2-py3-none-any.whl", hash = "sha256:34f24bd1d5f72a8c4519773d99ca6bf080a6c4e041b4e9f024fe230191dda62e"}, + {file = "google-api-core-2.11.0.tar.gz", hash = "sha256:4b9bb5d5a380a0befa0573b302651b8a9a89262c1730e37bf423cec511804c22"}, + {file = "google_api_core-2.11.0-py3-none-any.whl", hash = "sha256:ce222e27b0de0d7bc63eb043b956996d6dccab14cc3b690aaea91c9cc99dc16e"}, ] google-api-python-client = [ - {file = "google-api-python-client-2.66.0.tar.gz", hash = "sha256:4cfaf0205aa7c538c8fb1772368be3d049dfed7886adf48597e9a766e9828a6e"}, - {file = "google_api_python_client-2.66.0-py2.py3-none-any.whl", hash = "sha256:3b45110b638232959f75418231dfb487228102a4a91a7a3e64147684befaebee"}, + {file = "google-api-python-client-2.68.0.tar.gz", hash = "sha256:19177411b7dcf8fcd66bff085c6838ecea5fd6b598998d594be1f7290dfc34b4"}, + {file = "google_api_python_client-2.68.0-py2.py3-none-any.whl", hash = "sha256:d4d317ccd365118f96d8d4b6a61aaba8fd414cf1a8617cb229386f2094013cea"}, ] google-auth = [ - {file = "google-auth-2.14.1.tar.gz", hash = "sha256:ccaa901f31ad5cbb562615eb8b664b3dd0bf5404a67618e642307f00613eda4d"}, - {file = "google_auth-2.14.1-py2.py3-none-any.whl", hash = "sha256:f5d8701633bebc12e0deea4df8abd8aff31c28b355360597f7f2ee60f2e4d016"}, + {file = "google-auth-2.15.0.tar.gz", hash = "sha256:72f12a6cfc968d754d7bdab369c5c5c16032106e52d32c6dfd8484e4c01a6d1f"}, + {file = "google_auth-2.15.0-py2.py3-none-any.whl", hash = "sha256:6897b93556d8d807ad70701bb89f000183aea366ca7ed94680828b37437a4994"}, ] google-auth-httplib2 = [ {file = "google-auth-httplib2-0.1.0.tar.gz", hash = "sha256:a07c39fd632becacd3f07718dfd6021bf396978f03ad3ce4321d060015cc30ac"}, @@ -1451,47 +1464,29 @@ isort = [ {file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"}, ] jsonschema = [ - {file = "jsonschema-4.17.0-py3-none-any.whl", hash = "sha256:f660066c3966db7d6daeaea8a75e0b68237a48e51cf49882087757bb59916248"}, - {file = "jsonschema-4.17.0.tar.gz", hash = "sha256:5bfcf2bca16a087ade17e02b282d34af7ccd749ef76241e7f9bd7c0cb8a9424d"}, + {file = "jsonschema-4.17.3-py3-none-any.whl", hash = "sha256:a870ad254da1a8ca84b6a2905cac29d265f805acc57af304784962a2aa6508f6"}, + {file = "jsonschema-4.17.3.tar.gz", hash = "sha256:0f864437ab8b6076ba6707453ef8f98a6a0d512a80e93f8abdb676f737ecb60d"}, ] lazy-object-proxy = [ - {file = "lazy-object-proxy-1.7.1.tar.gz", hash = "sha256:d609c75b986def706743cdebe5e47553f4a5a1da9c5ff66d76013ef396b5a8a4"}, - {file = "lazy_object_proxy-1.7.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bb8c5fd1684d60a9902c60ebe276da1f2281a318ca16c1d0a96db28f62e9166b"}, - {file = "lazy_object_proxy-1.7.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a57d51ed2997e97f3b8e3500c984db50a554bb5db56c50b5dab1b41339b37e36"}, - {file = "lazy_object_proxy-1.7.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd45683c3caddf83abbb1249b653a266e7069a09f486daa8863fb0e7496a9fdb"}, - {file = "lazy_object_proxy-1.7.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:8561da8b3dd22d696244d6d0d5330618c993a215070f473b699e00cf1f3f6443"}, - {file = "lazy_object_proxy-1.7.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fccdf7c2c5821a8cbd0a9440a456f5050492f2270bd54e94360cac663398739b"}, - {file = "lazy_object_proxy-1.7.1-cp310-cp310-win32.whl", hash = "sha256:898322f8d078f2654d275124a8dd19b079080ae977033b713f677afcfc88e2b9"}, - {file = "lazy_object_proxy-1.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:85b232e791f2229a4f55840ed54706110c80c0a210d076eee093f2b2e33e1bfd"}, - {file = "lazy_object_proxy-1.7.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:46ff647e76f106bb444b4533bb4153c7370cdf52efc62ccfc1a28bdb3cc95442"}, - {file = "lazy_object_proxy-1.7.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:12f3bb77efe1367b2515f8cb4790a11cffae889148ad33adad07b9b55e0ab22c"}, - {file = "lazy_object_proxy-1.7.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c19814163728941bb871240d45c4c30d33b8a2e85972c44d4e63dd7107faba44"}, - {file = "lazy_object_proxy-1.7.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:e40f2013d96d30217a51eeb1db28c9ac41e9d0ee915ef9d00da639c5b63f01a1"}, - {file = "lazy_object_proxy-1.7.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:2052837718516a94940867e16b1bb10edb069ab475c3ad84fd1e1a6dd2c0fcfc"}, - {file = "lazy_object_proxy-1.7.1-cp36-cp36m-win32.whl", hash = "sha256:6a24357267aa976abab660b1d47a34aaf07259a0c3859a34e536f1ee6e76b5bb"}, - {file = "lazy_object_proxy-1.7.1-cp36-cp36m-win_amd64.whl", hash = "sha256:6aff3fe5de0831867092e017cf67e2750c6a1c7d88d84d2481bd84a2e019ec35"}, - {file = "lazy_object_proxy-1.7.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:6a6e94c7b02641d1311228a102607ecd576f70734dc3d5e22610111aeacba8a0"}, - {file = "lazy_object_proxy-1.7.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4ce15276a1a14549d7e81c243b887293904ad2d94ad767f42df91e75fd7b5b6"}, - {file = "lazy_object_proxy-1.7.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e368b7f7eac182a59ff1f81d5f3802161932a41dc1b1cc45c1f757dc876b5d2c"}, - {file = "lazy_object_proxy-1.7.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:6ecbb350991d6434e1388bee761ece3260e5228952b1f0c46ffc800eb313ff42"}, - {file = "lazy_object_proxy-1.7.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:553b0f0d8dbf21890dd66edd771f9b1b5f51bd912fa5f26de4449bfc5af5e029"}, - {file = "lazy_object_proxy-1.7.1-cp37-cp37m-win32.whl", hash = "sha256:c7a683c37a8a24f6428c28c561c80d5f4fd316ddcf0c7cab999b15ab3f5c5c69"}, - {file = "lazy_object_proxy-1.7.1-cp37-cp37m-win_amd64.whl", hash = "sha256:df2631f9d67259dc9620d831384ed7732a198eb434eadf69aea95ad18c587a28"}, - {file = "lazy_object_proxy-1.7.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:07fa44286cda977bd4803b656ffc1c9b7e3bc7dff7d34263446aec8f8c96f88a"}, - {file = "lazy_object_proxy-1.7.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4dca6244e4121c74cc20542c2ca39e5c4a5027c81d112bfb893cf0790f96f57e"}, - {file = "lazy_object_proxy-1.7.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:91ba172fc5b03978764d1df5144b4ba4ab13290d7bab7a50f12d8117f8630c38"}, - {file = "lazy_object_proxy-1.7.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:043651b6cb706eee4f91854da4a089816a6606c1428fd391573ef8cb642ae4f7"}, - {file = "lazy_object_proxy-1.7.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b9e89b87c707dd769c4ea91f7a31538888aad05c116a59820f28d59b3ebfe25a"}, - {file = "lazy_object_proxy-1.7.1-cp38-cp38-win32.whl", hash = "sha256:9d166602b525bf54ac994cf833c385bfcc341b364e3ee71e3bf5a1336e677b55"}, - {file = "lazy_object_proxy-1.7.1-cp38-cp38-win_amd64.whl", hash = "sha256:8f3953eb575b45480db6568306893f0bd9d8dfeeebd46812aa09ca9579595148"}, - {file = "lazy_object_proxy-1.7.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:dd7ed7429dbb6c494aa9bc4e09d94b778a3579be699f9d67da7e6804c422d3de"}, - {file = "lazy_object_proxy-1.7.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:70ed0c2b380eb6248abdef3cd425fc52f0abd92d2b07ce26359fcbc399f636ad"}, - {file = "lazy_object_proxy-1.7.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7096a5e0c1115ec82641afbdd70451a144558ea5cf564a896294e346eb611be1"}, - {file = "lazy_object_proxy-1.7.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f769457a639403073968d118bc70110e7dce294688009f5c24ab78800ae56dc8"}, - {file = "lazy_object_proxy-1.7.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:39b0e26725c5023757fc1ab2a89ef9d7ab23b84f9251e28f9cc114d5b59c1b09"}, - {file = "lazy_object_proxy-1.7.1-cp39-cp39-win32.whl", hash = "sha256:2130db8ed69a48a3440103d4a520b89d8a9405f1b06e2cc81640509e8bf6548f"}, - {file = "lazy_object_proxy-1.7.1-cp39-cp39-win_amd64.whl", hash = "sha256:677ea950bef409b47e51e733283544ac3d660b709cfce7b187f5ace137960d61"}, - {file = "lazy_object_proxy-1.7.1-pp37.pp38-none-any.whl", hash = "sha256:d66906d5785da8e0be7360912e99c9188b70f52c422f9fc18223347235691a84"}, + {file = "lazy-object-proxy-1.8.0.tar.gz", hash = "sha256:c219a00245af0f6fa4e95901ed28044544f50152840c5b6a3e7b2568db34d156"}, + {file = "lazy_object_proxy-1.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4fd031589121ad46e293629b39604031d354043bb5cdf83da4e93c2d7f3389fe"}, + {file = "lazy_object_proxy-1.8.0-cp310-cp310-win32.whl", hash = "sha256:b70d6e7a332eb0217e7872a73926ad4fdc14f846e85ad6749ad111084e76df25"}, + {file = "lazy_object_proxy-1.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:eb329f8d8145379bf5dbe722182410fe8863d186e51bf034d2075eb8d85ee25b"}, + {file = "lazy_object_proxy-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4e2d9f764f1befd8bdc97673261b8bb888764dfdbd7a4d8f55e4fbcabb8c3fb7"}, + {file = "lazy_object_proxy-1.8.0-cp311-cp311-win32.whl", hash = "sha256:e20bfa6db17a39c706d24f82df8352488d2943a3b7ce7d4c22579cb89ca8896e"}, + {file = "lazy_object_proxy-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:14010b49a2f56ec4943b6cf925f597b534ee2fe1f0738c84b3bce0c1a11ff10d"}, + {file = "lazy_object_proxy-1.8.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:6850e4aeca6d0df35bb06e05c8b934ff7c533734eb51d0ceb2d63696f1e6030c"}, + {file = "lazy_object_proxy-1.8.0-cp37-cp37m-win32.whl", hash = "sha256:5b51d6f3bfeb289dfd4e95de2ecd464cd51982fe6f00e2be1d0bf94864d58acd"}, + {file = "lazy_object_proxy-1.8.0-cp37-cp37m-win_amd64.whl", hash = "sha256:6f593f26c470a379cf7f5bc6db6b5f1722353e7bf937b8d0d0b3fba911998858"}, + {file = "lazy_object_proxy-1.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0c1c7c0433154bb7c54185714c6929acc0ba04ee1b167314a779b9025517eada"}, + {file = "lazy_object_proxy-1.8.0-cp38-cp38-win32.whl", hash = "sha256:d176f392dbbdaacccf15919c77f526edf11a34aece58b55ab58539807b85436f"}, + {file = "lazy_object_proxy-1.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:afcaa24e48bb23b3be31e329deb3f1858f1f1df86aea3d70cb5c8578bfe5261c"}, + {file = "lazy_object_proxy-1.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:71d9ae8a82203511a6f60ca5a1b9f8ad201cac0fc75038b2dc5fa519589c9288"}, + {file = "lazy_object_proxy-1.8.0-cp39-cp39-win32.whl", hash = "sha256:8f6ce2118a90efa7f62dd38c7dbfffd42f468b180287b748626293bf12ed468f"}, + {file = "lazy_object_proxy-1.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:eac3a9a5ef13b332c059772fd40b4b1c3d45a3a2b05e33a361dee48e54a4dad0"}, + {file = "lazy_object_proxy-1.8.0-pp37-pypy37_pp73-any.whl", hash = "sha256:ae032743794fba4d171b5b67310d69176287b5bf82a21f588282406a79498891"}, + {file = "lazy_object_proxy-1.8.0-pp38-pypy38_pp73-any.whl", hash = "sha256:7e1561626c49cb394268edd00501b289053a652ed762c58e1081224c8d881cec"}, + {file = "lazy_object_proxy-1.8.0-pp39-pypy39_pp73-any.whl", hash = "sha256:ce58b2b3734c73e68f0e30e4e725264d4d6be95818ec0a0be4bb6bf9a7e79aa8"}, ] mccabe = [ {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, @@ -1621,16 +1616,16 @@ packaging = [ {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, ] pathspec = [ - {file = "pathspec-0.10.1-py3-none-any.whl", hash = "sha256:46846318467efc4556ccfd27816e004270a9eeeeb4d062ce5e6fc7a87c573f93"}, - {file = "pathspec-0.10.1.tar.gz", hash = "sha256:7ace6161b621d31e7902eb6b5ae148d12cfd23f4a249b9ffb6b9fee12084323d"}, + {file = "pathspec-0.10.2-py3-none-any.whl", hash = "sha256:88c2606f2c1e818b978540f73ecc908e13999c6c3a383daf3705652ae79807a5"}, + {file = "pathspec-0.10.2.tar.gz", hash = "sha256:8f6bf73e5758fd365ef5d58ce09ac7c27d2833a8d7da51712eac6e27e35141b0"}, ] pkgutil_resolve_name = [ {file = "pkgutil_resolve_name-1.3.10-py3-none-any.whl", hash = "sha256:ca27cc078d25c5ad71a9de0a7a330146c4e014c2462d9af19c6b828280649c5e"}, {file = "pkgutil_resolve_name-1.3.10.tar.gz", hash = "sha256:357d6c9e6a755653cfd78893817c0853af365dd51ec97f3d358a819373bbd174"}, ] platformdirs = [ - {file = "platformdirs-2.5.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"}, - {file = "platformdirs-2.5.2.tar.gz", hash = "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"}, + {file = "platformdirs-2.5.4-py3-none-any.whl", hash = "sha256:af0276409f9a02373d540bf8480021a048711d572745aef4b7842dad245eba10"}, + {file = "platformdirs-2.5.4.tar.gz", hash = "sha256:1006647646d80f16130f052404c6b901e80ee4ed6bef6792e1f238a8969106f7"}, ] pluggy = [ {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, @@ -1661,10 +1656,6 @@ protobuf = [ {file = "protobuf-3.20.3-py2.py3-none-any.whl", hash = "sha256:a7ca6d488aa8ff7f329d4c545b2dbad8ac31464f1d8b1c87ad1346717731e4db"}, {file = "protobuf-3.20.3.tar.gz", hash = "sha256:2e3427429c9cffebf259491be0af70189607f365c2f41c7c3764af6f337105f2"}, ] -py = [ - {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, - {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, -] py-sr25519-bindings = [ {file = "py_sr25519_bindings-0.2.0-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:86cc1a571852a4f2ade827ebf211e066b23ab805d3e864cbe213a3d8cd53f7d5"}, {file = "py_sr25519_bindings-0.2.0-cp310-cp310-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:453c9088e39dd04b07bf3ada6c473a5349c4dfd965009a35124b2c807117eda8"}, @@ -1764,36 +1755,32 @@ pycparser = [ {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, ] pycryptodome = [ - {file = "pycryptodome-3.15.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:ff7ae90e36c1715a54446e7872b76102baa5c63aa980917f4aa45e8c78d1a3ec"}, - {file = "pycryptodome-3.15.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:2ffd8b31561455453ca9f62cb4c24e6b8d119d6d531087af5f14b64bee2c23e6"}, - {file = "pycryptodome-3.15.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:2ea63d46157386c5053cfebcdd9bd8e0c8b7b0ac4a0507a027f5174929403884"}, - {file = "pycryptodome-3.15.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:7c9ed8aa31c146bef65d89a1b655f5f4eab5e1120f55fc297713c89c9e56ff0b"}, - {file = "pycryptodome-3.15.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:5099c9ca345b2f252f0c28e96904643153bae9258647585e5e6f649bb7a1844a"}, - {file = "pycryptodome-3.15.0-cp27-cp27m-manylinux2014_aarch64.whl", hash = "sha256:2ec709b0a58b539a4f9d33fb8508264c3678d7edb33a68b8906ba914f71e8c13"}, - {file = "pycryptodome-3.15.0-cp27-cp27m-win32.whl", hash = "sha256:fd2184aae6ee2a944aaa49113e6f5787cdc5e4db1eb8edb1aea914bd75f33a0c"}, - {file = "pycryptodome-3.15.0-cp27-cp27m-win_amd64.whl", hash = "sha256:7e3a8f6ee405b3bd1c4da371b93c31f7027944b2bcce0697022801db93120d83"}, - {file = "pycryptodome-3.15.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:b9c5b1a1977491533dfd31e01550ee36ae0249d78aae7f632590db833a5012b8"}, - {file = "pycryptodome-3.15.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:0926f7cc3735033061ef3cf27ed16faad6544b14666410727b31fea85a5b16eb"}, - {file = "pycryptodome-3.15.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:2aa55aae81f935a08d5a3c2042eb81741a43e044bd8a81ea7239448ad751f763"}, - {file = "pycryptodome-3.15.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:c3640deff4197fa064295aaac10ab49a0d55ef3d6a54ae1499c40d646655c89f"}, - {file = "pycryptodome-3.15.0-cp27-cp27mu-manylinux2014_aarch64.whl", hash = "sha256:045d75527241d17e6ef13636d845a12e54660aa82e823b3b3341bcf5af03fa79"}, - {file = "pycryptodome-3.15.0-cp35-abi3-macosx_10_9_x86_64.whl", hash = "sha256:9ee40e2168f1348ae476676a2e938ca80a2f57b14a249d8fe0d3cdf803e5a676"}, - {file = "pycryptodome-3.15.0-cp35-abi3-manylinux1_i686.whl", hash = "sha256:4c3ccad74eeb7b001f3538643c4225eac398c77d617ebb3e57571a897943c667"}, - {file = "pycryptodome-3.15.0-cp35-abi3-manylinux1_x86_64.whl", hash = "sha256:1b22bcd9ec55e9c74927f6b1f69843cb256fb5a465088ce62837f793d9ffea88"}, - {file = "pycryptodome-3.15.0-cp35-abi3-manylinux2010_i686.whl", hash = "sha256:57f565acd2f0cf6fb3e1ba553d0cb1f33405ec1f9c5ded9b9a0a5320f2c0bd3d"}, - {file = "pycryptodome-3.15.0-cp35-abi3-manylinux2010_x86_64.whl", hash = "sha256:4b52cb18b0ad46087caeb37a15e08040f3b4c2d444d58371b6f5d786d95534c2"}, - {file = "pycryptodome-3.15.0-cp35-abi3-manylinux2014_aarch64.whl", hash = "sha256:092a26e78b73f2530b8bd6b3898e7453ab2f36e42fd85097d705d6aba2ec3e5e"}, - {file = "pycryptodome-3.15.0-cp35-abi3-win32.whl", hash = "sha256:e244ab85c422260de91cda6379e8e986405b4f13dc97d2876497178707f87fc1"}, - {file = "pycryptodome-3.15.0-cp35-abi3-win_amd64.whl", hash = "sha256:c77126899c4b9c9827ddf50565e93955cb3996813c18900c16b2ea0474e130e9"}, - {file = "pycryptodome-3.15.0-pp27-pypy_73-macosx_10_9_x86_64.whl", hash = "sha256:9eaadc058106344a566dc51d3d3a758ab07f8edde013712bc8d22032a86b264f"}, - {file = "pycryptodome-3.15.0-pp27-pypy_73-manylinux1_x86_64.whl", hash = "sha256:ff287bcba9fbeb4f1cccc1f2e90a08d691480735a611ee83c80a7d74ad72b9d9"}, - {file = "pycryptodome-3.15.0-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:60b4faae330c3624cc5a546ba9cfd7b8273995a15de94ee4538130d74953ec2e"}, - {file = "pycryptodome-3.15.0-pp27-pypy_73-win32.whl", hash = "sha256:a8f06611e691c2ce45ca09bbf983e2ff2f8f4f87313609d80c125aff9fad6e7f"}, - {file = "pycryptodome-3.15.0-pp36-pypy36_pp73-macosx_10_9_x86_64.whl", hash = "sha256:b9cc96e274b253e47ad33ae1fccc36ea386f5251a823ccb50593a935db47fdd2"}, - {file = "pycryptodome-3.15.0-pp36-pypy36_pp73-manylinux1_x86_64.whl", hash = "sha256:ecaaef2d21b365d9c5ca8427ffc10cebed9d9102749fd502218c23cb9a05feb5"}, - {file = "pycryptodome-3.15.0-pp36-pypy36_pp73-manylinux2010_x86_64.whl", hash = "sha256:d2a39a66057ab191e5c27211a7daf8f0737f23acbf6b3562b25a62df65ffcb7b"}, - {file = "pycryptodome-3.15.0-pp36-pypy36_pp73-win32.whl", hash = "sha256:9c772c485b27967514d0df1458b56875f4b6d025566bf27399d0c239ff1b369f"}, - {file = "pycryptodome-3.15.0.tar.gz", hash = "sha256:9135dddad504592bcc18b0d2d95ce86c3a5ea87ec6447ef25cfedea12d6018b8"}, + {file = "pycryptodome-3.16.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:e061311b02cefb17ea93d4a5eb1ad36dca4792037078b43e15a653a0a4478ead"}, + {file = "pycryptodome-3.16.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:dab9359cc295160ba96738ba4912c675181c84bfdf413e5c0621cf00b7deeeaa"}, + {file = "pycryptodome-3.16.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:0198fe96c22f7bc31e7a7c27a26b2cec5af3cf6075d577295f4850856c77af32"}, + {file = "pycryptodome-3.16.0-cp27-cp27m-manylinux2014_aarch64.whl", hash = "sha256:58172080cbfaee724067a3c017add6a1a3cc167bbc8478dc5f2e5f45fa658763"}, + {file = "pycryptodome-3.16.0-cp27-cp27m-win32.whl", hash = "sha256:4d950ed2a887905b3fa709b86be5a163e26e1b174703ed59d34eb6832f213222"}, + {file = "pycryptodome-3.16.0-cp27-cp27m-win_amd64.whl", hash = "sha256:c69e19afc734b2a17b9d78b7bcb544aabd5a52ff628e14283b6e9404d27d0517"}, + {file = "pycryptodome-3.16.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:1fc16c80a5da8231fd1f953a7b8dfeb415f68120248e8d68383c5c2c4b18708c"}, + {file = "pycryptodome-3.16.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:5df582f2112dd72331de7e567837e136a9629181a8ab69ef8949e4bc294a0b99"}, + {file = "pycryptodome-3.16.0-cp27-cp27mu-manylinux2014_aarch64.whl", hash = "sha256:2bf2a270906a02b7b255e1a0d7b3aea4f06b3983c51ddec1673c380e0dff5b30"}, + {file = "pycryptodome-3.16.0-cp35-abi3-macosx_10_9_x86_64.whl", hash = "sha256:b12a88566a98617b1a34b4e5a805dff2da98d83fc74262aff3c3d724d0f525d6"}, + {file = "pycryptodome-3.16.0-cp35-abi3-manylinux2014_aarch64.whl", hash = "sha256:69adf32522b75968e1cbf25b5d83e87c04cd9a55610ce1e4a19012e58e7e4023"}, + {file = "pycryptodome-3.16.0-cp35-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:d67a2d2fe344953e4572a7d30668cceb516b04287b8638170d562065e53ee2e0"}, + {file = "pycryptodome-3.16.0-cp35-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e750a21d8a265b1f9bfb1a28822995ea33511ba7db5e2b55f41fb30781d0d073"}, + {file = "pycryptodome-3.16.0-cp35-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:47c71a0347847b747ba1349767b16cde049bc36f21654eb09cc82306ef5fdcf8"}, + {file = "pycryptodome-3.16.0-cp35-abi3-musllinux_1_1_i686.whl", hash = "sha256:856ebf822d08d754af62c22e2b93626509a72773214f92db1551e2b68d9e2a1b"}, + {file = "pycryptodome-3.16.0-cp35-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:6016269bb56caf0327f6d42e7bad1247e08b78407446dff562240c65f85d5a5e"}, + {file = "pycryptodome-3.16.0-cp35-abi3-win32.whl", hash = "sha256:1047ac2b9847ae84ea454e6e20db7dcb755a81c1b1631a879213d2b0ad835ff2"}, + {file = "pycryptodome-3.16.0-cp35-abi3-win_amd64.whl", hash = "sha256:13b3e610a2f8938c61a90b20625069ab7a77ccea20d65a9a0f926cc0cc1314b1"}, + {file = "pycryptodome-3.16.0-pp27-pypy_73-macosx_10_9_x86_64.whl", hash = "sha256:265bfcbbf20d58e6871ce695a7a08aac9b41a0553060d9c05363abd6f3391bdd"}, + {file = "pycryptodome-3.16.0-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:54d807314c66785c69cd25425933d4bd4c23547a593cdcf49d962fa3e0081336"}, + {file = "pycryptodome-3.16.0-pp27-pypy_73-win32.whl", hash = "sha256:63165fbdc247450017eb9ef04cfe15cb3a72ca48ffcc3a3b75b08c0340bf3647"}, + {file = "pycryptodome-3.16.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:95069fd9e2813668a2713a1efcc65cc26d2c7e741401ac46628f1ec957511f1b"}, + {file = "pycryptodome-3.16.0-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:d1daec4d31bb00918e4e178297ac6ca6f86ec4c851ba584770533ece554d29e2"}, + {file = "pycryptodome-3.16.0-pp37-pypy37_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:48d99869d58f3979d72f6fa0c50f48d16f14973bc4a3adb0ce3b8325fdd7e223"}, + {file = "pycryptodome-3.16.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:c82e3bc1e70dde153b0956bffe20a15715a1fe3e00bc23e88d6973eda4505944"}, + {file = "pycryptodome-3.16.0.tar.gz", hash = "sha256:0e45d2d852a66ecfb904f090c3f87dc0dfb89a499570abad8590f10d9cffb350"}, ] pydantic = [ {file = "pydantic-1.10.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bb6ad4489af1bac6955d38ebcb95079a836af31e4c4f74aba1ca05bb9f6027bd"}, @@ -1834,8 +1821,8 @@ pydantic = [ {file = "pydantic-1.10.2.tar.gz", hash = "sha256:91b8e218852ef6007c2b98cd861601c6a09f1aa32bbbb74fab5b1c33d4a1e410"}, ] pylint = [ - {file = "pylint-2.15.3-py3-none-any.whl", hash = "sha256:7f6aad1d8d50807f7bc64f89ac75256a9baf8e6ed491cc9bc65592bc3f462cf1"}, - {file = "pylint-2.15.3.tar.gz", hash = "sha256:5fdfd44af182866999e6123139d265334267339f29961f00c89783155eacc60b"}, + {file = "pylint-2.15.7-py3-none-any.whl", hash = "sha256:1d561d1d3e8be9dd880edc685162fbdaa0409c88b9b7400873c0cf345602e326"}, + {file = "pylint-2.15.7.tar.gz", hash = "sha256:91e4776dbcb4b4d921a3e4b6fec669551107ba11f29d9199154a01622e460a57"}, ] PyNaCl = [ {file = "PyNaCl-1.5.0-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:401002a4aaa07c9414132aaed7f6836ff98f59277a234704ff66878c2ee4a0d1"}, @@ -1882,8 +1869,8 @@ pyrsistent = [ {file = "pyrsistent-0.19.2.tar.gz", hash = "sha256:bfa0351be89c9fcbcb8c9879b826f4353be10f58f8a677efab0c017bf7137ec2"}, ] pytest = [ - {file = "pytest-7.1.3-py3-none-any.whl", hash = "sha256:1377bda3466d70b55e3f5cecfa55bb7cfcf219c7964629b967c37cf0bda818b7"}, - {file = "pytest-7.1.3.tar.gz", hash = "sha256:4f365fec2dff9c1162f834d9f18af1ba13062db0c708bf7b946f8a5c76180c39"}, + {file = "pytest-7.2.0-py3-none-any.whl", hash = "sha256:892f933d339f068883b6fd5a459f03d85bfcb355e4981e146d2c7616c21fef71"}, + {file = "pytest-7.2.0.tar.gz", hash = "sha256:c4014eb40e10f11f355ad4e3c2fb2c6c6d1919c73f3b5a433de4708202cade59"}, ] pytz = [ {file = "pytz-2022.6-py2.py3-none-any.whl", hash = "sha256:222439474e9c98fced559f1709d89e6c9cbf8d79c794ff3eb9f8800064291427"}, @@ -1906,24 +1893,24 @@ tomli = [ {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, ] tomlkit = [ - {file = "tomlkit-0.11.5-py3-none-any.whl", hash = "sha256:f2ef9da9cef846ee027947dc99a45d6b68a63b0ebc21944649505bf2e8bc5fe7"}, - {file = "tomlkit-0.11.5.tar.gz", hash = "sha256:571854ebbb5eac89abcb4a2e47d7ea27b89bf29e09c35395da6f03dd4ae23d1c"}, + {file = "tomlkit-0.11.6-py3-none-any.whl", hash = "sha256:07de26b0d8cfc18f871aec595fda24d95b08fef89d147caa861939f37230bf4b"}, + {file = "tomlkit-0.11.6.tar.gz", hash = "sha256:71b952e5721688937fb02cf9d354dbcf0785066149d2855e44531ebdd2b65d73"}, ] tortoise-orm = [ {file = "tortoise-orm-0.19.2.tar.gz", hash = "sha256:bff4d79abfca7fb805972bb2438e8e0cd2e6590bc2cfd7593a803518a027bbf0"}, {file = "tortoise_orm-0.19.2-py3-none-any.whl", hash = "sha256:a99b8c9f42d5cd49493c70471b5c9a5df8ecf49cc624f2f41b9dc75bba993ac5"}, ] typing-extensions = [ - {file = "typing_extensions-4.3.0-py3-none-any.whl", hash = "sha256:25642c956049920a5aa49edcdd6ab1e06d7e5d467fc00e0506c44ac86fbfca02"}, - {file = "typing_extensions-4.3.0.tar.gz", hash = "sha256:e6d2677a32f47fc7eb2795db1dd15c1f34eff616bcaf2cfb5e997f854fa1c4a6"}, + {file = "typing_extensions-4.4.0-py3-none-any.whl", hash = "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e"}, + {file = "typing_extensions-4.4.0.tar.gz", hash = "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa"}, ] uritemplate = [ {file = "uritemplate-4.1.1-py2.py3-none-any.whl", hash = "sha256:830c08b8d99bdd312ea4ead05994a38e8936266f84b9a7878232db50b044e02e"}, {file = "uritemplate-4.1.1.tar.gz", hash = "sha256:4346edfc5c3b79f694bccd6d6099a322bbeb628dbf2cd86eea55a456ce5124f0"}, ] urllib3 = [ - {file = "urllib3-1.26.12-py2.py3-none-any.whl", hash = "sha256:b930dd878d5a8afb066a637fbb35144fe7901e3b209d1cd4f524bd0e9deee997"}, - {file = "urllib3-1.26.12.tar.gz", hash = "sha256:3fa96cf423e6987997fc326ae8df396db2a8b7c667747d47ddd8ecba91f4a74e"}, + {file = "urllib3-1.26.13-py2.py3-none-any.whl", hash = "sha256:47cc05d99aaa09c9e72ed5809b60e7ba354e64b59c9c173ac3018642d8bb41fc"}, + {file = "urllib3-1.26.13.tar.gz", hash = "sha256:c083dd0dce68dbfbe1129d5271cb90f9447dea7d52097c6e0126120c521ddea8"}, ] uvicorn = [ {file = "uvicorn-0.19.0-py3-none-any.whl", hash = "sha256:cc277f7e73435748e69e075a721841f7c4a95dba06d12a72fe9874acced16f6f"}, @@ -2061,6 +2048,6 @@ yarl = [ {file = "yarl-1.8.1.tar.gz", hash = "sha256:af887845b8c2e060eb5605ff72b6f2dd2aab7a761379373fd89d314f4752abbf"}, ] zipp = [ - {file = "zipp-3.10.0-py3-none-any.whl", hash = "sha256:4fcb6f278987a6605757302a6e40e896257570d11c51628968ccb2a47e80c6c1"}, - {file = "zipp-3.10.0.tar.gz", hash = "sha256:7a7262fd930bd3e36c50b9a64897aec3fafff3dfdeec9623ae22b40e93f99bb8"}, + {file = "zipp-3.11.0-py3-none-any.whl", hash = "sha256:83a28fcb75844b5c0cdaf5aa4003c2d728c77e05f5aeabe8e95e56727005fbaa"}, + {file = "zipp-3.11.0.tar.gz", hash = "sha256:a7a22e05929290a67401440b39690ae6563279bced5f314609d9d03798f56766"}, ] diff --git a/pyproject.toml b/pyproject.toml index 5b926fb3..259adb74 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,13 +14,15 @@ apispec = "^6.0.2" uvicorn = "^0.19.0" aiohttp = "^3.8.3" cosmpy = "^0.6.0" -tortoise-orm = "^0.19.2" [tool.poetry.dev-dependencies] black = "^22.8.0" pytest = "^7.1.3" pylint = "^2.15.3" +[tool.poetry.group.orm.dependencies] +tortoise-orm = "^0.19.2" + [build-system] requires = ["poetry-core>=1.0.0"] build-backend = "poetry.core.masonry.api" \ No newline at end of file From 35768159c675f15cf9a80db11f6d4ac94133eea6 Mon Sep 17 00:00:00 2001 From: James Riehl Date: Fri, 2 Dec 2022 15:32:42 +0000 Subject: [PATCH 23/41] feat: add on event handlers --- examples/10-cleaning-demo/cleaner.py | 9 ++++--- src/nexus/__init__.py | 2 +- src/nexus/agent.py | 40 +++++++++++++++++++++++++--- src/nexus/context.py | 7 +++++ 4 files changed, 51 insertions(+), 7 deletions(-) diff --git a/examples/10-cleaning-demo/cleaner.py b/examples/10-cleaning-demo/cleaner.py index 2306f388..0eaa1c1a 100644 --- a/examples/10-cleaning-demo/cleaner.py +++ b/examples/10-cleaning-demo/cleaner.py @@ -3,7 +3,7 @@ from protocols.cleaning import cleaning_proto from protocols.cleaning.models import Availability, Provider, Service, ServiceType -from nexus import Agent +from nexus import Agent, EventType from nexus.setup import fund_agent_if_low @@ -21,8 +21,8 @@ # build the cleaning service agent from the cleaning protocol cleaner.include(cleaning_proto) - -async def init_db(): +@cleaner.on_event(EventType.STARTUP) +async def startup(): await Tortoise.init(db_url="sqlite://db.sqlite3", modules={"models": ["__main__"]}) await Tortoise.generate_schemas() @@ -44,6 +44,9 @@ async def init_db(): min_hourly_price=5, ) +@cleaner.on_event(EventType.SHUTDOWN) +async def shutdown(): + await Tortoise.close_connections() if __name__ == "__main__": run_async(init_db()) diff --git a/src/nexus/__init__.py b/src/nexus/__init__.py index ecf464fa..ba114593 100644 --- a/src/nexus/__init__.py +++ b/src/nexus/__init__.py @@ -1,4 +1,4 @@ -from .context import Context +from .context import Context, EventType from .protocol import Protocol from .models import Model from .agent import Agent, Bureau diff --git a/src/nexus/agent.py b/src/nexus/agent.py index 9ed9a068..42f8ff4e 100644 --- a/src/nexus/agent.py +++ b/src/nexus/agent.py @@ -1,12 +1,12 @@ import asyncio import functools import logging -from typing import Optional, List, Set, Tuple, Any, Union +from typing import Callable, Optional, List, Set, Tuple, Any, Union from cosmpy.aerial.wallet import LocalWallet, PrivateKey from nexus.asgi import ASGIServer -from nexus.context import Context, IntervalCallback, MessageCallback, MsgDigest +from nexus.context import Context, EventCallback, IntervalCallback, MessageCallback, EventType, MsgDigest from nexus.crypto import Identity, derive_key_from_seed from nexus.dispatch import Sink, dispatcher from nexus.models import Model @@ -86,6 +86,8 @@ def __init__( ) self._dispatcher = dispatcher self._message_queue = asyncio.Queue() + self._on_startup = [] + self._on_shutdown = [] self._version = version or "0.1.0" # initialize the internal agent protocol @@ -202,6 +204,26 @@ def on_message( ): return self._protocol.on_message(model, replies) + def on_event(self, event_type: EventType) -> EventCallback: + def decorator_on_event(func: EventCallback) -> EventCallback: + @functools.wraps(func) + def handler(*args, **kwargs): + return func(*args, **kwargs) + + self._add_event_handler(event_type, func) + + return handler + + return decorator_on_event + + def _add_event_handler( + self, event_type: str, func: EventCallback, + ) -> None: + if event_type == EventType.STARTUP: + self._on_startup.append(func) + elif event_type ==EventType.SHUTDOWN: + self._on_shutdown.append(func) + def include(self, protocol: Protocol): for func, period in protocol.intervals: task = self._loop.create_task(_run_interval(func, self._ctx, period)) @@ -235,6 +257,14 @@ def include(self, protocol: Protocol): async def handle_message(self, sender, schema_digest: str, message: Any): await self._message_queue.put((schema_digest, sender, message)) + async def startup(self): + for handler in self.on_startup: + await handler() + + async def shutdown(self): + for handler in self.on_shutdown: + await handler() + def setup(self): # register the internal agent protocol self.include(self._protocol) @@ -246,7 +276,11 @@ def setup(self): def run(self): self.setup() - self._loop.run_until_complete(self._server.serve()) + self._loop.run_until_complete(self.startup()) + try: + self._loop.run_until_complete(self._server.serve()) + finally: + self._loop.run_until_complete(self.shutdown()) async def _process_message_queue(self): while True: diff --git a/src/nexus/context.py b/src/nexus/context.py index d995d5ab..85791392 100644 --- a/src/nexus/context.py +++ b/src/nexus/context.py @@ -1,6 +1,7 @@ import logging import uuid from dataclasses import dataclass +from enum import Enum from typing import Dict, Set, Optional, Callable, Any, Awaitable import aiohttp @@ -16,6 +17,12 @@ IntervalCallback = Callable[["Context"], Awaitable[None]] MessageCallback = Callable[["Context", str, Any], Awaitable[None]] +EventCallback = Callable[["Context"], Awaitable[None]] + + +class EventType(Enum): + STARTUP = 0 + SHUTDOWN = 1 @dataclass From bb7e3a52dcfb6efd30d36d2c7422cb6ce27d8116 Mon Sep 17 00:00:00 2001 From: James Riehl Date: Fri, 2 Dec 2022 15:58:24 +0000 Subject: [PATCH 24/41] fix: startup name --- examples/10-cleaning-demo/cleaner.py | 6 ++++-- src/nexus/agent.py | 23 ++++++++++++++++------- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/examples/10-cleaning-demo/cleaner.py b/examples/10-cleaning-demo/cleaner.py index 0eaa1c1a..af38b895 100644 --- a/examples/10-cleaning-demo/cleaner.py +++ b/examples/10-cleaning-demo/cleaner.py @@ -1,4 +1,4 @@ -from tortoise import Tortoise, run_async +from tortoise import Tortoise from protocols.cleaning import cleaning_proto from protocols.cleaning.models import Availability, Provider, Service, ServiceType @@ -21,6 +21,7 @@ # build the cleaning service agent from the cleaning protocol cleaner.include(cleaning_proto) + @cleaner.on_event(EventType.STARTUP) async def startup(): await Tortoise.init(db_url="sqlite://db.sqlite3", modules={"models": ["__main__"]}) @@ -44,10 +45,11 @@ async def startup(): min_hourly_price=5, ) + @cleaner.on_event(EventType.SHUTDOWN) async def shutdown(): await Tortoise.close_connections() + if __name__ == "__main__": - run_async(init_db()) cleaner.run() diff --git a/src/nexus/agent.py b/src/nexus/agent.py index 42f8ff4e..92b73310 100644 --- a/src/nexus/agent.py +++ b/src/nexus/agent.py @@ -1,12 +1,19 @@ import asyncio import functools import logging -from typing import Callable, Optional, List, Set, Tuple, Any, Union +from typing import Optional, List, Set, Tuple, Any, Union from cosmpy.aerial.wallet import LocalWallet, PrivateKey from nexus.asgi import ASGIServer -from nexus.context import Context, EventCallback, IntervalCallback, MessageCallback, EventType, MsgDigest +from nexus.context import ( + Context, + EventCallback, + IntervalCallback, + MessageCallback, + EventType, + MsgDigest, +) from nexus.crypto import Identity, derive_key_from_seed from nexus.dispatch import Sink, dispatcher from nexus.models import Model @@ -217,11 +224,13 @@ def handler(*args, **kwargs): return decorator_on_event def _add_event_handler( - self, event_type: str, func: EventCallback, - ) -> None: + self, + event_type: str, + func: EventCallback, + ) -> None: if event_type == EventType.STARTUP: self._on_startup.append(func) - elif event_type ==EventType.SHUTDOWN: + elif event_type == EventType.SHUTDOWN: self._on_shutdown.append(func) def include(self, protocol: Protocol): @@ -258,11 +267,11 @@ async def handle_message(self, sender, schema_digest: str, message: Any): await self._message_queue.put((schema_digest, sender, message)) async def startup(self): - for handler in self.on_startup: + for handler in self._on_startup: await handler() async def shutdown(self): - for handler in self.on_shutdown: + for handler in self._on_shutdown: await handler() def setup(self): From 95f80a9b8513f0e18f7574fa9fc0e2c6dec0f1e2 Mon Sep 17 00:00:00 2001 From: James Riehl Date: Tue, 6 Dec 2022 14:43:47 +0000 Subject: [PATCH 25/41] feat: add user logic and fix db init --- examples/10-cleaning-demo/cleaner.py | 4 +++- examples/10-cleaning-demo/protocols/cleaning/__init__.py | 9 ++++++++- examples/10-cleaning-demo/protocols/cleaning/models.py | 1 + examples/10-cleaning-demo/user.py | 1 + 4 files changed, 13 insertions(+), 2 deletions(-) diff --git a/examples/10-cleaning-demo/cleaner.py b/examples/10-cleaning-demo/cleaner.py index af38b895..93e1165d 100644 --- a/examples/10-cleaning-demo/cleaner.py +++ b/examples/10-cleaning-demo/cleaner.py @@ -24,7 +24,9 @@ @cleaner.on_event(EventType.STARTUP) async def startup(): - await Tortoise.init(db_url="sqlite://db.sqlite3", modules={"models": ["__main__"]}) + await Tortoise.init( + db_url="sqlite://db.sqlite3", modules={"models": ["protocols.cleaning.models"]} + ) await Tortoise.generate_schemas() provider = await Provider.create(name=cleaner.name, address=12) diff --git a/examples/10-cleaning-demo/protocols/cleaning/__init__.py b/examples/10-cleaning-demo/protocols/cleaning/__init__.py index deb922d4..2d0d7d55 100644 --- a/examples/10-cleaning-demo/protocols/cleaning/__init__.py +++ b/examples/10-cleaning-demo/protocols/cleaning/__init__.py @@ -2,7 +2,7 @@ from nexus import Context, Model, Protocol -from .models import Provider, Availability +from .models import Provider, Availability, User PROTOCOL_NAME = "cleaning" @@ -10,6 +10,7 @@ class ServiceRequest(Model): + user: str address: int time_start: int duration: int @@ -51,6 +52,9 @@ async def handle_query_request(ctx: Context, sender: str, msg: ServiceRequest): services = [int(service.type) for service in await provider.services] markup = provider.markup + user, _ = await User.get_or_create(name=msg.user, address=sender) + print(f"Received service request from user `{user.name}`") + if ( set(msg.services) <= set(services) and in_service_region(msg.address, availability, provider) @@ -76,6 +80,9 @@ async def handle_book_request(ctx: Context, sender: str, msg: ServiceBooking): availability = await Availability.get(provider=provider) services = [int(service.type) for service in await provider.services] + user = await User.get(address=sender) + print(f"Received booking request from user `{user.name}`") + success = ( set(msg.services) <= set(services) and availability.time_start <= msg.time_start diff --git a/examples/10-cleaning-demo/protocols/cleaning/models.py b/examples/10-cleaning-demo/protocols/cleaning/models.py index 39e31f6c..93ffe56b 100644 --- a/examples/10-cleaning-demo/protocols/cleaning/models.py +++ b/examples/10-cleaning-demo/protocols/cleaning/models.py @@ -14,6 +14,7 @@ class ServiceType(int, Enum): class User(models.Model): id = fields.IntField(pk=True) name = fields.CharField(max_length=64) + address = fields.CharField(max_length=100) created_at = fields.DatetimeField(auto_now_add=True) diff --git a/examples/10-cleaning-demo/user.py b/examples/10-cleaning-demo/user.py index 81f7b1bd..2b188f8c 100644 --- a/examples/10-cleaning-demo/user.py +++ b/examples/10-cleaning-demo/user.py @@ -21,6 +21,7 @@ fund_agent_if_low(user.wallet.address()) request = ServiceRequest( + user=user.name, address=17, time_start=12, duration=4, From 4e2f3060f0945be3ffe9ba0661f297986d34ef11 Mon Sep 17 00:00:00 2001 From: James Riehl Date: Tue, 6 Dec 2022 17:22:40 +0000 Subject: [PATCH 26/41] feat: add datetimes --- examples/10-cleaning-demo/cleaner.py | 7 +++++-- .../protocols/cleaning/__init__.py | 17 ++++++++++------- .../protocols/cleaning/models.py | 4 ++-- examples/10-cleaning-demo/user.py | 7 +++++-- 4 files changed, 22 insertions(+), 13 deletions(-) diff --git a/examples/10-cleaning-demo/cleaner.py b/examples/10-cleaning-demo/cleaner.py index 93e1165d..f123279a 100644 --- a/examples/10-cleaning-demo/cleaner.py +++ b/examples/10-cleaning-demo/cleaner.py @@ -1,3 +1,6 @@ +from datetime import datetime +from pytz import utc + from tortoise import Tortoise from protocols.cleaning import cleaning_proto @@ -41,8 +44,8 @@ async def startup(): await Availability.create( provider=provider, - time_start=10, - time_end=22, + time_start=utc.localize(datetime.fromisoformat("2022-12-31 10:00:00")), + time_end=utc.localize(datetime.fromisoformat("2022-12-31 22:00:00")), max_distance=10, min_hourly_price=5, ) diff --git a/examples/10-cleaning-demo/protocols/cleaning/__init__.py b/examples/10-cleaning-demo/protocols/cleaning/__init__.py index 2d0d7d55..a7d3ed25 100644 --- a/examples/10-cleaning-demo/protocols/cleaning/__init__.py +++ b/examples/10-cleaning-demo/protocols/cleaning/__init__.py @@ -1,3 +1,4 @@ +from datetime import datetime, timedelta from typing import List from nexus import Context, Model, Protocol @@ -12,8 +13,8 @@ class ServiceRequest(Model): user: str address: int - time_start: int - duration: int + time_start: datetime + duration: timedelta services: List[int] max_price: float @@ -25,8 +26,8 @@ class ServiceResponse(Model): class ServiceBooking(Model): address: str - time_start: int - duration: int + time_start: datetime + duration: timedelta services: List[int] price: float @@ -53,6 +54,7 @@ async def handle_query_request(ctx: Context, sender: str, msg: ServiceRequest): markup = provider.markup user, _ = await User.get_or_create(name=msg.user, address=sender) + msg_duration_hours: float = msg.duration.total_seconds() / 3600 print(f"Received service request from user `{user.name}`") if ( @@ -60,10 +62,10 @@ async def handle_query_request(ctx: Context, sender: str, msg: ServiceRequest): and in_service_region(msg.address, availability, provider) and availability.time_start <= msg.time_start and availability.time_end >= msg.time_start + msg.duration - and availability.min_hourly_price * msg.duration < msg.max_price + and availability.min_hourly_price * msg_duration_hours < msg.max_price ): accept = True - price = markup * availability.min_hourly_price * msg.duration + price = markup * availability.min_hourly_price * msg_duration_hours print(f"I am available! Proposing price: {price}.") else: accept = False @@ -81,13 +83,14 @@ async def handle_book_request(ctx: Context, sender: str, msg: ServiceBooking): services = [int(service.type) for service in await provider.services] user = await User.get(address=sender) + msg_duration_hours: float = msg.duration.total_seconds() / 3600 print(f"Received booking request from user `{user.name}`") success = ( set(msg.services) <= set(services) and availability.time_start <= msg.time_start and availability.time_end >= msg.time_start + msg.duration - and msg.price <= availability.min_hourly_price * msg.duration + and msg.price <= availability.min_hourly_price * msg_duration_hours ) if success: diff --git a/examples/10-cleaning-demo/protocols/cleaning/models.py b/examples/10-cleaning-demo/protocols/cleaning/models.py index 93ffe56b..5a90125d 100644 --- a/examples/10-cleaning-demo/protocols/cleaning/models.py +++ b/examples/10-cleaning-demo/protocols/cleaning/models.py @@ -37,6 +37,6 @@ class Availability(models.Model): id = fields.IntField(pk=True) provider = fields.OneToOneField("models.Provider", related_name="availability") max_distance = fields.IntField(default=10) - time_start = fields.IntField(default=0) - time_end = fields.IntField(default=24) + time_start = fields.DatetimeField() + time_end = fields.DatetimeField() min_hourly_price = fields.FloatField(default=0.0) diff --git a/examples/10-cleaning-demo/user.py b/examples/10-cleaning-demo/user.py index 2b188f8c..8d33f508 100644 --- a/examples/10-cleaning-demo/user.py +++ b/examples/10-cleaning-demo/user.py @@ -1,3 +1,6 @@ +from datetime import datetime, timedelta +from pytz import utc + from protocols.cleaning import ( ServiceBooking, BookingResponse, @@ -23,8 +26,8 @@ request = ServiceRequest( user=user.name, address=17, - time_start=12, - duration=4, + time_start=utc.localize(datetime.fromisoformat("2022-12-31 16:00:00")), + duration=timedelta(hours=4), services=[ServiceType.WINDOW, ServiceType.LAUNDRY], max_price=60, ) From 9c7c0eacdcd6ca60ea753d1602687ee67256251e Mon Sep 17 00:00:00 2001 From: James Riehl Date: Tue, 13 Dec 2022 14:41:48 +0000 Subject: [PATCH 27/41] chore: pr revisions and resolve conflicts --- .../protocols/book.py | 3 +- .../protocols/query.py | 20 ++++- examples/10-cleaning-demo/cleaner.py | 6 +- src/nexus/__init__.py | 2 +- src/nexus/agent.py | 81 ++++++++++++++----- src/nexus/asgi.py | 36 ++++++++- src/nexus/config.py | 3 + src/nexus/context.py | 44 +++++++--- src/nexus/crypto/__init__.py | 11 +++ src/nexus/envelope.py | 4 + src/nexus/models.py | 4 + src/nexus/protocol.py | 31 +++++-- 12 files changed, 199 insertions(+), 46 deletions(-) diff --git a/examples/09-booking-protocol-demo/protocols/book.py b/examples/09-booking-protocol-demo/protocols/book.py index 5b4ea0c2..bb3072a2 100644 --- a/examples/09-booking-protocol-demo/protocols/book.py +++ b/examples/09-booking-protocol-demo/protocols/book.py @@ -16,7 +16,7 @@ class BookTableResponse(Model): book_proto = Protocol() -@book_proto.on_message(model=BookTableRequest, replies={BookTableResponse}) +@book_proto.on_message(model=BookTableRequest, replies=BookTableResponse) async def handle_book_request(ctx: Context, sender: str, msg: BookTableRequest): tables = { @@ -25,6 +25,7 @@ async def handle_book_request(ctx: Context, sender: str, msg: BookTableRequest): num, status, ) in ctx.storage._data.items() # pylint: disable=protected-access + if isinstance(num, int) } table = tables[msg.table_number] diff --git a/examples/09-booking-protocol-demo/protocols/query.py b/examples/09-booking-protocol-demo/protocols/query.py index c58c0a7d..d07b4dce 100644 --- a/examples/09-booking-protocol-demo/protocols/query.py +++ b/examples/09-booking-protocol-demo/protocols/query.py @@ -19,10 +19,18 @@ class QueryTableResponse(Model): tables: List[int] +class GetTotalQueries(Model): + pass + + +class TotalQueries(Model): + total_queries: int + + query_proto = Protocol() -@query_proto.on_message(model=QueryTableRequest, replies={QueryTableResponse}) +@query_proto.on_message(model=QueryTableRequest, replies=QueryTableResponse) async def handle_query_request(ctx: Context, sender: str, msg: QueryTableRequest): tables = { int(num): TableStatus(**status) @@ -30,6 +38,7 @@ async def handle_query_request(ctx: Context, sender: str, msg: QueryTableRequest num, status, ) in ctx.storage._data.items() # pylint: disable=protected-access + if isinstance(num, int) } available_tables = [] @@ -44,3 +53,12 @@ async def handle_query_request(ctx: Context, sender: str, msg: QueryTableRequest print(f"Query: {msg}. Available tables: {available_tables}.") await ctx.send(sender, QueryTableResponse(tables=available_tables)) + + total_queries = int(ctx.storage.get("total_queries") or 0) + ctx.storage.set("total_queries", total_queries + 1) + + +@query_proto.on_query(model=GetTotalQueries, replies=TotalQueries) +async def handle_get_total_queries(ctx: Context, sender: str, _msg: GetTotalQueries): + total_queries = int(ctx.storage.get("total_queries") or 0) + await ctx.send(sender, TotalQueries(total_queries=total_queries)) diff --git a/examples/10-cleaning-demo/cleaner.py b/examples/10-cleaning-demo/cleaner.py index f123279a..544e8489 100644 --- a/examples/10-cleaning-demo/cleaner.py +++ b/examples/10-cleaning-demo/cleaner.py @@ -6,7 +6,7 @@ from protocols.cleaning import cleaning_proto from protocols.cleaning.models import Availability, Provider, Service, ServiceType -from nexus import Agent, EventType +from nexus import Agent from nexus.setup import fund_agent_if_low @@ -25,7 +25,7 @@ cleaner.include(cleaning_proto) -@cleaner.on_event(EventType.STARTUP) +@cleaner.on_event("startup") async def startup(): await Tortoise.init( db_url="sqlite://db.sqlite3", modules={"models": ["protocols.cleaning.models"]} @@ -51,7 +51,7 @@ async def startup(): ) -@cleaner.on_event(EventType.SHUTDOWN) +@cleaner.on_event("shutdown") async def shutdown(): await Tortoise.close_connections() diff --git a/src/nexus/__init__.py b/src/nexus/__init__.py index ba114593..ecf464fa 100644 --- a/src/nexus/__init__.py +++ b/src/nexus/__init__.py @@ -1,4 +1,4 @@ -from .context import Context, EventType +from .context import Context from .protocol import Protocol from .models import Model from .agent import Agent, Bureau diff --git a/src/nexus/agent.py b/src/nexus/agent.py index 4c364d50..18f3a8c5 100644 --- a/src/nexus/agent.py +++ b/src/nexus/agent.py @@ -1,7 +1,7 @@ import asyncio import functools import logging -from typing import Optional, List, Set, Tuple, Any, Union +from typing import Dict, Optional, List, Set, Tuple, Any, Union from cosmpy.aerial.wallet import LocalWallet, PrivateKey @@ -11,12 +11,11 @@ EventCallback, IntervalCallback, MessageCallback, - EventType, MsgDigest, ) -from nexus.crypto import Identity, derive_key_from_seed +from nexus.crypto import Identity, derive_key_from_seed, is_user_address from nexus.dispatch import Sink, dispatcher -from nexus.models import Model +from nexus.models import Model, ErrorMessage from nexus.protocol import Protocol from nexus.resolver import Resolver, AlmanacResolver from nexus.storage import KeyValueStore, get_or_create_private_keys @@ -41,6 +40,10 @@ async def _run_interval(func: IntervalCallback, ctx: Context, period: float): await asyncio.sleep(period) +async def _handle_error(ctx: Context, destination: str, msg: ErrorMessage): + await ctx.send(destination, msg) + + class Agent(Sink): def __init__( self, @@ -78,8 +81,9 @@ def __init__( self._models = {} self._replies = {} self._interval_messages = {} - self._message_handlers = {} - self._inbox = {} + self._signed_message_handlers = {} + self._unsigned_message_handlers = {} + self._queries: Dict[str, asyncio.Future] = {} self._ctx = Context( self._identity.address, self._name, @@ -88,6 +92,7 @@ def __init__( self._identity, self._wallet, self._ledger, + self._queries, replies=self._replies, interval_messages=self._interval_messages, ) @@ -111,7 +116,7 @@ def __init__( self._background_tasks.add(task) task.add_done_callback(self._background_tasks.discard) - self._server = ASGIServer(self._port, self._loop) + self._server = ASGIServer(self._port, self._loop, self._queries) @property def name(self) -> str: @@ -138,6 +143,9 @@ def sign_registration(self) -> str: def update_loop(self, loop): self._loop = loop + def update_queries(self, queries): + self._queries = queries + async def register(self, ctx: Context): if self.registration_status(): @@ -207,12 +215,22 @@ def on_interval( ): return self._protocol.on_interval(period, messages) + def on_query( + self, + model: Model, + replies: Optional[Union[Model, Set[Model]]] = None, + ): + return self._protocol.on_query(model, replies) + def on_message( - self, model: Model, replies: Optional[Union[Model, Set[Model]]] = None + self, + model: Model, + replies: Optional[Union[Model, Set[Model]]] = None, + allow_unverified: Optional[bool] = False, ): - return self._protocol.on_message(model, replies) + return self._protocol.on_message(model, replies, allow_unverified) - def on_event(self, event_type: EventType) -> EventCallback: + def on_event(self, event_type: str) -> EventCallback: def decorator_on_event(func: EventCallback) -> EventCallback: @functools.wraps(func) def handler(*args, **kwargs): @@ -229,9 +247,9 @@ def _add_event_handler( event_type: str, func: EventCallback, ) -> None: - if event_type == EventType.STARTUP: + if event_type == "startup": self._on_startup.append(func) - elif event_type == EventType.SHUTDOWN: + elif event_type == "shutdown": self._on_shutdown.append(func) def include(self, protocol: Protocol): @@ -248,16 +266,21 @@ def include(self, protocol: Protocol): for schema_digest in protocol.models: if schema_digest in self._models: raise RuntimeError("Unable to register duplicate model") - if schema_digest in self._message_handlers: + if schema_digest in self._signed_message_handlers: raise RuntimeError("Unable to register duplicate message handler") - if schema_digest not in protocol.message_handlers: + if schema_digest in protocol.signed_message_handlers: + self._signed_message_handlers[ + schema_digest + ] = protocol.signed_message_handlers[schema_digest] + elif schema_digest in protocol.unsigned_message_handlers: + self._unsigned_message_handlers[ + schema_digest + ] = protocol.unsigned_message_handlers[schema_digest] + else: raise RuntimeError("Unable to lookup up message handler in protocol") - # include the message handlers from the protocol self._models[schema_digest] = protocol.models[schema_digest] - self._message_handlers[schema_digest] = protocol.message_handlers[ - schema_digest - ] + if schema_digest in protocol.replies: self._replies[schema_digest] = protocol.replies[schema_digest] @@ -313,6 +336,7 @@ async def _process_message_queue(self): self._identity, self._wallet, self._ledger, + self._queries, replies=self._replies, interval_messages=self._interval_messages, message_received=MsgDigest( @@ -321,7 +345,22 @@ async def _process_message_queue(self): ) # attempt to find the handler - handler: MessageCallback = self._message_handlers.get(schema_digest) + handler: MessageCallback = self._unsigned_message_handlers.get( + schema_digest + ) + if handler is None: + if not is_user_address(sender): + handler = self._signed_message_handlers.get(schema_digest) + elif schema_digest in self._signed_message_handlers: + await _handle_error( + context, + sender, + ErrorMessage( + error="Message must be sent from verified agent address" + ), + ) + continue + if handler is not None: await handler(context, sender, recovered) @@ -331,10 +370,12 @@ def __init__(self, port: Optional[int] = None): self._loop = asyncio.get_event_loop_policy().get_event_loop() self._agents = [] self._port = port or 8000 - self._server = ASGIServer(self._port, self._loop) + self._queries: Dict[str, asyncio.Future] = {} + self._server = ASGIServer(self._port, self._loop, self._queries) def add(self, agent: Agent): agent.update_loop(self._loop) + agent.update_queries(self._queries) self._agents.append(agent) def run(self): diff --git a/src/nexus/asgi.py b/src/nexus/asgi.py index 7f7aa84b..c1a3b017 100644 --- a/src/nexus/asgi.py +++ b/src/nexus/asgi.py @@ -1,11 +1,16 @@ import asyncio import json +from datetime import datetime +from typing import Dict import pydantic import uvicorn +from nexus.crypto import is_user_address from nexus.dispatch import dispatcher from nexus.envelope import Envelope +from nexus.models import Model, ErrorMessage +from nexus.query import enclose_response async def _read_asgi_body(receive): @@ -21,9 +26,15 @@ async def _read_asgi_body(receive): class ASGIServer: - def __init__(self, port: int, loop: asyncio.AbstractEventLoop): + def __init__( + self, + port: int, + loop: asyncio.AbstractEventLoop, + queries: Dict[str, asyncio.Future], + ): self._port = int(port) self._loop = loop + self._queries = queries async def serve(self): config = uvicorn.Config(self, host="0.0.0.0", port=self._port, log_level="info") @@ -72,7 +83,7 @@ async def __call__(self, scope, receive, send): contents = json.loads(raw_contents.decode()) try: - env = Envelope.parse_obj(contents) + env: Envelope = Envelope.parse_obj(contents) except pydantic.ValidationError: await send( { @@ -88,7 +99,14 @@ async def __call__(self, scope, receive, send): ) return - if not env.verify(): + expects_response = b"sync" == headers.get(b"x-uagents-connection") + do_verify = not is_user_address(env.sender) + + if expects_response: + # Add a future that will be resolved once the query is answered + self._queries[env.sender] = asyncio.Future() + + if do_verify and env.verify() is False: await send( { "type": "http.response.start", @@ -128,6 +146,16 @@ async def __call__(self, scope, receive, send): env.sender, env.target, env.protocol, env.decode_payload() ) + # wait for any queries to be resolved + if expects_response: + response_msg: Model = await self._queries[env.sender] + if datetime.now() > datetime.fromtimestamp(env.expires): + response_msg = ErrorMessage("Query envelope expired") + sender = env.target + response = enclose_response(response_msg, sender, env.session) + else: + response = "{}" + await send( { "type": "http.response.start", @@ -140,6 +168,6 @@ async def __call__(self, scope, receive, send): await send( { "type": "http.response.body", - "body": b"{}", + "body": response.encode(), } ) diff --git a/src/nexus/config.py b/src/nexus/config.py index 422b438a..2dfcb113 100644 --- a/src/nexus/config.py +++ b/src/nexus/config.py @@ -11,8 +11,11 @@ class AgentNetwork(Enum): AGENT_PREFIX = "agent" LEDGER_PREFIX = "fetch" +USER_PREFIX = "user" CONTRACT_ALMANAC = "fetch1tjagw8g8nn4cwuw00cf0m5tl4l6wfw9c0ue507fhx9e3yrsck8zs0l3q4w" REGISTRATION_FEE = 500000000000000000 REGISTRATION_DENOM = "atestfet" REG_UPDATE_INTERVAL_SECONDS = 60 AGENT_NETWORK = AgentNetwork.FETCHAI_TESTNET + +DEFAULT_ENVELOPE_TIMEOUT_SECONDS = 30 diff --git a/src/nexus/context.py b/src/nexus/context.py index 85791392..386966a8 100644 --- a/src/nexus/context.py +++ b/src/nexus/context.py @@ -1,17 +1,19 @@ +import asyncio import logging import uuid from dataclasses import dataclass -from enum import Enum +from time import time from typing import Dict, Set, Optional, Callable, Any, Awaitable import aiohttp from cosmpy.aerial.client import LedgerClient from cosmpy.aerial.wallet import LocalWallet -from nexus.crypto import Identity +from nexus.config import DEFAULT_ENVELOPE_TIMEOUT_SECONDS +from nexus.crypto import Identity, is_user_address from nexus.dispatch import dispatcher from nexus.envelope import Envelope -from nexus.models import Model +from nexus.models import Model, ErrorMessage from nexus.resolver import Resolver from nexus.storage import KeyValueStore @@ -20,17 +22,15 @@ EventCallback = Callable[["Context"], Awaitable[None]] -class EventType(Enum): - STARTUP = 0 - SHUTDOWN = 1 - - @dataclass class MsgDigest: message: Any schema_digest: str +ERROR_MESSAGE_DIGEST = Model.build_schema_digest(ErrorMessage) + + class Context: def __init__( self, @@ -41,6 +41,7 @@ def __init__( identity: Identity, wallet: LocalWallet, ledger: LedgerClient, + queries: Dict[str, asyncio.Future], replies: Optional[Dict[str, Set[str]]] = None, interval_messages: Optional[Dict[str, Set[str]]] = None, message_received: Optional[MsgDigest] = None, @@ -52,6 +53,7 @@ def __init__( self._address = str(address) self._resolver = resolve self._identity = identity + self._queries = queries self._replies = replies self._interval_messages = interval_messages self._message_received = message_received @@ -66,13 +68,22 @@ def name(self) -> str: def address(self) -> str: return self._address - async def send(self, destination: str, message: Model): + async def send( + self, + destination: str, + message: Model, + timeout: Optional[int] = DEFAULT_ENVELOPE_TIMEOUT_SECONDS, + ): # convert the message into object form json_message = message.json() schema_digest = Model.build_schema_digest(message) # check if this message is a reply - if self._message_received is not None and self._replies: + if ( + self._message_received is not None + and self._replies + and schema_digest != ERROR_MESSAGE_DIGEST + ): received = self._message_received if received.schema_digest in self._replies: # ensure the reply is valid @@ -98,6 +109,15 @@ async def send(self, destination: str, message: Model): ) return + # handle queries waiting for a response + if is_user_address(destination): + if destination not in self._queries: + logging.exception(f"Unable to resolve query to user {destination}") + return + self._queries[destination].set_result(message) + del self._queries[destination] + return + # resolve the endpoint endpoint = await self._resolver.resolve(destination) if endpoint is None: @@ -106,6 +126,9 @@ async def send(self, destination: str, message: Model): ) return + # calculate when envelope expires + expires = int(time()) + timeout + # handle external dispatch of messages env = Envelope( version=1, @@ -113,6 +136,7 @@ async def send(self, destination: str, message: Model): target=destination, session=uuid.uuid4(), protocol=schema_digest, + expires=expires, ) env.encode_payload(json_message) env.sign(self._identity) diff --git a/src/nexus/crypto/__init__.py b/src/nexus/crypto/__init__.py index 18718fee..9b9c118c 100644 --- a/src/nexus/crypto/__init__.py +++ b/src/nexus/crypto/__init__.py @@ -1,9 +1,12 @@ import hashlib import struct +from secrets import token_bytes from typing import Tuple, Union import bech32 import ecdsa +from nexus.config import USER_PREFIX + def _decode_bech32(value: str) -> Tuple[str, bytes]: prefix, data_base5 = bech32.bech32_decode(value) @@ -16,6 +19,14 @@ def _encode_bech32(prefix: str, value: bytes) -> str: return bech32.bech32_encode(prefix, value_base5) +def generate_user_address() -> str: + return _encode_bech32(USER_PREFIX, token_bytes(32)) + + +def is_user_address(address: str) -> bool: + return address[0 : len(USER_PREFIX)] == USER_PREFIX + + def _key_derivation_hash(prefix: str, index: int) -> bytes: hasher = hashlib.sha256() hasher.update(prefix.encode()) diff --git a/src/nexus/envelope.py b/src/nexus/envelope.py index f0f9b3f7..92c8585d 100644 --- a/src/nexus/envelope.py +++ b/src/nexus/envelope.py @@ -1,6 +1,7 @@ import base64 import hashlib import json +import struct from typing import Optional, Any from pydantic import BaseModel, UUID4 @@ -15,6 +16,7 @@ class Envelope(BaseModel): session: UUID4 protocol: str payload: Optional[str] = None + expires: Optional[int] = None signature: Optional[str] = None def encode_payload(self, value: Any): @@ -43,4 +45,6 @@ def _digest(self) -> bytes: hasher.update(self.protocol.encode()) if self.payload is not None: hasher.update(self.payload.encode()) + if self.expires is not None: + hasher.update(struct.pack(">Q", self.expires)) return hasher.digest() diff --git a/src/nexus/models.py b/src/nexus/models.py index f378fa92..f62b9136 100644 --- a/src/nexus/models.py +++ b/src/nexus/models.py @@ -13,3 +13,7 @@ def build_schema_digest(model: "Model") -> str: .digest() .hex() ) + + +class ErrorMessage(Model): + error: str diff --git a/src/nexus/protocol.py b/src/nexus/protocol.py index 9c4af0cb..55ce66be 100644 --- a/src/nexus/protocol.py +++ b/src/nexus/protocol.py @@ -15,7 +15,8 @@ class Protocol: def __init__(self, name: Optional[str] = None, version: Optional[str] = None): self._intervals = [] self._interval_messages = {} - self._message_handlers = {} + self._signed_message_handlers = {} + self._unsigned_message_handlers = {} self._models = {} self._replies = {} self._name = name or "" @@ -46,8 +47,12 @@ def interval_messages(self): return self._interval_messages @property - def message_handlers(self): - return self._message_handlers + def signed_message_handlers(self): + return self._signed_message_handlers + + @property + def unsigned_message_handlers(self): + return self._unsigned_message_handlers @property def name(self): @@ -98,15 +103,25 @@ def _add_interval_handler( self.spec.path(path=message.__name__, operations={}) self._update_digest() + def on_query( + self, + model: Model, + replies: Optional[Union[Model, Set[Model]]] = None, + ): + return self.on_message(model, replies, allow_unverified=True) + def on_message( - self, model: Model, replies: Optional[Union[Model, Set[Model]]] = None + self, + model: Model, + replies: Optional[Union[Model, Set[Model]]] = None, + allow_unverified: Optional[bool] = False, ): def decorator_on_message(func: MessageCallback): @functools.wraps(func) def handler(*args, **kwargs): return func(*args, **kwargs) - self._add_message_handler(model, func, replies) + self._add_message_handler(model, func, replies, allow_unverified) return handler @@ -117,12 +132,16 @@ def _add_message_handler( model: Model, func: MessageCallback, replies: Optional[Union[Model, Set[Model]]], + allow_unverified: Optional[bool] = False, ): model_digest = Model.build_schema_digest(model) # update the model database self._models[model_digest] = model - self._message_handlers[model_digest] = func + if allow_unverified: + self._unsigned_message_handlers[model_digest] = func + else: + self._signed_message_handlers[model_digest] = func if replies is not None: if not isinstance(replies, set): replies = {replies} From 12e1c1675b529c188053d6d05d0e7717e50b7a88 Mon Sep 17 00:00:00 2001 From: James Riehl Date: Tue, 13 Dec 2022 14:59:58 +0000 Subject: [PATCH 28/41] chore: resolve conflict --- src/nexus/agent.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/nexus/agent.py b/src/nexus/agent.py index 1ed7c56f..18f3a8c5 100644 --- a/src/nexus/agent.py +++ b/src/nexus/agent.py @@ -9,7 +9,7 @@ from nexus.context import ( Context, EventCallback, - IntervalCallback, + IntervalCallback, MessageCallback, MsgDigest, ) @@ -229,7 +229,6 @@ def on_message( allow_unverified: Optional[bool] = False, ): return self._protocol.on_message(model, replies, allow_unverified) -<<<<<<< HEAD def on_event(self, event_type: str) -> EventCallback: def decorator_on_event(func: EventCallback) -> EventCallback: @@ -252,8 +251,6 @@ def _add_event_handler( self._on_startup.append(func) elif event_type == "shutdown": self._on_shutdown.append(func) -======= ->>>>>>> origin/master def include(self, protocol: Protocol): for func, period in protocol.intervals: From 428a946968b37baab1f6a632f541464a9f7dcc4b Mon Sep 17 00:00:00 2001 From: James Riehl Date: Thu, 15 Dec 2022 14:04:44 +0000 Subject: [PATCH 29/41] feat: initial connection working --- examples/10-cleaning-demo/user.py | 4 +++- src/nexus/agent.py | 5 +++-- src/nexus/asgi.py | 2 +- src/nexus/envelope.py | 9 ++++++--- src/nexus/models.py | 8 ++++++++ 5 files changed, 21 insertions(+), 7 deletions(-) diff --git a/examples/10-cleaning-demo/user.py b/examples/10-cleaning-demo/user.py index 8d33f508..e90943bf 100644 --- a/examples/10-cleaning-demo/user.py +++ b/examples/10-cleaning-demo/user.py @@ -8,7 +8,7 @@ ServiceResponse, ) from protocols.cleaning.models import ServiceType -from nexus import Agent, Context +from nexus import Agent, Context, Model from nexus.setup import fund_agent_if_low @@ -34,6 +34,8 @@ MARKDOWN = 0.8 +print(Model.build_schema_digest(ServiceRequest)) + @user.on_interval(period=3.0, messages=ServiceRequest) async def interval(ctx: Context): diff --git a/src/nexus/agent.py b/src/nexus/agent.py index 18f3a8c5..10936f22 100644 --- a/src/nexus/agent.py +++ b/src/nexus/agent.py @@ -1,5 +1,6 @@ import asyncio import functools +import json import logging from typing import Dict, Optional, List, Set, Tuple, Any, Union @@ -321,12 +322,12 @@ async def _process_message_queue(self): schema_digest, sender, message = await self._message_queue.get() # lookup the model definition - model_class = self._models.get(schema_digest) + model_class: Model = self._models.get(schema_digest) if model_class is None: continue # parse the received message - recovered = model_class.parse_raw(message) + recovered = model_class.parse(message) context = Context( self._identity.address, diff --git a/src/nexus/asgi.py b/src/nexus/asgi.py index c1a3b017..ca936750 100644 --- a/src/nexus/asgi.py +++ b/src/nexus/asgi.py @@ -63,7 +63,7 @@ async def __call__(self, scope, receive, send): return headers = dict(scope.get("headers", {})) - if headers[b"content-type"] != b"application/json": + if b"application/json" not in headers[b"content-type"]: await send( { "type": "http.response.start", diff --git a/src/nexus/envelope.py b/src/nexus/envelope.py index 92c8585d..74f589fb 100644 --- a/src/nexus/envelope.py +++ b/src/nexus/envelope.py @@ -32,10 +32,13 @@ def sign(self, identity: Identity): self.signature = identity.sign_digest(self._digest()) def verify(self) -> bool: - if self.signature is None: - return False + # Temporary for demo + return True - return Identity.verify_digest(self.sender, self._digest(), self.signature) + # if self.signature is None: + # return False + + # return Identity.verify_digest(self.sender, self._digest(), self.signature) def _digest(self) -> bytes: hasher = hashlib.sha256() diff --git a/src/nexus/models.py b/src/nexus/models.py index f62b9136..7aec9d7f 100644 --- a/src/nexus/models.py +++ b/src/nexus/models.py @@ -1,4 +1,5 @@ import hashlib +from typing import Any, Dict from pydantic import BaseModel @@ -14,6 +15,13 @@ def build_schema_digest(model: "Model") -> str: .hex() ) + @classmethod + def parse(cls, model: Any) -> Dict[str, Any]: + if isinstance(model, dict): + return cls.parse_obj(model) + else: + return cls.parse_raw(model) + class ErrorMessage(Model): error: str From b5e18d737228a288e2381492ded1c0800e5c28a4 Mon Sep 17 00:00:00 2001 From: James Riehl Date: Thu, 15 Dec 2022 14:23:40 +0000 Subject: [PATCH 30/41] fix: do not check for user address on query --- src/nexus/agent.py | 1 - src/nexus/asgi.py | 5 +++-- src/nexus/context.py | 7 ++----- src/nexus/models.py | 3 +-- 4 files changed, 6 insertions(+), 10 deletions(-) diff --git a/src/nexus/agent.py b/src/nexus/agent.py index 10936f22..e063d65d 100644 --- a/src/nexus/agent.py +++ b/src/nexus/agent.py @@ -1,6 +1,5 @@ import asyncio import functools -import json import logging from typing import Dict, Optional, List, Set, Tuple, Any, Union diff --git a/src/nexus/asgi.py b/src/nexus/asgi.py index ca936750..f683acfd 100644 --- a/src/nexus/asgi.py +++ b/src/nexus/asgi.py @@ -149,8 +149,9 @@ async def __call__(self, scope, receive, send): # wait for any queries to be resolved if expects_response: response_msg: Model = await self._queries[env.sender] - if datetime.now() > datetime.fromtimestamp(env.expires): - response_msg = ErrorMessage("Query envelope expired") + if env.expires is not None: + if datetime.now() > datetime.fromtimestamp(env.expires): + response_msg = ErrorMessage("Query envelope expired") sender = env.target response = enclose_response(response_msg, sender, env.session) else: diff --git a/src/nexus/context.py b/src/nexus/context.py index 386966a8..05ab43e7 100644 --- a/src/nexus/context.py +++ b/src/nexus/context.py @@ -10,7 +10,7 @@ from cosmpy.aerial.wallet import LocalWallet from nexus.config import DEFAULT_ENVELOPE_TIMEOUT_SECONDS -from nexus.crypto import Identity, is_user_address +from nexus.crypto import Identity from nexus.dispatch import dispatcher from nexus.envelope import Envelope from nexus.models import Model, ErrorMessage @@ -110,10 +110,7 @@ async def send( return # handle queries waiting for a response - if is_user_address(destination): - if destination not in self._queries: - logging.exception(f"Unable to resolve query to user {destination}") - return + if destination in self._queries: self._queries[destination].set_result(message) del self._queries[destination] return diff --git a/src/nexus/models.py b/src/nexus/models.py index 7aec9d7f..a641af93 100644 --- a/src/nexus/models.py +++ b/src/nexus/models.py @@ -19,8 +19,7 @@ def build_schema_digest(model: "Model") -> str: def parse(cls, model: Any) -> Dict[str, Any]: if isinstance(model, dict): return cls.parse_obj(model) - else: - return cls.parse_raw(model) + return cls.parse_raw(model) class ErrorMessage(Model): From 00d985d4ebc4cee650dd809ed6c324c7cbbbdb45 Mon Sep 17 00:00:00 2001 From: James Riehl Date: Thu, 15 Dec 2022 16:26:19 +0000 Subject: [PATCH 31/41] fix: envelope encoding --- src/nexus/query.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/nexus/query.py b/src/nexus/query.py index 7de3b99d..614e7411 100644 --- a/src/nexus/query.py +++ b/src/nexus/query.py @@ -73,5 +73,5 @@ def enclose_response(message: Model, sender: str, session: str) -> dict: session=session, protocol=Model.build_schema_digest(message), ) - response_env.encode_payload(message.json()) + response_env.encode_payload(message.dict()) return response_env.json() From d562a3cf970002d1fb84b455932a92f5d42c7f25 Mon Sep 17 00:00:00 2001 From: James Riehl Date: Fri, 16 Dec 2022 10:04:16 +0000 Subject: [PATCH 32/41] fix: get first provider --- examples/10-cleaning-demo/cleaner.py | 4 ++-- examples/10-cleaning-demo/protocols/cleaning/__init__.py | 2 +- examples/10-cleaning-demo/user.py | 1 + 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/examples/10-cleaning-demo/cleaner.py b/examples/10-cleaning-demo/cleaner.py index 544e8489..68acdf0d 100644 --- a/examples/10-cleaning-demo/cleaner.py +++ b/examples/10-cleaning-demo/cleaner.py @@ -44,8 +44,8 @@ async def startup(): await Availability.create( provider=provider, - time_start=utc.localize(datetime.fromisoformat("2022-12-31 10:00:00")), - time_end=utc.localize(datetime.fromisoformat("2022-12-31 22:00:00")), + time_start=utc.localize(datetime.fromisoformat("2022-12-31 12:00:00")), + time_end=utc.localize(datetime.fromisoformat("2023-01-01 00:00:00")), max_distance=10, min_hourly_price=5, ) diff --git a/examples/10-cleaning-demo/protocols/cleaning/__init__.py b/examples/10-cleaning-demo/protocols/cleaning/__init__.py index a7d3ed25..60a232c8 100644 --- a/examples/10-cleaning-demo/protocols/cleaning/__init__.py +++ b/examples/10-cleaning-demo/protocols/cleaning/__init__.py @@ -78,7 +78,7 @@ async def handle_query_request(ctx: Context, sender: str, msg: ServiceRequest): @cleaning_proto.on_message(model=ServiceBooking, replies=BookingResponse) async def handle_book_request(ctx: Context, sender: str, msg: ServiceBooking): - provider = await Provider.get(name=ctx.name) + provider = await Provider.filter(name=ctx.name).first() availability = await Availability.get(provider=provider) services = [int(service.type) for service in await provider.services] diff --git a/examples/10-cleaning-demo/user.py b/examples/10-cleaning-demo/user.py index e90943bf..6a01be10 100644 --- a/examples/10-cleaning-demo/user.py +++ b/examples/10-cleaning-demo/user.py @@ -35,6 +35,7 @@ MARKDOWN = 0.8 print(Model.build_schema_digest(ServiceRequest)) +print(Model.build_schema_digest(ServiceBooking)) @user.on_interval(period=3.0, messages=ServiceRequest) From 228e23a0e4c168951bd3e9e441c90706c65f938c Mon Sep 17 00:00:00 2001 From: Alejandro-Morales <77800944+Alejandro-Morales@users.noreply.github.com> Date: Tue, 3 Jan 2023 12:00:58 +0000 Subject: [PATCH 33/41] feat: enable signature verification (#31) Co-authored-by: James Riehl --- examples/10-cleaning-demo/cleaner.py | 2 +- src/nexus/crypto/__init__.py | 6 +--- src/nexus/envelope.py | 17 +++++++--- tests/test_agent_registration.py | 51 ++++++++++++++++++++++++++++ tests/test_verify_msg.py | 20 +++++++++-- 5 files changed, 83 insertions(+), 13 deletions(-) create mode 100644 tests/test_agent_registration.py diff --git a/examples/10-cleaning-demo/cleaner.py b/examples/10-cleaning-demo/cleaner.py index 68acdf0d..648b36b8 100644 --- a/examples/10-cleaning-demo/cleaner.py +++ b/examples/10-cleaning-demo/cleaner.py @@ -14,7 +14,7 @@ name="cleaner", port=8001, seed="cleaner secret seed phrase", - endpoint="http://127.0.0.1:8001/submit", + endpoint="https://bdf3-187-213-48-169.ngrok.io/submit", ) fund_agent_if_low(cleaner.wallet.address()) diff --git a/src/nexus/crypto/__init__.py b/src/nexus/crypto/__init__.py index 9b9c118c..d1de15ff 100644 --- a/src/nexus/crypto/__init__.py +++ b/src/nexus/crypto/__init__.py @@ -112,19 +112,15 @@ def sign_registration(self, contract_address: str, sequence: int) -> str: def verify_digest(address: str, digest: bytes, signature: str) -> bool: pk_prefix, pk_data = _decode_bech32(address) - sig_prefix, sig_data = _decode_bech32(signature) if pk_prefix != "agent": raise ValueError("Unable to decode agent address") - if sig_prefix != "sig": - raise ValueError("Unable to decode signature") - # build the verifying key verifying_key = ecdsa.VerifyingKey.from_string(pk_data, curve=ecdsa.SECP256k1) try: - result = verifying_key.verify_digest(sig_data, digest) + result = verifying_key.verify_digest(bytes.fromhex(signature), digest) except ecdsa.keys.BadSignatureError: return False diff --git a/src/nexus/envelope.py b/src/nexus/envelope.py index 74f589fb..1369f57f 100644 --- a/src/nexus/envelope.py +++ b/src/nexus/envelope.py @@ -32,13 +32,20 @@ def sign(self, identity: Identity): self.signature = identity.sign_digest(self._digest()) def verify(self) -> bool: - # Temporary for demo - return True - # if self.signature is None: - # return False + if self.signature is None: + return False - # return Identity.verify_digest(self.sender, self._digest(), self.signature) + verification = Identity.verify_digest( + self.sender, self._digest(), self.signature + ) + + if verification: + print(f"Verifired request from {self.sender}") + else: + print(f"Failed to verify request from {self.sender}") + + return verification def _digest(self) -> bytes: hasher = hashlib.sha256() diff --git a/tests/test_agent_registration.py b/tests/test_agent_registration.py new file mode 100644 index 00000000..95da6efb --- /dev/null +++ b/tests/test_agent_registration.py @@ -0,0 +1,51 @@ +import unittest + +from nexus import Agent +from nexus.setup import fund_agent_if_low + + +class TestVerify(unittest.TestCase): + def test_agent_registration(self): + + agent = Agent( + name="alice", + port=8000, + seed="alice secret phrase", + endpoint="http://127.0.0.1:8000/submit", + ) + + REG_FEE = "500000000000000000atestfet" + + fund_agent_if_low(agent.wallet.address()) + + sequence = agent.get_registration_sequence() + + + signature = agent._identity.sign_registration( + agent._reg_contract.address, agent.get_registration_sequence()) + + msg = { + "register": { + "record": { + "service": { + "protocols": [], + "endpoints": [{"url": agent._endpoint, "weight": 1}], + } + }, + "signature": signature, + "sequence": sequence, + "agent_address": agent.address, + } + } + + + tx = agent._reg_contract.execute(msg, agent.wallet, funds=REG_FEE) + tx.wait_to_complete() + + is_registered = agent.registration_status() + + self.assertEqual(is_registered, True, "Registration failed") + + +if __name__ == "__main__": + unittest.main() \ No newline at end of file diff --git a/tests/test_verify_msg.py b/tests/test_verify_msg.py index a078d060..63ddcc1b 100644 --- a/tests/test_verify_msg.py +++ b/tests/test_verify_msg.py @@ -12,19 +12,35 @@ def encode(message: str) -> bytes: class TestVerify(unittest.TestCase): - def test_verify_message(self): + def test_sign_and_verify_message(self): alice = Agent(name="alice", seed="alice recovery password") alice_msg = "hello there bob" encoded_msg = encode(alice_msg) - signature = alice.sign_digest(encoded_msg) + signature = alice._identity._sk.sign_digest(encoded_msg).hex() # Message signature can be verified using alice address result = Identity.verify_digest(alice.address, encoded_msg, signature) self.assertEqual(result, True, "Verification failed") + def test_verify_dart_digest(self): + + # Generate public key + address = "agent1qf5gfqm48k9acegez3sg82ney2aa6l5fvpwh3n3z0ajh0nam3ssgwnn5me7" + + # Signature + signature = "3e8a94a928f65f5bfdc29d7389e92e2a76d0aef341b968440736d5e983bf5c75c3a877bb7b7c2401b50d40094b9b26fa22cb842fe0ff0d3c2fe787c079671652" + + # Message + dart_digest = "a29af8b704077d394a9756dc04f0bb5f1424fc391b3de91144d683c5893ca234" + bytes_dart_digest = bytes.fromhex(dart_digest) + + result = Identity.verify_digest(address, bytes_dart_digest, signature) + + self.assertEqual(result, True, "Verification failed") + if __name__ == "__main__": unittest.main() From 8c648ddb417a85da061302103511b7442f7935dc Mon Sep 17 00:00:00 2001 From: James Riehl Date: Thu, 5 Jan 2023 17:55:28 +0000 Subject: [PATCH 34/41] chore: revert to bech32 signature --- examples/10-cleaning-demo/cleaner.py | 4 ++-- src/nexus/crypto/__init__.py | 6 +++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/examples/10-cleaning-demo/cleaner.py b/examples/10-cleaning-demo/cleaner.py index 648b36b8..b3352d2d 100644 --- a/examples/10-cleaning-demo/cleaner.py +++ b/examples/10-cleaning-demo/cleaner.py @@ -44,8 +44,8 @@ async def startup(): await Availability.create( provider=provider, - time_start=utc.localize(datetime.fromisoformat("2022-12-31 12:00:00")), - time_end=utc.localize(datetime.fromisoformat("2023-01-01 00:00:00")), + time_start=utc.localize(datetime.fromisoformat("2022-01-31 00:00:00")), + time_end=utc.localize(datetime.fromisoformat("2023-02-01 00:00:00")), max_distance=10, min_hourly_price=5, ) diff --git a/src/nexus/crypto/__init__.py b/src/nexus/crypto/__init__.py index d1de15ff..9b9c118c 100644 --- a/src/nexus/crypto/__init__.py +++ b/src/nexus/crypto/__init__.py @@ -112,15 +112,19 @@ def sign_registration(self, contract_address: str, sequence: int) -> str: def verify_digest(address: str, digest: bytes, signature: str) -> bool: pk_prefix, pk_data = _decode_bech32(address) + sig_prefix, sig_data = _decode_bech32(signature) if pk_prefix != "agent": raise ValueError("Unable to decode agent address") + if sig_prefix != "sig": + raise ValueError("Unable to decode signature") + # build the verifying key verifying_key = ecdsa.VerifyingKey.from_string(pk_data, curve=ecdsa.SECP256k1) try: - result = verifying_key.verify_digest(bytes.fromhex(signature), digest) + result = verifying_key.verify_digest(sig_data, digest) except ecdsa.keys.BadSignatureError: return False From bc7f381ced1462b5f08a78cf677800739661094d Mon Sep 17 00:00:00 2001 From: James Riehl Date: Sat, 7 Jan 2023 12:28:54 +0000 Subject: [PATCH 35/41] test: update tests --- tests/test_verify_msg.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_verify_msg.py b/tests/test_verify_msg.py index 63ddcc1b..68f3ffcc 100644 --- a/tests/test_verify_msg.py +++ b/tests/test_verify_msg.py @@ -18,7 +18,7 @@ def test_sign_and_verify_message(self): alice_msg = "hello there bob" encoded_msg = encode(alice_msg) - signature = alice._identity._sk.sign_digest(encoded_msg).hex() + signature = alice.sign_digest(encoded_msg) # Message signature can be verified using alice address result = Identity.verify_digest(alice.address, encoded_msg, signature) @@ -31,7 +31,7 @@ def test_verify_dart_digest(self): address = "agent1qf5gfqm48k9acegez3sg82ney2aa6l5fvpwh3n3z0ajh0nam3ssgwnn5me7" # Signature - signature = "3e8a94a928f65f5bfdc29d7389e92e2a76d0aef341b968440736d5e983bf5c75c3a877bb7b7c2401b50d40094b9b26fa22cb842fe0ff0d3c2fe787c079671652" + signature = "sig1qyvn5fjzrhjzqcmj2gfg4us6xj00gvscs4u9uqxy6wpvp9agxjf723eh5l6w878p67lycgd3fz77zr3h0q6mrheg48e35zsvv0rm2tsuvyn3l" # Message dart_digest = "a29af8b704077d394a9756dc04f0bb5f1424fc391b3de91144d683c5893ca234" From 037d84743905b46d901af6ae7589b9f09e7e1b1c Mon Sep 17 00:00:00 2001 From: James Riehl Date: Mon, 9 Jan 2023 10:28:37 +0000 Subject: [PATCH 36/41] test: generate new wallet addresses --- tests/test_agent_registration.py | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/tests/test_agent_registration.py b/tests/test_agent_registration.py index 95da6efb..65e753d6 100644 --- a/tests/test_agent_registration.py +++ b/tests/test_agent_registration.py @@ -4,15 +4,10 @@ from nexus.setup import fund_agent_if_low -class TestVerify(unittest.TestCase): - def test_agent_registration(self): - - agent = Agent( - name="alice", - port=8000, - seed="alice secret phrase", - endpoint="http://127.0.0.1:8000/submit", - ) +class TestVerify(unittest.TestCase): + def test_agent_registration(self): + + agent = Agent(name="alice") REG_FEE = "500000000000000000atestfet" @@ -20,9 +15,9 @@ def test_agent_registration(self): sequence = agent.get_registration_sequence() - signature = agent._identity.sign_registration( - agent._reg_contract.address, agent.get_registration_sequence()) + agent._reg_contract.address, agent.get_registration_sequence() + ) msg = { "register": { @@ -38,7 +33,6 @@ def test_agent_registration(self): } } - tx = agent._reg_contract.execute(msg, agent.wallet, funds=REG_FEE) tx.wait_to_complete() @@ -48,4 +42,4 @@ def test_agent_registration(self): if __name__ == "__main__": - unittest.main() \ No newline at end of file + unittest.main() From 1c45c261ab23d036bd6bd85afa417931368ba2e4 Mon Sep 17 00:00:00 2001 From: James Riehl Date: Mon, 9 Jan 2023 10:34:44 +0000 Subject: [PATCH 37/41] chore: revert endpoint and remove print statements --- examples/10-cleaning-demo/cleaner.py | 2 +- examples/10-cleaning-demo/user.py | 3 --- src/nexus/envelope.py | 12 +----------- 3 files changed, 2 insertions(+), 15 deletions(-) diff --git a/examples/10-cleaning-demo/cleaner.py b/examples/10-cleaning-demo/cleaner.py index b3352d2d..2f94798f 100644 --- a/examples/10-cleaning-demo/cleaner.py +++ b/examples/10-cleaning-demo/cleaner.py @@ -14,7 +14,7 @@ name="cleaner", port=8001, seed="cleaner secret seed phrase", - endpoint="https://bdf3-187-213-48-169.ngrok.io/submit", + endpoint="http://127.0.0.1:8001/submit", ) fund_agent_if_low(cleaner.wallet.address()) diff --git a/examples/10-cleaning-demo/user.py b/examples/10-cleaning-demo/user.py index 6a01be10..1a478488 100644 --- a/examples/10-cleaning-demo/user.py +++ b/examples/10-cleaning-demo/user.py @@ -34,9 +34,6 @@ MARKDOWN = 0.8 -print(Model.build_schema_digest(ServiceRequest)) -print(Model.build_schema_digest(ServiceBooking)) - @user.on_interval(period=3.0, messages=ServiceRequest) async def interval(ctx: Context): diff --git a/src/nexus/envelope.py b/src/nexus/envelope.py index 1369f57f..92c8585d 100644 --- a/src/nexus/envelope.py +++ b/src/nexus/envelope.py @@ -32,20 +32,10 @@ def sign(self, identity: Identity): self.signature = identity.sign_digest(self._digest()) def verify(self) -> bool: - if self.signature is None: return False - verification = Identity.verify_digest( - self.sender, self._digest(), self.signature - ) - - if verification: - print(f"Verifired request from {self.sender}") - else: - print(f"Failed to verify request from {self.sender}") - - return verification + return Identity.verify_digest(self.sender, self._digest(), self.signature) def _digest(self) -> bytes: hasher = hashlib.sha256() From 479ddd6d9ef08203235c6ed237232081c86101c0 Mon Sep 17 00:00:00 2001 From: James Riehl Date: Mon, 9 Jan 2023 10:47:12 +0000 Subject: [PATCH 38/41] chore: fix linting errors --- examples/10-cleaning-demo/user.py | 2 +- tests/test_agent_registration.py | 7 ++++--- tests/test_verify_msg.py | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/examples/10-cleaning-demo/user.py b/examples/10-cleaning-demo/user.py index 1a478488..8d33f508 100644 --- a/examples/10-cleaning-demo/user.py +++ b/examples/10-cleaning-demo/user.py @@ -8,7 +8,7 @@ ServiceResponse, ) from protocols.cleaning.models import ServiceType -from nexus import Agent, Context, Model +from nexus import Agent, Context from nexus.setup import fund_agent_if_low diff --git a/tests/test_agent_registration.py b/tests/test_agent_registration.py index 65e753d6..dbbeaa01 100644 --- a/tests/test_agent_registration.py +++ b/tests/test_agent_registration.py @@ -1,3 +1,4 @@ +# pylint: disable=protected-access import unittest from nexus import Agent @@ -9,7 +10,7 @@ def test_agent_registration(self): agent = Agent(name="alice") - REG_FEE = "500000000000000000atestfet" + reg_fee = "500000000000000000atestfet" fund_agent_if_low(agent.wallet.address()) @@ -33,8 +34,8 @@ def test_agent_registration(self): } } - tx = agent._reg_contract.execute(msg, agent.wallet, funds=REG_FEE) - tx.wait_to_complete() + transaction = agent._reg_contract.execute(msg, agent.wallet, funds=reg_fee) + transaction.wait_to_complete() is_registered = agent.registration_status() diff --git a/tests/test_verify_msg.py b/tests/test_verify_msg.py index 68f3ffcc..8fc40cc8 100644 --- a/tests/test_verify_msg.py +++ b/tests/test_verify_msg.py @@ -31,7 +31,7 @@ def test_verify_dart_digest(self): address = "agent1qf5gfqm48k9acegez3sg82ney2aa6l5fvpwh3n3z0ajh0nam3ssgwnn5me7" # Signature - signature = "sig1qyvn5fjzrhjzqcmj2gfg4us6xj00gvscs4u9uqxy6wpvp9agxjf723eh5l6w878p67lycgd3fz77zr3h0q6mrheg48e35zsvv0rm2tsuvyn3l" + signature = "sig1qyvn5fjzrhjzqcmj2gfg4us6xj00gvscs4u9uqxy6wpvp9agxjf723eh5l6w878p67lycgd3fz77zr3h0q6mrheg48e35zsvv0rm2tsuvyn3l" # pylint: disable=line-too-long # Message dart_digest = "a29af8b704077d394a9756dc04f0bb5f1424fc391b3de91144d683c5893ca234" From 227faed17a23821c4a0357ec9b18f7293a3d46cd Mon Sep 17 00:00:00 2001 From: James Riehl <33920192+jrriehl@users.noreply.github.com> Date: Tue, 10 Jan 2023 09:42:16 +0000 Subject: [PATCH 39/41] test: add asgi server tests (#34) Co-authored-by: Alejandro-Morales --- src/nexus/agent.py | 12 +- tests/test_server.py | 463 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 469 insertions(+), 6 deletions(-) create mode 100644 tests/test_server.py diff --git a/src/nexus/agent.py b/src/nexus/agent.py index e063d65d..190ffc46 100644 --- a/src/nexus/agent.py +++ b/src/nexus/agent.py @@ -57,7 +57,7 @@ def __init__( self._name = name self._intervals: List[Tuple[float, Any]] = [] self._port = port if port is not None else 8000 - self._background_tasks = set() + self._background_tasks: Set[asyncio.Task] = set() self._resolver = resolve if resolve is not None else AlmanacResolver() self._loop = asyncio.get_event_loop_policy().get_event_loop() if seed is None: @@ -111,11 +111,6 @@ def __init__( # register with the dispatcher self._dispatcher.register(self.address, self) - # start the background message queue processor - task = self._loop.create_task(self._process_message_queue()) - self._background_tasks.add(task) - task.add_done_callback(self._background_tasks.discard) - self._server = ASGIServer(self._port, self._loop, self._queries) @property @@ -302,6 +297,11 @@ def setup(self): # register the internal agent protocol self.include(self._protocol) + # start the background message queue processor + task = self._loop.create_task(self._process_message_queue()) + self._background_tasks.add(task) + task.add_done_callback(self._background_tasks.discard) + # start the contract registration update loop self._loop.create_task( _run_interval(self.register, self._ctx, REG_UPDATE_INTERVAL_SECONDS) diff --git a/tests/test_server.py b/tests/test_server.py new file mode 100644 index 00000000..fc253afb --- /dev/null +++ b/tests/test_server.py @@ -0,0 +1,463 @@ +import asyncio +import unittest +import uuid +from unittest.mock import patch, AsyncMock, call + +from nexus import Agent, Model +from nexus.envelope import Envelope +from nexus.crypto import generate_user_address +from nexus.query import enclose_response + + +class Message(Model): + message: str + + +class TestServer(unittest.IsolatedAsyncioTestCase): + def setUp(self) -> None: + self.agent = Agent(name="alice", seed="alice recovery password") + self.bob = Agent(name="bob", seed="bob recovery password") + self.loop: asyncio.BaseEventLoop = self._asyncioTestLoop + return super().setUp() + + async def mock_process_sync_message(self, sender: str, msg: Model): + while True: + if sender in self.agent._server._queries: + self.agent._server._queries[sender].set_result(msg) + return + + async def test_message_success(self): + message = Message(message="hello") + env = Envelope( + version=1, + sender=self.bob.address, + target=self.agent.address, + session=uuid.uuid4(), + protocol=Model.build_schema_digest(message), + ) + env.encode_payload(message.json()) + env.sign(self.bob._identity) + + mock_send = AsyncMock() + with patch("nexus.asgi._read_asgi_body") as mock_receive: + mock_receive.return_value = env.json().encode() + await self.agent._server( + scope=dict( + type="http", + path="/submit", + headers={b"content-type": b"application/json"}, + ), + receive=None, + send=mock_send, + ) + mock_send.assert_has_calls( + [ + call( + { + "type": "http.response.start", + "status": 200, + "headers": [[b"content-type", b"application/json"]], + } + ), + call( + { + "type": "http.response.body", + "body": b"{}", + } + ), + ] + ) + + async def test_message_success_unsigned(self): + message = Message(message="hello") + user = generate_user_address() + session = uuid.uuid4() + env = Envelope( + version=1, + sender=user, + target=self.agent.address, + session=session, + protocol=Model.build_schema_digest(message), + ) + env.encode_payload(message.json()) + + mock_send = AsyncMock() + with patch("nexus.asgi._read_asgi_body") as mock_receive: + mock_receive.return_value = env.json().encode() + await self.agent._server( + scope=dict( + type="http", + path="/submit", + headers={b"content-type": b"application/json"}, + ), + receive=None, + send=mock_send, + ) + mock_send.assert_has_calls( + [ + call( + { + "type": "http.response.start", + "status": 200, + "headers": [[b"content-type", b"application/json"]], + } + ), + call( + { + "type": "http.response.body", + "body": b"{}", + } + ), + ] + ) + + async def test_message_success_sync(self): + message = Message(message="hello") + reply = Message(message="hey") + user = generate_user_address() + session = uuid.uuid4() + env = Envelope( + version=1, + sender=user, + target=self.agent.address, + session=session, + protocol=Model.build_schema_digest(message), + ) + env.encode_payload(message.json()) + mock_send = AsyncMock() + with patch("nexus.asgi._read_asgi_body") as mock_receive: + mock_receive.return_value = env.json().encode() + await asyncio.gather( + self.loop.create_task( + self.agent._server( + scope=dict( + type="http", + path="/submit", + headers={ + b"content-type": b"application/json", + b"x-uagents-connection": b"sync", + }, + ), + receive=None, + send=mock_send, + ) + ), + self.loop.create_task(self.mock_process_sync_message(user, reply)), + ) + response = enclose_response(reply, self.agent.address, session) + mock_send.assert_has_calls( + [ + call( + { + "type": "http.response.start", + "status": 200, + "headers": [[b"content-type", b"application/json"]], + } + ), + call( + { + "type": "http.response.body", + "body": response.encode(), + } + ), + ] + ) + + async def test_message_success_sync_unsigned(self): + message = Message(message="hello") + reply = Message(message="hey") + session = uuid.uuid4() + env = Envelope( + version=1, + sender=self.bob.address, + target=self.agent.address, + session=session, + protocol=Model.build_schema_digest(message), + ) + env.encode_payload(message.json()) + env.sign(self.bob._identity) + mock_send = AsyncMock() + with patch("nexus.asgi._read_asgi_body") as mock_receive: + mock_receive.return_value = env.json().encode() + await asyncio.gather( + self.loop.create_task( + self.agent._server( + scope=dict( + type="http", + path="/submit", + headers={ + b"content-type": b"application/json", + b"x-uagents-connection": b"sync", + }, + ), + receive=None, + send=mock_send, + ) + ), + self.loop.create_task( + self.mock_process_sync_message(self.bob.address, reply) + ), + ) + response = enclose_response(reply, self.agent.address, session) + mock_send.assert_has_calls( + [ + call( + { + "type": "http.response.start", + "status": 200, + "headers": [[b"content-type", b"application/json"]], + } + ), + call( + { + "type": "http.response.body", + "body": response.encode(), + } + ), + ] + ) + + async def test_message_fail_wrong_path(self): + message = Message(message="hello") + env = Envelope( + version=1, + sender=self.bob.address, + target=self.agent.address, + session=uuid.uuid4(), + protocol=Model.build_schema_digest(message), + ) + env.encode_payload(message.json()) + env.sign(self.bob._identity) + + mock_send = AsyncMock() + with patch("nexus.asgi._read_asgi_body") as mock_receive: + mock_receive.return_value = env.json().encode() + await self.agent._server( + scope=dict( + type="http", + path="/bad/path", + headers={b"content-type": b"application/json"}, + ), + receive=None, + send=mock_send, + ) + mock_send.assert_has_calls( + [ + call( + { + "type": "http.response.start", + "status": 404, + "headers": [[b"content-type", b"application/json"]], + } + ), + call( + { + "type": "http.response.body", + "body": b'{"error": "not found"}', + } + ), + ] + ) + + async def test_message_fail_wrong_headers(self): + message = Message(message="hello") + env = Envelope( + version=1, + sender=self.bob.address, + target=self.agent.address, + session=uuid.uuid4(), + protocol=Model.build_schema_digest(message), + ) + env.encode_payload(message.json()) + env.sign(self.bob._identity) + + mock_send = AsyncMock() + with patch("nexus.asgi._read_asgi_body") as mock_receive: + mock_receive.return_value = env.json().encode() + await self.agent._server( + scope=dict( + type="http", + path="/submit", + headers={b"content-type": b"application/badapp"}, + ), + receive=None, + send=mock_send, + ) + mock_send.assert_has_calls( + [ + call( + { + "type": "http.response.start", + "status": 400, + "headers": [[b"content-type", b"application/json"]], + } + ), + call( + { + "type": "http.response.body", + "body": b'{"error": "invalid format"}', + } + ), + ] + ) + + async def test_message_fail_bad_data(self): + message = Message(message="hello") + mock_send = AsyncMock() + with patch("nexus.asgi._read_asgi_body") as mock_receive: + mock_receive.return_value = message.json().encode() + await self.agent._server( + scope=dict( + type="http", + path="/submit", + headers={b"content-type": b"application/json"}, + ), + receive=None, + send=mock_send, + ) + mock_send.assert_has_calls( + [ + call( + { + "type": "http.response.start", + "status": 400, + "headers": [[b"content-type", b"application/json"]], + } + ), + call( + { + "type": "http.response.body", + "body": b'{"error": "invalid format"}', + } + ), + ] + ) + + async def test_message_fail_unsigned(self): + message = Message(message="hello") + env = Envelope( + version=1, + sender=self.bob.address, + target=self.agent.address, + session=uuid.uuid4(), + protocol=Model.build_schema_digest(message), + ) + env.encode_payload(message.json()) + + mock_send = AsyncMock() + with patch("nexus.asgi._read_asgi_body") as mock_receive: + mock_receive.return_value = env.json().encode() + await self.agent._server( + scope=dict( + type="http", + path="/submit", + headers={b"content-type": b"application/json"}, + ), + receive=None, + send=mock_send, + ) + mock_send.assert_has_calls( + [ + call( + { + "type": "http.response.start", + "status": 400, + "headers": [[b"content-type", b"application/json"]], + } + ), + call( + { + "type": "http.response.body", + "body": b'{"error": "unable to verify payload"}', + } + ), + ] + ) + + async def test_message_fail_verify(self): + message = Message(message="hello") + env = Envelope( + version=1, + sender=self.bob.address, + target=self.agent.address, + session=uuid.uuid4(), + protocol=Model.build_schema_digest(message), + ) + env.encode_payload(message.json()) + env.sign(self.agent._identity) + + mock_send = AsyncMock() + with patch("nexus.asgi._read_asgi_body") as mock_receive: + mock_receive.return_value = env.json().encode() + await self.agent._server( + scope=dict( + type="http", + path="/submit", + headers={b"content-type": b"application/json"}, + ), + receive=None, + send=mock_send, + ) + mock_send.assert_has_calls( + [ + call( + { + "type": "http.response.start", + "status": 400, + "headers": [[b"content-type", b"application/json"]], + } + ), + call( + { + "type": "http.response.body", + "body": b'{"error": "unable to verify payload"}', + } + ), + ] + ) + + async def test_message_fail_dispatch(self): + message = Message(message="hello") + env = Envelope( + version=1, + sender=self.bob.address, + target=generate_user_address(), + session=uuid.uuid4(), + protocol=Model.build_schema_digest(message), + ) + env.encode_payload(message.json()) + env.sign(self.bob._identity) + + mock_send = AsyncMock() + with patch("nexus.asgi._read_asgi_body") as mock_receive: + mock_receive.return_value = env.json().encode() + await self.agent._server( + scope=dict( + type="http", + path="/submit", + headers={b"content-type": b"application/json"}, + ), + receive=None, + send=mock_send, + ) + mock_send.assert_has_calls( + [ + call( + { + "type": "http.response.start", + "status": 400, + "headers": [[b"content-type", b"application/json"]], + } + ), + call( + { + "type": "http.response.body", + "body": b'{"error": "unable to route envelope"}', + } + ), + ] + ) + + +if __name__ == "__main__": + unittest.main() From ce589000e0eaec721a2bb93b7f92c18d1266e00b Mon Sep 17 00:00:00 2001 From: James Riehl Date: Fri, 13 Jan 2023 14:31:56 +0000 Subject: [PATCH 40/41] fix: pylint and test event loop --- tests/test_server.py | 1 + tests/test_verify_msg.py | 2 ++ 2 files changed, 3 insertions(+) diff --git a/tests/test_server.py b/tests/test_server.py index fc253afb..59e379f0 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -1,3 +1,4 @@ +# pylint: disable=protected-access import asyncio import unittest import uuid diff --git a/tests/test_verify_msg.py b/tests/test_verify_msg.py index 8fc40cc8..555f0413 100644 --- a/tests/test_verify_msg.py +++ b/tests/test_verify_msg.py @@ -1,3 +1,4 @@ +import asyncio import hashlib import unittest @@ -13,6 +14,7 @@ def encode(message: str) -> bytes: class TestVerify(unittest.TestCase): def test_sign_and_verify_message(self): + asyncio.set_event_loop(asyncio.new_event_loop()) alice = Agent(name="alice", seed="alice recovery password") alice_msg = "hello there bob" From 3045b776acbcc15b2c5a9dad23703e96194b835f Mon Sep 17 00:00:00 2001 From: James Riehl Date: Fri, 13 Jan 2023 14:57:16 +0000 Subject: [PATCH 41/41] fix: set message type to json str --- src/nexus/agent.py | 6 +++--- src/nexus/asgi.py | 2 +- src/nexus/context.py | 3 ++- src/nexus/dispatch.py | 8 +++++--- src/nexus/models.py | 7 ------- 5 files changed, 11 insertions(+), 15 deletions(-) diff --git a/src/nexus/agent.py b/src/nexus/agent.py index 190ffc46..cdae0472 100644 --- a/src/nexus/agent.py +++ b/src/nexus/agent.py @@ -14,7 +14,7 @@ MsgDigest, ) from nexus.crypto import Identity, derive_key_from_seed, is_user_address -from nexus.dispatch import Sink, dispatcher +from nexus.dispatch import Sink, dispatcher, JsonStr from nexus.models import Model, ErrorMessage from nexus.protocol import Protocol from nexus.resolver import Resolver, AlmanacResolver @@ -282,7 +282,7 @@ def include(self, protocol: Protocol): if protocol.digest is not None: self.protocols[protocol.canonical_name] = protocol.digest - async def handle_message(self, sender, schema_digest: str, message: Any): + async def handle_message(self, sender, schema_digest: str, message: JsonStr): await self._message_queue.put((schema_digest, sender, message)) async def startup(self): @@ -326,7 +326,7 @@ async def _process_message_queue(self): continue # parse the received message - recovered = model_class.parse(message) + recovered = model_class.parse_raw(message) context = Context( self._identity.address, diff --git a/src/nexus/asgi.py b/src/nexus/asgi.py index f683acfd..c63a4c01 100644 --- a/src/nexus/asgi.py +++ b/src/nexus/asgi.py @@ -143,7 +143,7 @@ async def __call__(self, scope, receive, send): return await dispatcher.dispatch( - env.sender, env.target, env.protocol, env.decode_payload() + env.sender, env.target, env.protocol, json.dumps(env.decode_payload()) ) # wait for any queries to be resolved diff --git a/src/nexus/context.py b/src/nexus/context.py index 05ab43e7..d14cb8ee 100644 --- a/src/nexus/context.py +++ b/src/nexus/context.py @@ -1,4 +1,5 @@ import asyncio +import json import logging import uuid from dataclasses import dataclass @@ -105,7 +106,7 @@ async def send( # handle local dispatch of messages if dispatcher.contains(destination): await dispatcher.dispatch( - self.address, destination, schema_digest, json_message + self.address, destination, schema_digest, json.dumps(json_message) ) return diff --git a/src/nexus/dispatch.py b/src/nexus/dispatch.py index 072c82e9..50bf145d 100644 --- a/src/nexus/dispatch.py +++ b/src/nexus/dispatch.py @@ -1,10 +1,12 @@ from abc import ABC, abstractmethod -from typing import Dict, Set, Any +from typing import Dict, Set + +JsonStr = str class Sink(ABC): @abstractmethod - async def handle_message(self, sender: str, schema_digest: str, message: Any): + async def handle_message(self, sender: str, schema_digest: str, message: JsonStr): pass @@ -26,7 +28,7 @@ def contains(self, address: str) -> bool: return address in self._sinks async def dispatch( - self, sender: str, destination: str, schema_digest: str, message: Any + self, sender: str, destination: str, schema_digest: str, message: JsonStr ): for handler in self._sinks.get(destination, set()): await handler.handle_message(sender, schema_digest, message) diff --git a/src/nexus/models.py b/src/nexus/models.py index a641af93..f62b9136 100644 --- a/src/nexus/models.py +++ b/src/nexus/models.py @@ -1,5 +1,4 @@ import hashlib -from typing import Any, Dict from pydantic import BaseModel @@ -15,12 +14,6 @@ def build_schema_digest(model: "Model") -> str: .hex() ) - @classmethod - def parse(cls, model: Any) -> Dict[str, Any]: - if isinstance(model, dict): - return cls.parse_obj(model) - return cls.parse_raw(model) - class ErrorMessage(Model): error: str