Skip to content

Missions example

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.

Overview

The missions example demonstrates how to build a full-featured application using Plateforme's resource management capabilities. This project showcases a space mission management system with multiple interconnected packages handling different aspects of space operations.

Getting started

The missions example can be run using the Plateforme built-in CLI:

# Build the application and initialize the database
plateforme -p src/examples/missions build

# Start the application server
plateforme -p src/examples/missions start

The application will be available at http://127.0.0.1:8001.

Project structure

The project implements a modular architecture through three packages: assets, ops, and staff. Each package manages a specific domain of space missions, such as physical resources, mission operations, and personnel.

missions/
├── client/           # API request examples
├── packages/
   ├── assets/       # Physical resource management
   ├── ops/          # Mission operations
   └── staff/        # Personnel management
├── server/
   ├── main.py       # Application entry point
   └── setup.py      # Database initialization
└── pyproject.toml    # Project configuration

Project configuration

The project is configured through pyproject.toml, which defines both the project metadata and application settings:

[project]
name = "missions"
version = "0.1.0"
description = "The missions example project"
keywords = ["plateforme", "missions", "example"]

[tool.plateforme.apps.default]
scripts = {setup = "python server/setup.py"}
build = ["setup"]
start = "server.main:app --port 8001"

The configuration consists of two main sections. The project section defines basic metadata including the package name, version, and keywords. This information helps identify and categorize the project within a larger ecosystem.

The plateforme.apps.default section configures the application's build and runtime behavior. It specifies that the build process should run the setup script, which initializes the database with sample data. The start configuration indicates that the application should run on port 8001 using the main.py entry point.

Application configuration

The application uses Plateforme's main entry point to configure and connect all components:

from plateforme import Plateforme

# Create application
app = Plateforme(
    debug=True,  # (1)!
    logging={'level': 'DEBUG'},  # (2)!
    database_engines='plateforme.db',  # (3)!
    namespaces=[  # (4)!
        ('assets', {'alias': ''}),
        ('staff', {'alias': ''}),
    ],
    packages=[  # (5)!
        'packages.assets',
        'packages.ops',
        'packages.staff',
    ],
)
  1. Enable debug mode for development.
  2. Configure logging to output debug messages. Advanced configuration can be provided to customize logging behavior.
  3. Define the database connection as a simple SQLite engine. Advanced configuration can be provided to specify multiple database engines and custom connection settings.
  4. Configure namespaces for the packages. Setting an empty alias allows accessing resources endpoints directly without a prefix.
  5. Register the packages to include within the application.

This configuration enables debug mode for development, sets up logging, and configures the database connection. It also registers the required packages and their namespaces, establishing the foundation for the modular architecture described below.

Application build

The application build process is configured within the pyproject.toml file to run the setup.py script. The script initializes the database with sample data:

...
# Create database
app.metadata.create_all()

# Create fake data
fake = Faker()

# Create fake materials
materials = [
    Material(
        code=str(random.randint(1, 9999999999)),
        name=fake.name(),
        description=fake.sentence(),
        price=random.uniform(100000, 100000000),
    )
    for _ in range(random.randint(1, 100))
]
...
Source code in src/examples/missions/server/setup.py
import logging
import random

from faker import Faker

from server.main import app
from packages.assets import Material, Rocket, RocketPart
from packages.ops import Mission, Station
from packages.staff import Astronaut, Crew

logging.getLogger('faker').setLevel(logging.ERROR)

# Create database
app.metadata.create_all()

# Create fake data
fake = Faker()

# Create fake materials
materials = [
    Material(
        code=str(random.randint(1, 9999999999)),
        name=fake.name(),
        description=fake.sentence(),
        price=random.uniform(100000, 100000000),
    )
    for _ in range(random.randint(1, 100))
]

# Create fake rockets
rockets = [
    Rocket(
        code=str(random.randint(1, 9999999999)),
        name=fake.name(),
        description=fake.sentence(),
        parts=[
            RocketPart(
                code=f"{(count * 10):04d}",
                material=material,
                quantity=random.randint(1, 10),
            )
            for count, material in enumerate(
                random.sample(materials, random.randint(1, 10))
            )
        ],
    )
    for _ in range(random.randint(1, 100))
]

