Skip to content

Adding new API

API is constelite is responsible for:

  • Providing user access to call protocols
  • Providing user access to stores

For example, StarliteAPI creates a web server that accepts HTTP requests and translates them either into protocol calls or access to the core store methods: get, patch, post, delete and query.

If you want to support other ways to call protocols or access stores, you need a new API.

To create a new API, you should create a new class derived from the ConsteliteAPI and implement a run method

from constelite.api import ConsteliteAPI

class MyAPI(ConsteliteAPI):
    def run(self, *args, **kwargs) -> None:
        ...

Not much, eh?

Don't worry, ConsteliteAPI is packed with methods that will help you to design your new API.

Getting protocols

ConsteliteAPI has a method for discovering protocols (discover_protocols). Usually it will be called before the API is started and will populate API's protocols with instances of ProtocolModel.

class MyAPI(ConsteliteAPI):
    def run(self, *args, **kwargs) -> None:
        ...

    def print_protocols(self) -> None:
        for protocol_model in self.protocols:
            print(protocol_model)


api = MyAPI()

api.print_protocols()

An example of what you will get:

path='hello_world' name='HelloWorldProtocol' fn=<function protocol.wrap_fn.<locals>.wrapper at 0x10045d1c0> fn_model=<class 'pydantic.v1.main.hello_world'> ret_model=None slug='hello_world'

Looks like we are dealing with a protocol called "HelloWorldProtocol" with function that encapsulates the protocol logic given in fn. In addition, we are getting fn_model a BaseModel that we can use for validating function arguments and ret_model tells us that this protocol returns None

Calling protocols

Say your API received a message from the user asking to call hello_world protocol with arguments kwargs = {"name": "Lisa"}. All you need to do, as an API developer is call run_protocol method. For example:

class MyAPI(ConsteliteAPI):
    async def on_call_protocol_message_received(self, protocol_slug: str, kwargs: dict):
        logger = await self.get_logger()
        try:
            ret = await self.run_protocol(slug=protocol_slug, logger=logger, **kwargs)
        except ValueError:
            await self.send_message_to_user(f"Protocol {protocol_slug} is not found")
        except Exception as e:
            await self.send_message_to_user(f"Protocol {protocol_slug} failed with error: {e}")

    async send_message_to_user(self, message: str):
        ...

Handling store calls

If your API support operations with a store, here is how you could handle them:

class MyAPI(ConsteliteAPI):
    async def handle_put_request(self, ref: Ref, store_model: Optional[StoreModel]):
        if store is not None:
            store_uid = store.uid
        elif ref.record is not None:
            store_uid = ref.record.store.uid
        else:
            raise ValueError("Can't figure out where to save the reference")

        store = self.get_store(uid=store_uid)
        store.put(ref=ref)

Here we are using get_store method of the ConsteliteAPI to retrieve a store object that is registered under the given UID.