27
loading...
This website collects cookies to deliver better user experience
$schema: "https://json-schema.org/draft/2020-12/schema"
properties:
lendings:
additionalProperties:
type: object
properties:
id: { type: string }
user_id: { type: string, format: uuid }
book_item_id: { type: string }
required: [id, user_email, book_item_id]
propertyNames: { type: string, format: uuid }
required: [lendings]
$schema: "https://json-schema.org/draft/2020-12/schema"
properties:
members_by_id:
type: object
additionalProperties:
type: object
properties:
is_blocked: { type: boolean }
required: [is_blocked]
propertyNames: { type: string, format: uuid }
required: [members_by_id]
library_data = {
"catalog": {
"books_by_isbn": {
"9781234567897": {
"title": "Data Oriented Programming",
"author": "Yehonathan Sharvit",
}
},
"book_items_by_id": {
"book-item-1": {
"isbn": "9781617298578",
},
"book-item-2": {
"isbn": "9781617298578",
}
},
"lendings": [
{
"id": "...",
"user_id": "member-1",
"book_item_id": "book-item-1",
}
],
},
"user_management": {
"members_by_id": {
"member-1": {
"id": "member-1",
"name": "Xavier B.",
"email": "[email protected]",
"password": "aG93IGRhcmUgeW91IQ==",
"is_blocked": False,
}
}
},
}
dict
are used like some Mapping
, and I forbid myself to update them.classes+static
method form in order to make this article readable. In a production code, the modules+functions
form is the way to go.from __future__ import annotations
from typing import Tuple, TypeVar
from uuid import uuid4
T = TypeVar("T")
class Library:
@staticmethod
def checkout(library_data: T, user_id, book_item_id) -> tuple[T, dict]:
user_management_data = library_data["user_management"]
if not UserManagement.is_member(user_management_data, user_id):
raise Exception("Only members can borrow books")
if UserManagement.is_blocked(user_management_data, user_id):
raise Exception("Member cannot borrow book because he is bloqued")
catalog_data = library_data["catalog"]
if not Catalog.is_available(catalog_data, book_item_id):
raise Exception("Book is already borrowed")
catalog_data, lending = Catalog.checkout(catalog_data, book_item_id, user_id)
return (
library_data | {
"catalog": catalog_data,
},
lending,
)
class UserManagement:
@staticmethod
def is_member(user_management_data: T, user_id) -> bool:
return user_id in user_management_data["members_by_id"]
@staticmethod
def is_blocked(user_management_data: T, user_id) -> bool:
return user_management_data["members_by_id"][user_id]["is_blocked"] is True
class Catalog:
@staticmethod
def is_available(catalog_data: T, book_item_id) -> bool:
lendings = catalog_data["lendings"]
return all(lending["book_item_id"] != book_item_id for lending in lendings)
@staticmethod
def checkout(catalog_data: T, book_item_id, user_id) -> Tuple[T, dict]:
lending_id = uuid4().__str__()
lending = {"id": lending_id, "user_id": user_id, "book_item_id": book_item_id}
lendings = catalog_data["lendings"]
return (
catalog_data | {
"lendings": lendings + [lending]
},
lending
)
library_data, lending = Library.checkout(
library_data,
user_id="member-1",
book_item_id="book-item-2",
)
Data is systematically transmitted to every function calls. This object is quite opaque, each level use only a fragment that he knows without worrying about the remaining:
# 1. injects data into Library.checkout module
library_data, lending = Library.checkout(library_data, ...)
# 2. extracts data from user_management
user_management_data = library_data["user_management"]
# 3. uses this data fragment into UserManagement module
if not UserManagement.is_member(user_management_data, ...):
...
if UserManagement.is_blocked(user_management_data, ...):
...
# 4. picks catalog data
catalog_data = library_data["catalog"]
# 5. uses this data fragment into Catalog module
if not Catalog.is_available(catalog_data, ...):
...
... = Catalog.checkout(catalog_data, ...)
When a function is about to change a state, it returns a new version of data. Every level of the call stack must returns a new version of data:
# 1. handles the request in Catalog.checkout
lending = ...
lendings = catalog_data["lendings"]
# 2. creates a new version of catalog_data
catalog_data = catalog_data | {
"lendings": lendings + [lending]
}
# 3. interception of the new catalog_data by Library.checkout
catalog_data, ... = Catalog.checkout(...)
# 4. creation of a new version of library_data
library_data = library_data | {
"catalog": catalog_data,
}
Then, this new version of data can be exposed to whole system.
map()
, filter()
as well as functions from standard operator
module also contribute to make this paradigm quite natural in Python.from __future__ import annotations
from typing import Any, Mapping
class M(Mapping[str, Any]):
def __or__(self, other: dict) -> M:
return {**self, **other} # type: ignore
def __hash__(self) -> int:
...
# which can be used as well in the source code
def my_func(members_data: M[str, M], member_id: str):
member = members_data[member_id]
@app.post("/checkout")
def checkout_view():
...
try:
..., lending = Library.checkout(library_data, user_id, book_item_id)
# la fonction 'checkout' peut lever des exceptions
...
return jsonify(lending), 201
except Exception as error:
result = {"error": str(error)}
return jsonify(result), 400