# Create fake astronauts
astronauts = [
    Astronaut(
        code=str(random.randint(1, 9999999999)),
        name=fake.name(),
        role=random.choice([
            'doctor', 'engineer', 'pilot', 'technician'
        ]),
    )
    for _ in range(random.randint(1, 100))
]

# Create fake crews
crews = [
    Crew(
        code=str(random.randint(1, 9999999999)),
        name=fake.word().capitalize(),
        astronauts=random.sample(astronauts, random.randint(2, 10)),
    )
    for _ in range(random.randint(1, 10))
]
# Assign lead to crews
for crew in crews:
    crew.lead = random.choice(crew.astronauts)

# Create fake stations
stations = [
    Station(
        code=str(random.randint(1, 9999999999)),
        name=fake.city(),
        description=fake.sentence(),
        coordinates=str(fake.local_latlng(coords_only=True)),
    )
    for _ in range(random.randint(1, 100))
]

# Create fake missions
missions = [
    Mission(
        code=str(random.randint(1, 9999999999)),
        name=fake.word().capitalize(),
        description=fake.sentence(),
        crew=random.choice(crews),
        rocket=random.choice(rockets),
        station=random.choice(stations),
        launch_date=fake.date_this_decade(),
    )
    for _ in range(random.randint(1, 100))
]

# Commit fake data
with app.session() as session:
    session.add_all([
        *astronauts,
        *crews,
        *materials,
        *rockets,
        *stations,
        *missions,
    ])
    session.commit()
    session.close()

# Generate summary
print('Mock data generated:')
print('- Astronauts:', len(astronauts))
print('- Crews:', len(crews))
print('- Materials:', len(materials))
print('- Rockets:', len(rockets))
print('- Stations:', len(stations))
print('- Missions:', len(missions))

Implementation

Each package is defined with a configuration file that includes the package's information and default namespace and API settings. For example, the assets package configuration is defined as follows:

[plateforme]
name = "assets"
version = "0.1.0"
description = "The missions example assets package"
keywords = ["plateforme", "example", "missions", "assets"]

[plateforme.package]
namespace = "assets"
api_services = false

Assets package

The assets package manages physical resources required for space missions. It defines two primary resources for managing materials and rockets, where materials are basic components used in rockets:

Define material resource
from plateforme import CRUDResource, Field

class Material(CRUDResource):
    code: str = Field(unique=True, max_length=10)
    name: str = Field(max_length=100)
    description: str | None = Field(default=None, max_length=1000)
    price: float = Field(gt=0)

Rockets are assembled from multiple parts linked to materials:

Define rocket resource
from plateforme import BaseResource, CRUDResource, ConfigDict, Field
# TODO: from plateforme.schema import computed_field

from .materials import Material

class Rocket(CRUDResource):
    code: str = Field(unique=True, max_length=10)
    name: str = Field(max_length=100)
    description: str | None = Field(default=None, max_length=1000)
    parts: list['RocketPart'] = Field(default_factory=list)

    # TODO: @computed_field
    # TODO: @property
    # TODO: def price(self) -> float:
    # TODO:     return sum(part.material.price * part.quantity for part in self.parts)

class RocketPart(BaseResource):
    __config__ = ConfigDict(
        indexes=[
            {'rocket', 'code'},
            {'rocket', 'material'}
        ],
    )
    code: str = Field(max_length=10)
    rocket: Rocket
    material: Material
    quantity: int

Operations package

The operations package handles mission planning and space station management. Stations provide mission destinations:

Define station resource
import typing
from plateforme import CRUDResource, Field

if typing.TYPE_CHECKING:
    from .missions import Mission

class Station(CRUDResource):
    code: str = Field(unique=True, max_length=10)
    name: str = Field(max_length=100)
    description: str | None = Field(default=None, max_length=1000)
    coordinates: str | None = Field(default=None)
    missions: list['Mission'] = Field(default_factory=list)

