How does it work?¶
Base Model¶
AppKernel is built around Domain-Driven Design. You start a project by laying out the domain model (the Entity).
Note
All example code below can be followed in Python’s interactive console.
Prerequisites: MongoDB 4.0+ running on localhost and AppKernel installed in a virtual environment
(pip install appkernel).
The Model class represents the data in your application. Fields are declared as class-level
type annotations. All fields default to None — validation runs later, explicitly via
finalise_and_validate() or implicitly when calling save() or dumps():
from typing import Annotated
from appkernel import Model
class User(Model):
id: str | None = None
name: str | None = None
email: str | None = None
password: str | None = None
roles: list[str] | None = None
You get a keyword-argument constructor, JSON serialisation, and a readable string representation for free:
u = User(name='some name', email='some name')
u.password = 'some pass'
str(u)
'<User> {"email": "some name", "name": "some name", "password": "some pass"}'
u.dumps()
'{"email": "some name", "name": "some name", "password": "some pass"}'
Or with pretty-printing:
print(u.dumps(pretty_print=True))
{
"email": "some name",
"name": "some name",
"password": "some pass"
}
Now let’s add validation rules and a default value:
from appkernel import Required, Default, Validators, Email
class User(Model):
id: str | None = None
name: Annotated[str | None, Required()] = None
email: Annotated[str | None, Validators(Email)] = None
password: Annotated[str | None, Required()] = None
roles: Annotated[list[str] | None, Default(['Login'])] = None
Trying to serialise with an invalid e-mail:
u = User(name='some name', email='not-an-email')
u.dumps()
ValidationException: REGEXP on type str - The property email cannot be validated ...
That’s expected — ‘not-an-email’ is not a valid address. Let’s fix it:
u.email = 'user@acme.com'
u.dumps()
PropertyRequiredException: The property [password] on class [User] is required.
Also expected — the required password is missing. Final attempt:
u.password = 'some pass'
print(u.dumps(pretty_print=True))
{
"email": "user@acme.com",
"name": "some name",
"password": "some pass",
"roles": [
"Login"
]
}
The Default(['Login']) marker on the roles field populated it automatically.
To validate a model without serialising it:
u.finalise_and_validate()
finalise_and_validate() does more than validate — it also runs generators and converters:
a generator produces a value for a field when it is
None(e.g. UUID, current timestamp);a converter transforms an existing value (e.g. hashing a password, normalising text);
Let’s add both:
from appkernel import Generator, Converter, create_uuid_generator, content_hasher
class User(Model):
id: Annotated[str | None, Generator(create_uuid_generator('U'))] = None
name: Annotated[str | None, Required()] = None
email: Annotated[str | None, Validators(Email)] = None
password: Annotated[str | None, Required(), Converter(content_hasher())] = None
roles: Annotated[list[str] | None, Default(['Login'])] = None
u = User(name='some name', email='user@acme.com', password='some pass')
print(u.dumps(pretty_print=True))
{
"email": "user@acme.com",
"id": "U013333e7-9f23-4e9d-80de-480505535cad",
"name": "some name",
"password": "$pbkdf2-sha256$20000$C0GI8f4/B2AsRah1LiWE8A$2KBVlwBMtaoy1c2dhNORCETNEwssKMnYvB5NAPbkg1s",
"roles": [
"Login"
]
}
Two things happened:
The id was auto-generated and prefixed with
'U', making it immediately identifiable as a User record just from the ID alone.The password was hashed before storage, so the plain-text value never touches the database.
Service classes¶
Once you have your model, you can persist it in MongoDB and expose it as a REST service by mixing in the appropriate classes.
Repository¶
Extend MongoRepository to add CRUD, schema generation, indexing, and querying:
from appkernel import Model, MongoRepository, AppKernelEngine
from appkernel import Required, Generator, Converter, Validators, Email
from appkernel import create_uuid_generator, content_hasher
from typing import Annotated
kernel = AppKernelEngine('tutorial', enable_defaults=True)
class User(Model, MongoRepository):
id: Annotated[str | None, Generator(create_uuid_generator('U'))] = None
name: Annotated[str | None, Required()] = None
email: Annotated[str | None, Validators(Email)] = None
password: Annotated[str | None, Required(), Converter(content_hasher())] = None
roles: Annotated[list[str] | None, Default(['Login'])] = None
u = User(name='some name', email='user@acme.com', password='some pass')
u.save()
# Returns the saved document's ID
'U7ebc9ae7-d33c-458e-af56-d08283dcabb7'
Retrieve it by ID:
loaded_user = User.find_by_id(u.id)
print(loaded_user)
<User> {"email": "user@acme.com", "id": "Ua727d463-...", "name": "some name", "roles": ["Login"]}
Or with a query expression:
user_at_acme = User.where(User.email == 'user@acme.com').find_one()
print(user_at_acme.dumps(pretty_print=True))
{
"email": "user@acme.com",
"id": "Ueeb4139a-1e35-43cd-ab69-7bc3b9104ae4",
"name": "some name",
"roles": ["Login"]
}
More details are in the Repository section.
REST Service¶
Registering a model with the AppKernel engine exposes it as a REST API. No separate Service class is needed — just register the model:
from appkernel import AppKernelEngine, Model, MongoRepository
from appkernel import Required, Generator, Converter, Validators, Email
from appkernel import create_uuid_generator, content_hasher, Default
from typing import Annotated
kernel = AppKernelEngine('demo app')
class User(Model, MongoRepository):
id: Annotated[str | None, Generator(create_uuid_generator('U'))] = None
name: Annotated[str | None, Required()] = None
email: Annotated[str | None, Validators(Email)] = None
password: Annotated[str | None, Required(), Converter(content_hasher())] = None
roles: Annotated[list[str] | None, Default(['Login'])] = None
kernel.register(User, methods=['GET', 'POST', 'PUT', 'DELETE'])
if __name__ == '__main__':
kernel.run()
Expected output:
INFO: Started server process
INFO: Uvicorn running on http://0.0.0.0:5000 (Press CTRL+C to quit)
The User model is now available at http://localhost:5000/users/.
List all users:
curl -X GET http://127.0.0.1:5000/users/
{
"_items": [
{
"_type": "User",
"email": "user@acme.com",
"id": "U9c6785f5-b8b1-4801-a09c-a45109af1222",
"name": "some name",
"roles": ["Login"]
}
],
"_links": {
"self": {"href": "/users/"}
}
}
Search by a field value (the ~ prefix means “contains”):
curl -X GET "http://127.0.0.1:5000/users/?name=~some"
Retrieve the JSON schema:
curl -X GET http://127.0.0.1:5000/users/schema
Retrieve the UI metadata:
curl -X GET http://127.0.0.1:5000/users/meta
Try to delete without enabling the DELETE method (the default only enables GET):
curl -X DELETE "http://127.0.0.1:5000/users/U9c6785f5-b8b1-4801-a09c-a45109af1222"
{
"_type": "ErrorMessage",
"code": 405,
"message": "MethodNotAllowed/The method is not allowed for the requested URL."
}
Register with the full set of methods to enable all operations:
kernel.register(User, methods=['GET', 'PUT', 'POST', 'PATCH', 'DELETE'])
Now delete works:
curl -X DELETE "http://127.0.0.1:5000/users/U9c6785f5-b8b1-4801-a09c-a45109af1222"
{
"_type": "OperationResult",
"result": 1
}
Service Hooks¶
Register lifecycle hooks by implementing before_<method> or after_<method> class methods:
from appkernel import Model, MongoRepository, AppKernelEngine, action
from appkernel.http_client import HttpClientServiceProxy
class Order(Model, MongoRepository):
id: Annotated[str | None, Generator(create_uuid_generator('O'))] = None
products: Annotated[list | None, Required()] = None
order_date: Annotated[datetime | None, Required(), Generator(date_now_generator)] = None
@classmethod
def before_post(cls, *args, **kwargs):
order = kwargs['model']
client = HttpClientServiceProxy('http://127.0.0.1:5000/')
status_code, rsp_dict = client.reservation.post(
Reservation(order_id=order.id, products=order.products))
order.update(reservation_id=rsp_dict.get('result'))
if __name__ == '__main__':
kernel = AppKernelEngine('Order Service', development=True)
kernel.register(Order, methods=['GET', 'POST', 'DELETE'])
kernel.run()
Now that you have a taste of AppKernel, explore the full feature set in the rest of this documentation — or just start building something.