Why use Plateforme?
Plateforme lets you build powerful data-driven applications and services with minimal effort. Its resource-centric architecture features a natural, intuitive syntax that prioritizes developer experience, enabling extremely fast prototyping while maintaining extensive customization options, high performance and scalability.
Unlike traditional frameworks that require piecing together multiple components, Plateforme unifies your data models and schemas, API endpoints, and business logic into simple, declarative resources and services. This opinionated approach eliminates boilerplate code and creates predictable patterns, making it also an ideal foundation for AI-assisted development where complex applications can be generated from minimal, precise specifications.
The documentation is under heavy development!
The documentation is actively being developed and might not cover every feature. For full details, refer to the source code.
In actions
A minimal example
Let's explore a practical implementation of a rocket factory system that demonstrates Plateforme's resource-centric architecture and its ability to handle complex relationships with minimal code:
from plateforme import CRUDResource, Field
class Material(CRUDResource): # (1)!
name: str
class Rocket(CRUDResource):
name: str
parts: list[Material] = Field(default_factory=list) # (2)!
- The
Material
resource inherits fromCRUDResource
, automatically providing a complete set of create, read, update, upsert, and delete operations without additional implementation. - The
parts
field usesdefault_factory=list
to ensure each rocket instance receives its own unique list of materials, preventing shared state between different instances. This is a crucial Python best practice for mutable default values.
Understanding the difference between Base
and CRUD
resources
Plateforme uses a service-oriented architecture where resources can subscribe to different services that define their behavior. The BaseResource
class provides core resource functionality, while the CRUDResource
extends it by subscribing to the built-in CRUDService
. This service implements standard create, read, update, upsert, and delete operations, making it immediately ready for use.
The following examples showcase how to use CRUDResource
classes effectively. The CRUDService
can be configured to provide additional operations or restrict access to default ones, serving as both a complete solution for common use cases and a reference implementation for custom services.
Beyond the built-in functionality, Plateforme enables you to define custom services for your resources, allowing you to encapsulate specific business logic or behavior.
Schema
This straightforward definition generates a database schema that automatically handles the identification strategy and polymorphism used for class inheritance through the id
and type
fields. Primary key can be configured to use either integers or UUIDs, where the value can be either generated automatically or provided by the user depending on the specified resource configuration. The generated schema also captures the many-to-many relationship between rockets and materials:
erDiagram
material {
int id PK
string type
string name
}
rocket {
int id PK
string type
string name
}
rocket_material {
int rocket_id PK, FK
int material_id PK, FK
}
material ||--o{ rocket_material : "used in"
rocket ||--o{ rocket_material : "composed of"
Endpoints
For resources that subscribe to the CRUDService
, it will generate a set of RESTful CRUD endpoints that allow you to interact with the data:
-
GET Read operations
-
/materials
→ Retrieve multiple materials -
/materials/{_material_key}
→ Retrieve a specific material -
/rockets
→ Retrieve multiple rockets -
/rockets/{_rocket_key}
→ Retrieve a specific rocket
-
-
POST Create operations
-
/materials
→ Create new materials -
/rockets
→ Create new rockets
-
-
PATCH Update operations
-
/materials
→ Update multiple materials -
/materials/{_material_key}
→ Update a specific material -
/rockets
→ Update multiple rockets -
/rockets/{_rocket_key}
→ Update a specific rocket
-
-
PUT Upsert operations
-
/materials
→ Create or update materials -
/rockets
→ Create or update rockets
-
-
DELETE Delete operations
-
/materials
→ Delete multiple materials -
/materials/{_material_key}
→ Delete a specific material -
/rockets
→ Delete multiple rockets -
/rockets/{_rocket_key}
→ Delete a specific rocket
-
A better example
To enhance our model with additional relationship attributes, such as tracking the quantity of materials used in each rocket, we can introduce a RocketPart
resource:
from plateforme import ConfigDict, CRUDResource, Field
class Material(CRUDResource):
code: str = Field(unique=True)
name: str
rocket_parts: list['RocketPart'] = Field(default_factory=list) # (1)!
class Rocket(CRUDResource):
code: str = Field(unique=True)
name: str
parts: list['RocketPart'] = Field(default_factory=list) # (2)!
class RocketPart(CRUDResource):
__config__ = ConfigDict(indexes=[{'rocket', 'material'}]) # (3)!
rocket: Rocket # (4)!
material: Material
quantity: int
- The
rocket_parts
field establishes a reverse relationship toRocketPart
, enabling bidirectional navigation between materials and their usage in rockets. - The
parts
field now referencesRocketPart
instead ofMaterial
directly, allowing for additional attributes on the relationship. - A composite index ensures uniqueness of material-rocket combinations, preventing duplicate entries.
- Forward references using string literals establish proper relationships while avoiding circular imports.
Schema
This enhanced model generates an expanded database schema with additional fields to capture the relationship between rockets, materials, and their quantities:
erDiagram
material {
int id PK
string type
string code UK
string name
}
rocket {
int id PK
string type
string code UK
string name
}
rocket_part {
int id PK
string type
int rocket_id UK, FK
int material_id UK, FK
float quantity
}
material ||--o{ rocket_part : "used in"
rocket ||--o{ rocket_part : "composed of"
Endpoints
This will generate additional endpoints to manage the new relationships. The framework can automatically infer nested endpoint trees based on the resource relationships, the depth of which can be configured at both the application, package, and resource levels:
-
GET Added read operations
- Extends previous endpoints with...
-
/materials/{_material_key}/rocket-parts
→ List all uses of a material -
/rockets/{_rocket_key}/parts
→ List all parts of a rocket -
/rocket-parts/{_rocket_part_key}/material
→ Get used material -
/rocket-parts/{_rocket_part_key}/rocket
→ Get parent rocket
-
POST Added create operations
- Extends previous endpoints with...
-
/materials/{_material_key}/rocket-parts
→ Create new material usage -
/rockets/{_rocket_key}/parts
→ Add parts to a rocket
-
PATCH Added update operations
- Extends previous endpoints with...
-
/materials/{_material_key}/rocket-parts
→ Update material usages -
/rockets/{_rocket_key}/parts
→ Update multiple parts -
/rocket-parts/{_rocket_part_key}/material
→ Update part's material -
/rocket-parts/{_rocket_part_key}/rocket
→ Update part's rocket
-
PUT Added upsert operations
- Extends previous endpoints with...
-
/materials/{_material_key}/rocket-parts
→ Create or update material usage -
/rockets/{_rocket_key}/parts
→ Create or update rocket parts
-
DELETE Added delete operations
- Extends previous endpoints with...
-
/materials/{_material_key}/rocket-parts
→ Remove material usages -
/rockets/{_rocket_key}/parts
→ Remove multiple parts -
/rocket-parts/{_rocket_part_key}/material
→ Remove material from part -
/rocket-parts/{_rocket_part_key}/rocket
→ Remove part from rocket
Playing with the API
Materials
Add some materials
Let's start by creating some materials. Each CRUD method exposes a set of arguments that can be used for instance to filter, sort, and paginate the results. The POST
endpoint bellow expects a JSON payload with the material's name and code. This will create 16 materials in total:
POST http://127.0.0.1:8001/materials HTTP/1.1
content-type: application/json
{
"payload": [
{
"name": "Main Engine",
"code": "ME-1000"
},
{
"name": "Primary Fuel Tank",
"code": "FT-500"
},
...
]
}
Request
POST http://127.0.0.1:8001/materials HTTP/1.1
content-type: application/json
{
"payload": [
{
"name": "Main Engine",
"code": "ME-1000"
},
{
"name": "Primary Fuel Tank",
"code": "FT-500"
},
{
"name": "Secondary Fuel Tank",
"code": "FT-501"
},
{
"name": "Guidance System",
"code": "GS-200"
},
{
"name": "Primary Payload Bay",
"code": "PB-300"
},
{
"name": "Secondary Payload Bay",
"code": "PB-301"
},
{
"name": "Nose Cone",
"code": "NC-150"
},
{
"name": "Thrust Vector Control",
"code": "TV-400"
},
{
"name": "Navigation Computer",
"code": "NC-600"
},
{
"name": "Communication System",
"code": "CS-700"
},
{
"name": "Thermal Protection System",
"code": "TP-800"
},
{
"name": "Landing Gear",
"code": "LG-250"
},
{
"name": "Power Distribution Unit",
"code": "PD-900"
},
{
"name": "Life Support System",
"code": "LS-350"
},
{
"name": "Emergency Escape System",
"code": "EE-450"
},
{
"name": "Oxidizer Tank",
"code": "OT-550"
}
]
}
Response
[
{
"id": 1,
"type": "material",
"code": "ME-1000",
"name": "Main Engine",
"rocket_parts": []
},
{
"id": 2,
"type": "material",
"code": "FT-500",
"name": "Primary Fuel Tank",
"rocket_parts": []
},
{
"id": 3,
"type": "material",
"code": "FT-501",
"name": "Secondary Fuel Tank",
"rocket_parts": []
},
{
"id": 4,
"type": "material",
"code": "GS-200",
"name": "Guidance System",
"rocket_parts": []
},
{
"id": 5,
"type": "material",
"code": "PB-300",
"name": "Primary Payload Bay",
"rocket_parts": []
},
{
"id": 6,
"type": "material",
"code": "PB-301",
"name": "Secondary Payload Bay",
"rocket_parts": []
},
{
"id": 7,
"type": "material",
"code": "NC-150",
"name": "Nose Cone",
"rocket_parts": []
},
{
"id": 8,
"type": "material",
"code": "TV-400",
"name": "Thrust Vector Control",
"rocket_parts": []
},
{
"id": 9,
"type": "material",
"code": "NC-600",
"name": "Navigation Computer",
"rocket_parts": []
},
{
"id": 10,
"type": "material",
"code": "CS-700",
"name": "Communication System",
"rocket_parts": []
},
{
"id": 11,
"type": "material",
"code": "TP-800",
"name": "Thermal Protection System",
"rocket_parts": []
},
{
"id": 12,
"type": "material",
"code": "LG-250",
"name": "Landing Gear",
"rocket_parts": []
},
{
"id": 13,
"type": "material",
"code": "PD-900",
"name": "Power Distribution Unit",
"rocket_parts": []
},
{
"id": 14,
"type": "material",
"code": "LS-350",
"name": "Life Support System",
"rocket_parts": []
},
{
"id": 15,
"type": "material",
"code": "EE-450",
"name": "Emergency Escape System",
"rocket_parts": []
},
{
"id": 16,
"type": "material",
"code": "OT-550",
"name": "Oxidizer Tank",
"rocket_parts": []
}
]
Fetch materials
We can now retrieve the list of materials we just created and use built-in filtering and sorting capabilities to narrow down the results. The following request fetches the first 5 materials sorted by code
in descending order:
GET http://127.0.0.1:8001/materials?limit=5&sort=-code HTTP/1.1
Response
[
{
"id": 8,
"type": "material",
"code": "TV-400",
"name": "Thrust Vector Control",
"rocket_parts": []
},
{
"id": 11,
"type": "material",
"code": "TP-800",
"name": "Thermal Protection System",
"rocket_parts": []
},
{
"id": 13,
"type": "material",
"code": "PD-900",
"name": "Power Distribution Unit",
"rocket_parts": []
},
{
"id": 6,
"type": "material",
"code": "PB-301",
"name": "Secondary Payload Bay",
"rocket_parts": []
},
{
"id": 5,
"type": "material",
"code": "PB-300",
"name": "Primary Payload Bay",
"rocket_parts": []
}
]
Filter materials
We can also use the built-in query capabilities to fetch materials that match specific conditions. Multiple filters can be combined to create complex queries, each condition can be applied to specific fields, nested or not, using the dot .
notation. For instance, the following request fetches materials whose code
contains the string NC
and have an id
greater than 2:
GET http://127.0.0.1:8001/materials?.code=like~*NC*&.id=gt~2 HTTP/1.1
Rockets
Add some rockets
Now that we have created some materials, let's create a few rockets. We will start by creating simple rockets without any material parts associated:
POST http://127.0.0.1:8001/rockets HTTP/1.1
content-type: application/json
{
"payload": [
{
"name": "Saturn V",
"code": "SAT-V"
},
{
"name": "Falcon 9",
"code": "FAL-9"
},
{
"name": "Space Launch System",
"code": "SLS-1"
}
]
}
We can also create rockets and associate them with materials at the same time, where either a reference to an existing material or a new material can be provided. The following request creates two rockets, one associated with existing materials and another with new materials:
POST http://127.0.0.1:8001/rockets HTTP/1.1
content-type: application/json
{
"payload": [
{
"name": "Atlas V",
"code": "ATL-6",
"parts": [
{
"material": 3,
"quantity": 1
},
...
]
},
{
"name": "Ariane 6",
"code": "ARI-6",
"parts": [
{
"material": {
"name": "Attitude Control System",
"code": "AC-650"
},
"quantity": 2
},
...
]
}
]
}
Request
POST http://127.0.0.1:8001/rockets HTTP/1.1
content-type: application/json
{
"payload": [
{
"name": "Atlas V",
"code": "ATL-6",
"parts": [
{
"material": 3,
"quantity": 1
},
{
"material": 4,
"quantity": 1
},
{
"material": 7,
"quantity": 2
},
{
"material": 9,
"quantity": 5
},
{
"material": 10,
"quantity": 4
},
{
"material": 11,
"quantity": 1
}
]
},
{
"name": "Ariane 6",
"code": "ARI-6",
"parts": [
{
"material": {
"name": "Attitude Control System",
"code": "AC-650"
},
"quantity": 2
},
{
"material": {
"name": "Telemetry System",
"code": "TS-750"
},
"quantity": 1
},
{
"material": {
"name": "Staging Mechanism",
"code": "SM-850"
},
"quantity": 3
},
{
"material": {
"name": "Propellant Feed System",
"code": "PF-950"
},
"quantity": 3
}
]
}
]
}
Response
[
{
"id": 4,
"type": "rocket",
"code": "ATL-6",
"name": "Atlas V",
"parts": [
{
"id": 1,
"type": "rocket_part",
"material": {
"id": 3,
"type": "material",
"code": "FT-501",
"name": "Secondary Fuel Tank"
},
"quantity": 1
},
{
"id": 2,
"type": "rocket_part",
"material": {
"id": 4,
"type": "material",
"code": "GS-200",
"name": "Guidance System"
},
"quantity": 1
},
{
"id": 3,
"type": "rocket_part",
"material": {
"id": 7,
"type": "material",
"code": "NC-150",
"name": "Nose Cone"
},
"quantity": 2
},
{
"id": 4,
"type": "rocket_part",
"material": {
"id": 9,
"type": "material",
"code": "NC-600",
"name": "Navigation Computer"
},
"quantity": 5
},
{
"id": 5,
"type": "rocket_part",
"material": {
"id": 10,
"type": "material",
"code": "CS-700",
"name": "Communication System"
},
"quantity": 4
},
{
"id": 6,
"type": "rocket_part",
"material": {
"id": 11,
"type": "material",
"code": "TP-800",
"name": "Thermal Protection System"
},
"quantity": 1
}
]
},
{
"id": 5,
"type": "rocket",
"code": "ARI-6",
"name": "Ariane 6",
"parts": [
{
"id": 7,
"type": "rocket_part",
"material": {
"id": 17,
"type": "material",
"code": "AC-650",
"name": "Attitude Control System"
},
"quantity": 2
},
{
"id": 8,
"type": "rocket_part",
"material": {
"id": 18,
"type": "material",
"code": "TS-750",
"name": "Telemetry System"
},
"quantity": 1
},
{
"id": 9,
"type": "rocket_part",
"material": {
"id": 19,
"type": "material",
"code": "SM-850",
"name": "Staging Mechanism"
},
"quantity": 3
},
{
"id": 10,
"type": "rocket_part",
"material": {
"id": 20,
"type": "material",
"code": "PF-950",
"name": "Propellant Feed System"
},
"quantity": 3
}
]
}
]
Automatic handling of recursive relationships
Plateforme automatically handles recursive relationships between resources. As you can see in the example above, we created rockets with associated materials, which in turn have a reference back to the rockets they are used in.
Fetch rockets
So far, we have created 5 rockets:
Response
[
{
"id": 1,
"type": "rocket",
"code": "SAT-V",
"name": "Saturn V",
"parts": []
},
{
"id": 2,
"type": "rocket",
"code": "FAL-9",
"name": "Falcon 9",
"parts": []
},
{
"id": 3,
"type": "rocket",
"code": "SLS-1",
"name": "Space Launch System",
"parts": []
},
{
"id": 4,
"type": "rocket",
"code": "ATL-6",
"name": "Atlas V",
"parts": [
{
"id": 1,
"type": "rocket_part",
"material": {
"id": 3,
"type": "material",
"code": "FT-501",
"name": "Secondary Fuel Tank"
},
"quantity": 1
},
{
"id": 2,
"type": "rocket_part",
"material": {
"id": 4,
"type": "material",
"code": "GS-200",
"name": "Guidance System"
},
"quantity": 1
},
{
"id": 6,
"type": "rocket_part",
"material": {
"id": 11,
"type": "material",
"code": "TP-800",
"name": "Thermal Protection System"
},
"quantity": 1
},
{
"id": 3,
"type": "rocket_part",
"material": {
"id": 7,
"type": "material",
"code": "NC-150",
"name": "Nose Cone"
},
"quantity": 2
},
{
"id": 5,
"type": "rocket_part",
"material": {
"id": 10,
"type": "material",
"code": "CS-700",
"name": "Communication System"
},
"quantity": 4
},
{
"id": 4,
"type": "rocket_part",
"material": {
"id": 9,
"type": "material",
"code": "NC-600",
"name": "Navigation Computer"
},
"quantity": 5
}
]
},
{
"id": 5,
"type": "rocket",
"code": "ARI-6",
"name": "Ariane 6",
"parts": [
{
"id": 8,
"type": "rocket_part",
"material": {
"id": 18,
"type": "material",
"code": "TS-750",
"name": "Telemetry System"
},
"quantity": 1
},
{
"id": 7,
"type": "rocket_part",
"material": {
"id": 17,
"type": "material",
"code": "AC-650",
"name": "Attitude Control System"
},
"quantity": 2
},
{
"id": 9,
"type": "rocket_part",
"material": {
"id": 19,
"type": "material",
"code": "SM-850",
"name": "Staging Mechanism"
},
"quantity": 3
},
{
"id": 10,
"type": "rocket_part",
"material": {
"id": 20,
"type": "material",
"code": "PF-950",
"name": "Propellant Feed System"
},
"quantity": 3
}
]
}
]
Update rocket parts
To update the list of materials for a specific rocket, we can use the framework automatically generated endpoints. The following request updates the list of materials for the rocket with the ID 1
using the PATCH
method:
PATCH http://127.0.0.1:8001/rockets?.code=like~*ARI* HTTP/1.1
content-type: application/json
{
"payload": {
"parts": [
{
"material": 1,
"quantity": 2
},
{
"material": 4,
"quantity": 1
}
]
}
}
Response
{
"id": 1,
"type": "rocket",
"code": "SAT-V",
"name": "Saturn IB",
"parts": [
{
"id": 11,
"type": "rocket_part",
"material": {
"id": 1,
"type": "material",
"code": "ME-1000",
"name": "Main Engine"
},
"quantity": 2
},
{
"id": 12,
"type": "rocket_part",
"material": {
"id": 4,
"type": "material",
"code": "GS-200",
"name": "Guidance System"
},
"quantity": 1
}
]
}
Fetch rocket parts
The nested endpoints let us query directly the materials associated with a specific rocket. The following request fetches the materials for the rocket with the ID 1
:
Filter rocket parts
Finally, we can demonstrate the powerfull built-in query capabilities of Plateforme that gives for our RESTful API a GraphQL-like querying for related resources. The following request fetches parts from rocket with id=4
where material codes start with NC
, returning only part IDs, quantities, and material details (code and name):
GET http://127.0.0.1:8001/rockets/4/parts?.material.code=like~NC* HTTP/1.1
content-type: application/json
{
"include": {
"__all__": {
"id": true,
"material": {
"code": true,
"name": true
},
"quantity": true
}
}
}
Everything is customizable
Plateforme is designed to be highly customizable, allowing you to tailor the framework to your specific needs. You can extend the built-in functionality by creating custom services, defining additional behaviors, or modifying the default configurations to suit your application requirements.
The built-in route
context aware decorator function can be used to define custom routes for your resources. The decorator can be also applied to class or static methods, allowing you to create custom endpoints that perform specific actions at the resource level.
The following example demonstrates how to create a custom route for the Material
resource instance as well as a custom route for the Material
resource class:
from plateforme import BaseResource, Field
from plateforme.api import AsyncSessionDep, route
from plateforme.database import func, select
class Material(BaseResource):
code: str = Field(unique=True)
name: str
@route.post() # (1)!
async def update_name(self, name: str) -> str:
self.name = name
return f'Material name updated to {name!r}.'
@route.get() # (2)!
@classmethod
async def count(cls, session: AsyncSessionDep) -> int:
query = select(func.count()).select_from(cls)
result = await session.execute(query)
return result.scalar()
- The
update_name
method is a custom route defined at the resource instance level. It updates thename
attribute of theMaterial
instance and returns a message confirming the update. It can be accessed at the/materials/{_material_key}/update-name
endpoint. - The
count
method is a custom route defined at the resource class level. It queries the database to count the number ofMaterial
instances and returns the result. It can be accessed at the/materials/count
endpoint.
Use services for reusable logic
While custom routes are useful for defining specific actions at the resource level, services are better suited for encapsulating reusable logic that can be shared across multiple resources. Services can be used to define custom business logic, data validation, or authorization checks, providing a centralized location for common functionality.
The built-in services, such as the CRUDService
, can be extended to add additional operations or modify existing ones. You can also create custom services that subscribe to your resources to define specific behaviors or implement complex business logic.
More to come
This is just a basic overview of what Plateforme can do. The documentation will be expanded to cover more advanced features and use cases, so stay tuned for updates!