Missions coordinate crews, rockets, and stations:

Define mission resource
import datetime
from plateforme import CRUDResource, Field

from ..assets import Rocket
from ..staff import Crew
from .stations import Station

class Mission(CRUDResource):
    code: str = Field(unique=True, max_length=10)
    name: str = Field(max_length=100)
    description: str | None = Field(default=None, max_length=1000)
    crew: Crew
    rocket: Rocket
    station: Station
    launch_date: datetime.date = Field(default_factory=datetime.date.today)

Staff package

The staff package manages personnel and crew assignments. It defines astronaut profiles:

Define astronaut resource
import typing
from typing import Literal
from plateforme import CRUDResource, Field

if typing.TYPE_CHECKING:
    from .crews import Crew

class Astronaut(CRUDResource):
    code: str = Field(unique=True, max_length=10)
    name: str = Field(max_length=100)
    role: Literal['doctor', 'engineer', 'pilot', 'technician']
    crews: list['Crew'] = Field(default_factory=list)

And organizes them into crews:

Define crew resource
from typing import Self
from plateforme import CRUDResource, Key, Field, route

from .astronauts import Astronaut

class Crew(CRUDResource):
    code: str = Field(unique=True, max_length=10)
    name: str = Field(max_length=100)
    astronauts: list[Astronaut] = Field(
        default_factory=list,
        rel_load='selectin',
    )
    lead: Astronaut | None = Field(
        default=None,
        init=False,
        association_alias='crew_lead',
    )

    @route.post()
    async def assign_lead(self, astronaut: Key[Astronaut]) -> Self:
        lead = await astronaut.resolve()
        if lead not in self.astronauts:
            raise ValueError('The lead must be part of the crew')
        self.lead = lead
        return self

Playground

Managing rockets

The system provides comprehensive rocket management capabilities.

Add some rockets

Create some rockets with dry run
POST http://127.0.0.1:8001/assets/rockets HTTP/1.1
content-type: application/json

{
  "payload": [
    { "code": "SAT-V", "name": "Saturn V" },
    { "code": "ARI-5", "name": "Ariane 5" }
  ],
  "dry_run": true
}
Response
[
  {
    "id": null,
    "type": "rocket",
    "code": "SAT-V",
    "name": "Saturn V",
    "description": null,
    "parts": []
  },
  {
    "id": null,
    "type": "rocket",
    "code": "ARI-5",
    "name": "Ariane 5",
    "description": null,
    "parts": []
  }
]

Fetch rockets

Fetch rockets '3' and '4' with selected fields
GET http://127.0.0.1:8001/assets/rockets?.id=in~3,4 HTTP/1.1
content-type: application/json

{
  "include": {
    "__all__": {
      "id": true,
      "code": true,
      "parts": {
        "__all__": {
          "id": true,
          "material": {
            "id": true,
            "code": true
          },
          "quantity": true
        }
      }
    }
  }
}
Response
[
  {
    "id": 3,
    "code": "9506131347",
    "parts": [
      {
        "id": 21,
        "material": {
          "id": 17,
          "code": "9627606784"
        },
        "quantity": 5
      },
      {
        "id": 20,
        "material": {
          "id": 23,
          "code": "9070371458"
        },
        "quantity": 9
      },
      {
        "id": 19,
        "material": {
          "id": 27,
          "code": "1926426509"
        },
        "quantity": 2
      },
      {
        "id": 15,
        "material": {
          "id": 30,
          "code": "9128315132"
        },
        "quantity": 1
      },
      {
        "id": 17,
        "material": {
          "id": 42,
          "code": "5479104904"
        },
        "quantity": 4
      },
      {
        "id": 16,
        "material": {
          "id": 49,
          "code": "9598485656"
        },
        "quantity": 7
      },
      {
        "id": 18,
        "material": {
          "id": 51,
          "code": "4039707946"
        },
        "quantity": 7
      }
    ]
  },
  {
    "id": 4,
    "code": "2734370508",
    "parts": [
      {
        "id": 22,
        "material": {
          "id": 6,
          "code": "2587569535"
        },
        "quantity": 4
      }
    ]
  }
]

