|
| 1 | +# Project description |
| 2 | + |
| 3 | +Cloudstate is a specification, protocol, and reference implementation for providing distributed state management patterns suitable for **Serverless** computing. |
| 4 | +The current supported and envisioned patterns include: |
| 5 | + |
| 6 | +* **Event Sourcing** |
| 7 | +* **Conflict-Free Replicated Data Types (CRDTs)** |
| 8 | +* **Key-Value storage** |
| 9 | +* **P2P messaging** |
| 10 | +* **CQRS read side projections** |
| 11 | + |
| 12 | +Cloudstate is polyglot, which means that services can be written in any language that supports gRPC, |
| 13 | +and with language specific libraries provided that allow idiomatic use of the patterns in each language. |
| 14 | +Cloudstate can be used either by itself, in combination with a Service Mesh, |
| 15 | +or it is envisioned that it will be integrated with other Serverless technologies such as [Knative](https://knative.dev/). |
| 16 | + |
| 17 | +Read more about the design, architecture, techniques, and technologies behind Cloudstate in [this section in the documentation](https://github.com/cloudstateio/cloudstate/blob/master/README.md#enter-cloudstate). |
| 18 | + |
| 19 | +The Cloudstate Python user language support is a library that implements the Cloudstate protocol and offers an pythonistic API |
| 20 | +for writing entities that implement the types supported by the Cloudstate protocol. |
| 21 | + |
| 22 | +The Cloudstate documentation can be found [here](https://cloudstate.io/docs/) |
| 23 | + |
| 24 | +## Install and update using pip: |
| 25 | + |
| 26 | +``` |
| 27 | +pip install -U cloudstate |
| 28 | +``` |
| 29 | + |
| 30 | +## A Simple EventSourced Example: |
| 31 | + |
| 32 | +### 1. Define your gRPC contract |
| 33 | + |
| 34 | +``` |
| 35 | +// This is the public API offered by the shopping cart entity. |
| 36 | +syntax = "proto3"; |
| 37 | +
|
| 38 | +import "google/protobuf/empty.proto"; |
| 39 | +import "cloudstate/entity_key.proto"; |
| 40 | +import "google/api/annotations.proto"; |
| 41 | +import "google/api/http.proto"; |
| 42 | +
|
| 43 | +package com.example.shoppingcart; |
| 44 | +
|
| 45 | +message AddLineItem { |
| 46 | + string user_id = 1 [(.cloudstate.entity_key) = true]; |
| 47 | + string product_id = 2; |
| 48 | + string name = 3; |
| 49 | + int32 quantity = 4; |
| 50 | +} |
| 51 | +
|
| 52 | +message RemoveLineItem { |
| 53 | + string user_id = 1 [(.cloudstate.entity_key) = true]; |
| 54 | + string product_id = 2; |
| 55 | +} |
| 56 | +
|
| 57 | +message GetShoppingCart { |
| 58 | + string user_id = 1 [(.cloudstate.entity_key) = true]; |
| 59 | +} |
| 60 | +
|
| 61 | +message LineItem { |
| 62 | + string product_id = 1; |
| 63 | + string name = 2; |
| 64 | + int32 quantity = 3; |
| 65 | +} |
| 66 | +
|
| 67 | +message Cart { |
| 68 | + repeated LineItem items = 1; |
| 69 | +} |
| 70 | +
|
| 71 | +service ShoppingCart { |
| 72 | + rpc AddItem(AddLineItem) returns (google.protobuf.Empty) { |
| 73 | + option (google.api.http) = { |
| 74 | + post: "/cart/{user_id}/items/add", |
| 75 | + body: "*", |
| 76 | + }; |
| 77 | + } |
| 78 | +
|
| 79 | + rpc RemoveItem(RemoveLineItem) returns (google.protobuf.Empty) { |
| 80 | + option (google.api.http).post = "/cart/{user_id}/items/{product_id}/remove"; |
| 81 | + } |
| 82 | +
|
| 83 | + rpc GetCart(GetShoppingCart) returns (Cart) { |
| 84 | + option (google.api.http) = { |
| 85 | + get: "/carts/{user_id}", |
| 86 | + additional_bindings: { |
| 87 | + get: "/carts/{user_id}/items", |
| 88 | + response_body: "items" |
| 89 | + } |
| 90 | + }; |
| 91 | + } |
| 92 | +} |
| 93 | +
|
| 94 | +``` |
| 95 | + |
| 96 | +### 2. Generate Python files |
| 97 | + |
| 98 | +It is necessary to compile your .proto files using the protoc compiler in order to generate Python files. |
| 99 | +See [this official gRPC for Python quickstart](https://grpc.io/docs/languages/python/quickstart/) if you are not familiar with the gRPC protocol. |
| 100 | + |
| 101 | +Here is an example of how to compile the sample proto file: |
| 102 | +``` |
| 103 | +python -m grpc_tools.protoc -I../../protos --python_out=. --grpc_python_out=. ../../protos/shoppingcart.proto |
| 104 | +``` |
| 105 | + |
| 106 | +### 3. Implement your business logic under an EventSourced Cloudstate Entity |
| 107 | + |
| 108 | +``` |
| 109 | +from dataclasses import dataclass, field |
| 110 | +from typing import MutableMapping |
| 111 | +
|
| 112 | +from google.protobuf.empty_pb2 import Empty |
| 113 | +
|
| 114 | +from cloudstate.event_sourced_context import EventSourcedCommandContext |
| 115 | +from cloudstate.event_sourced_entity import EventSourcedEntity |
| 116 | +from shoppingcart.domain_pb2 import (Cart as DomainCart, LineItem as DomainLineItem, ItemAdded, ItemRemoved) |
| 117 | +from shoppingcart.shoppingcart_pb2 import (Cart, LineItem, AddLineItem, RemoveLineItem) |
| 118 | +from shoppingcart.shoppingcart_pb2 import (_SHOPPINGCART, DESCRIPTOR as FILE_DESCRIPTOR) |
| 119 | +
|
| 120 | +
|
| 121 | +@dataclass |
| 122 | +class ShoppingCartState: |
| 123 | + entity_id: str |
| 124 | + cart: MutableMapping[str, LineItem] = field(default_factory=dict) |
| 125 | +
|
| 126 | +
|
| 127 | +def init(entity_id: str) -> ShoppingCartState: |
| 128 | + return ShoppingCartState(entity_id) |
| 129 | +
|
| 130 | +
|
| 131 | +entity = EventSourcedEntity(_SHOPPINGCART, [FILE_DESCRIPTOR], init) |
| 132 | +
|
| 133 | +
|
| 134 | +def to_domain_line_item(item): |
| 135 | + domain_item = DomainLineItem() |
| 136 | + domain_item.productId = item.product_id |
| 137 | + domain_item.name = item.name |
| 138 | + domain_item.quantity = item.quantity |
| 139 | + return domain_item |
| 140 | +
|
| 141 | +
|
| 142 | +@entity.snapshot() |
| 143 | +def snapshot(state: ShoppingCartState): |
| 144 | + cart = DomainCart() |
| 145 | + cart.items = [to_domain_line_item(item) for item in state.cart.values()] |
| 146 | + return cart |
| 147 | +
|
| 148 | +
|
| 149 | +def to_line_item(domain_item): |
| 150 | + item = LineItem() |
| 151 | + item.product_id = domain_item.productId |
| 152 | + item.name = domain_item.name |
| 153 | + item.quantity = domain_item.quantity |
| 154 | + return item |
| 155 | +
|
| 156 | +
|
| 157 | +@entity.snapshot_handler() |
| 158 | +def handle_snapshot(state: ShoppingCartState, domain_cart: DomainCart): |
| 159 | + state.cart = {domain_item.productId: to_line_item(domain_item) for domain_item in domain_cart.items} |
| 160 | +
|
| 161 | +
|
| 162 | +@entity.event_handler(ItemAdded) |
| 163 | +def item_added(state: ShoppingCartState, event: ItemAdded): |
| 164 | + cart = state.cart |
| 165 | + if event.item.productId in cart: |
| 166 | + item = cart[event.item.productId] |
| 167 | + item.quantity = item.quantity + event.item.quantity |
| 168 | + else: |
| 169 | + item = to_line_item(event.item) |
| 170 | + cart[item.product_id] = item |
| 171 | +
|
| 172 | +
|
| 173 | +@entity.event_handler(ItemRemoved) |
| 174 | +def item_removed(state: ShoppingCartState, event: ItemRemoved): |
| 175 | + del state.cart[event.productId] |
| 176 | +
|
| 177 | +
|
| 178 | +@entity.command_handler("GetCart") |
| 179 | +def get_cart(state: ShoppingCartState): |
| 180 | + cart = Cart() |
| 181 | + cart.items.extend(state.cart.values()) |
| 182 | + return cart |
| 183 | +
|
| 184 | +
|
| 185 | +@entity.command_handler("AddItem") |
| 186 | +def add_item(item: AddLineItem, ctx: EventSourcedCommandContext): |
| 187 | + if item.quantity <= 0: |
| 188 | + ctx.fail("Cannot add negative quantity of to item {}".format(item.productId)) |
| 189 | + else: |
| 190 | + item_added_event = ItemAdded() |
| 191 | + item_added_event.item.CopyFrom(to_domain_line_item(item)) |
| 192 | + ctx.emit(item_added_event) |
| 193 | + return Empty() |
| 194 | +
|
| 195 | +
|
| 196 | +@entity.command_handler("RemoveItem") |
| 197 | +def remove_item(state: ShoppingCartState, item: RemoveLineItem, ctx: EventSourcedCommandContext): |
| 198 | + cart = state.cart |
| 199 | + if item.product_id not in cart: |
| 200 | + ctx.fail("Cannot remove item {} because it is not in the cart.".format(item.productId)) |
| 201 | + else: |
| 202 | + item_removed_event = ItemRemoved() |
| 203 | + item_removed_event.productId = item.product_id |
| 204 | + ctx.emit(item_removed_event) |
| 205 | + return Empty() |
| 206 | +``` |
| 207 | + |
| 208 | +### 4. Register Entity |
| 209 | + |
| 210 | +``` |
| 211 | +from cloudstate.cloudstate import CloudState |
| 212 | +from shoppingcart.shopping_cart_entity import entity as shopping_cart_entity |
| 213 | +import logging |
| 214 | +
|
| 215 | +if __name__ == '__main__': |
| 216 | + logging.basicConfig() |
| 217 | + CloudState().register_event_sourced_entity(shopping_cart_entity).start() |
| 218 | +``` |
| 219 | + |
| 220 | +### 5. Deployment |
| 221 | + |
| 222 | +Cloudstate runs on Docker and Kubernetes you need to package your application so that it works as a Docker container |
| 223 | +and can deploy it together with Cloudstate Operator on Kubernetes, the details and examples of all of which can be found [here](https://code.visualstudio.com/docs/containers/quickstart-python), [here](https://github.com/cloudstateio/python-support/blob/master/shoppingcart/Dockerfile) and [here](https://cloudstate.io/docs/core/current/user/deployment/index.html). |
| 224 | + |
| 225 | +## Contributing |
| 226 | + |
| 227 | +For guidance on setting up a development environment and how to make a contribution to Cloudastate, |
| 228 | +see the contributing [project page](https://github.com/cloudstateio/python-support) or consult an official documentation [here](https://cloudstate.io/docs/). |
| 229 | + |
| 230 | +## Links |
| 231 | + |
| 232 | +* [Website:](https://cloudstate.io/) |
| 233 | +* [Documentation:](https://cloudstate.io/docs/) |
| 234 | +* [Releases:](https://pypi.org/project/cloudstate/) |
| 235 | +* [Code:](https://github.com/cloudstateio/python-support) |
| 236 | +* [Issue tracker:](https://github.com/cloudstateio/python-support/issues) |
0 commit comments