Update rockets

Clear description field for all rockets whose code ends with '2'
PATCH http://127.0.0.1:8001/assets/rockets?.code=like~*2 HTTP/1.1
content-type: application/json

{
  "payload": {
    "description": null
  },
  "include": {
    "__all__": {
      "id": true,
      "code": true,
      "description": true
    }
  }
}
Response
[
  {
    "id": 10,
    "code": "4760719322",
    "description": null
  },
  {
    "id": 18,
    "code": "3142734512",
    "description": null
  },
  {
    "id": 19,
    "code": "7177675612",
    "description": null
  },
  {
    "id": 21,
    "code": "9112122",
    "description": null
  },
  {
    "id": 68,
    "code": "9010040722",
    "description": null
  },
  {
    "id": 79,
    "code": "8145379582",
    "description": null
  }
]

Upsert rockets

Create or update a new rocket if it does not exist
PUT http://127.0.0.1:8001/assets/rockets HTTP/1.1
content-type: application/json

{
  "payload": {
    "code": "ARI-6",
    "name": "Ariane 6",
    "description": "European launch vehicle."
  }
}
Response
[
  {
    "id": 85,
    "type": "rocket",
    "code": "ARI-6",
    "name": "Ariane 6",
    "description": "European launch vehicle.",
    "parts": []
  }
]

Delete rockets

Delete the rocket with id '85'
DELETE http://127.0.0.1:8001/assets/rockets/85 HTTP/1.1
content-type: application/json

{
  "return_result": true
}
Response
{
  "id": 85,
  "type": "rocket",
  "code": "ARI-6",
  "name": "Ariane 6",
  "description": "European launch vehicle.",
  "parts": []
}

Managing crews

The staff package enables comprehensive crew management.

Assign crew leads

Assign first crew its lead astronaut
POST http://127.0.0.1:8001/staff/crews/1/assign-lead HTTP/1.1
content-type: application/json

"2"
Response
{
  "id": 1,
  "type": "crew",
  "code": "881557580",
  "name": "Base",
  "astronauts": [
    {
      "id": 2,
      "type": "astronaut",
      "code": "8685543559",
      "name": "Brandon Rodriguez",
      "role": "engineer"
    },
    {
      "id": 4,
      "type": "astronaut",
      "code": "9889304965",
      "name": "Linda Fernandez",
      "role": "pilot"
    },
    {
      "id": 3,
      "type": "astronaut",
      "code": "5054931889",
      "name": "Jack Rogers",
      "role": "technician"
    }
  ],
  "lead": {
    "id": 2,
    "type": "astronaut",
    "code": "8685543559",
    "name": "Brandon Rodriguez",
    "role": "engineer"
  }
}

Filter crews

Filter crews by astronauts
GET http://127.0.0.1:8001/staff/crews?.astronauts*.id=ge~1;le~15&sort=+name HTTP/1.1
content-type: application/json

{
  "include": {
    "__all__": {
      "id": true,
      "code": true,
      "name": true,
      "lead": true
    }
  }
}
Response
[
  {
    "id": 1,
    "code": "881557580",
    "name": "Base",
    "lead": {
      "id": 2,
      "type": "astronaut",
      "code": "8685543559",
      "name": "Brandon Rodriguez",
      "role": "engineer"
    }
  },
  {
    "id": 2,
    "code": "5167989951",
    "name": "Serious",
    "lead": {
      "id": 7,
      "type": "astronaut",
      "code": "3717936433",
      "name": "Joshua Santos",
      "role": "technician"
    }
  }
]

Planning missions

The operations package facilitates mission planning and execution.

Query missions

Query mission information with pagination and sorting
GET http://127.0.0.1:8001/missions?sort=-id&page_size=10&page=2 HTTP/1.1
content-type: application/json

{
  "filter": {
    "id": "ge~1;le~30",
    "crew.lead.id": "in~1,2,3"
  },
  "include": {
    "__all__": {
      "id": true,
      "code": true,
      "name": true,
      "lead": true,
      "crew": {
        "id": true,
        "code": true,
        "lead": true
      },
      "rocket": {
        "id": true,
        "code": true
      },
      "station": {
        "code": true,
        "name": true,
        "coordinates": true
      },
      "launch_date": true
    }
  }
}
Response
[
  {
    "id": 6,
    "code": "2993145882",
    "name": "Central",
    "crew": {
      "id": 1,
      "code": "881557580",
      "lead": {
        "id": 2,
        "type": "astronaut",
        "code": "8685543559",
        "name": "Brandon Rodriguez",
        "role": "engineer"
      }
    },
    "rocket": {
      "id": 38,
      "code": "4545618943"
    },
    "station": {
      "code": "4183126061",
      "name": "Matthewburgh",
      "coordinates": "('33.7207', '-116.21677')"
    },
    "launch_date": "2022-11-27"
  },
  {
    "id": 5,
    "code": "2725533067",
    "name": "Cultural",
    "crew": {
      "id": 1,
      "code": "881557580",
      "lead": {
        "id": 2,
        "type": "astronaut",
        "code": "8685543559",
        "name": "Brandon Rodriguez",
        "role": "engineer"
      }
    },
    "rocket": {
      "id": 50,
      "code": "743920576"
    },
    "station": {
      "code": "4183126061",
      "name": "Matthewburgh",
      "coordinates": "('33.7207', '-116.21677')"
    },
    "launch_date": "2025-01-02"
  },
  {
    "id": 3,
    "code": "1971626508",
    "name": "Push",
    "crew": {
      "id": 1,
      "code": "881557580",
      "lead": {
        "id": 2,
        "type": "astronaut",
        "code": "8685543559",
        "name": "Brandon Rodriguez",
        "role": "engineer"
      }
    },
    "rocket": {
      "id": 20,
      "code": "1117447911"
    },
    "station": {
      "code": "917844342",
      "name": "Lake Jeff",
      "coordinates": "('33.76446', '-117.79394')"
    },
    "launch_date": "2024-04-23"
  },
  {
    "id": 2,
    "code": "7593017353",
    "name": "Professional",
    "crew": {
      "id": 1,
      "code": "881557580",
      "lead": {
        "id": 2,
        "type": "astronaut",
        "code": "8685543559",
        "name": "Brandon Rodriguez",
        "role": "engineer"
      }
    },
    "rocket": {
      "id": 16,
      "code": "4150234806"
    },
    "station": {
      "code": "917844342",
      "name": "Lake Jeff",
      "coordinates": "('33.76446', '-117.79394')"
    },
    "launch_date": "2024-10-14"
  }
]

Advanced features

Custom endpoints

The project demonstrates how to create custom endpoints for specific operations. For example, the crew resource includes a custom endpoint for assigning leads:

Custom crew endpoint
@route.post()
async def assign_lead(self, astronaut: Key[Astronaut]) -> Self:
    lead = await astronaut.resolve()
    if lead not in self.astronauts:
        raise ValueError('The lead must be part of the crew')
    self.lead = lead
    return self

This endpoint ensures that only crew members can be assigned as leads.

Resource relationships

The project showcases also various relationship types:

Relationship examples
class Rocket(CRUDResource):
    ...
    parts: list['RocketPart'] = Field(default_factory=list)  # (1)!

class Crew(CRUDResource):
    ...
    astronauts: list[Astronaut] = Field(  # (2)!
        default_factory=list,
        rel_load='selectin',
    )

class Crew(CRUDResource):
    ...
    lead: Astronaut | None = Field(  # (3)!
        default=None,
        init=False,
        association_alias='crew_lead',
    )
  1. The Rocket resource has a one-to-many relationship with RocketPart resources.
  2. The Crew resource has a many-to-many relationship with Astronaut resources.
  3. The Crew resource has a one-to-one relationship with an Astronaut resource representing the crew lead.