bluebird-api 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,77 @@
1
+ import logging
2
+ import os
3
+ import re
4
+ from collections.abc import Awaitable, Callable
5
+ from pathlib import Path
6
+
7
+ from bluebird_dt import logger
8
+ from fastapi import FastAPI, Request, Response
9
+ from fastapi.middleware.cors import CORSMiddleware
10
+ from fastapi.middleware.gzip import GZipMiddleware
11
+ from fastapi.routing import APIRoute
12
+ from fastapi.staticfiles import StaticFiles
13
+
14
+ from .route_tags import tags_metadata
15
+ from .routes import router
16
+
17
+ bluebird_logger = logging.getLogger("bluebird_dt")
18
+ bluebird_logger.setLevel(logging.DEBUG)
19
+ stream_handler = logging.StreamHandler()
20
+ stream_handler.setFormatter(logger.CustomFormatter())
21
+ bluebird_logger.addHandler(stream_handler)
22
+
23
+ app = FastAPI(
24
+ title="BluebirdATC: AI for air traffic control",
25
+ description="FastAPI interface to control the simulation framework BluebirdATC.",
26
+ license_info={
27
+ "name": "License",
28
+ "url": "https://github.com/project-bluebird/BluebirdATC/blob/dev/LICENSE",
29
+ },
30
+ openapi_tags=tags_metadata,
31
+ strict_content_type=False,
32
+ )
33
+ app.include_router(router)
34
+ app.add_middleware(
35
+ CORSMiddleware,
36
+ allow_origins=["*"],
37
+ allow_credentials=True,
38
+ allow_methods=["*"],
39
+ allow_headers=["*"],
40
+ )
41
+ app.add_middleware(GZipMiddleware, minimum_size=500)
42
+
43
+ # serve the frontend
44
+ repo_root = Path(__file__).resolve().parents[2]
45
+ dist_path = repo_root / "bluebird-hmi" / "dist"
46
+
47
+ if dist_path.is_dir():
48
+ app.mount("/hmi", StaticFiles(directory=str(dist_path), html=True), name="frontend")
49
+ app.mount("/hmi/assets", StaticFiles(directory=os.path.join(dist_path, "assets")), name="assets")
50
+
51
+
52
+ # For webapp, requests will be prepended with "/api" - strip that out here
53
+ @app.middleware("http")
54
+ async def strip_api_prefix(request: Request, call_next: Callable[[Request], Awaitable[Response]]) -> Response:
55
+ path = request.url.path
56
+ if path.startswith("/api"):
57
+ newpath = re.sub("/api", "", path)
58
+ request.scope["path"] = newpath
59
+ return await call_next(request)
60
+
61
+
62
+ def use_route_names_as_operation_ids(app: FastAPI) -> None:
63
+ """
64
+ Simplify operation IDs so that generated API clients have simpler function
65
+ names.
66
+
67
+ Must be called after all routes have been added.
68
+ """
69
+ for route in app.routes:
70
+ if isinstance(route, APIRoute):
71
+ route.operation_id = route.name
72
+
73
+
74
+ use_route_names_as_operation_ids(app)
75
+
76
+ # keep track of current simulation instance
77
+ # app.state.current_runner = None
bluebird_api/models.py ADDED
@@ -0,0 +1,39 @@
1
+ import typing
2
+
3
+ from bluebird_dt.utility.supported_actions import SUPPORTED_ACTIONS
4
+ from pydantic import BaseModel, Field
5
+
6
+ from bluebird_api.runnerabc import RunnerABC
7
+
8
+ SUPPORTED_ACTIONS_LIST = [action for actions in SUPPORTED_ACTIONS.values() for action in actions]
9
+
10
+
11
+ class RunnerStore:
12
+ current_runner: RunnerABC = None
13
+
14
+
15
+ class ActionInput(BaseModel):
16
+ """An action to be sent to the simulator."""
17
+
18
+ agent: str = Field(
19
+ description="The agent performing the action.",
20
+ json_schema_extra={"example": "atc_1"},
21
+ )
22
+ callsign: str = Field(
23
+ description="The callsign of the flight being acted on.",
24
+ json_schema_extra={"example": "AIR123"},
25
+ )
26
+ kind: typing.Literal[tuple(SUPPORTED_ACTIONS_LIST)] = Field(
27
+ description="The kind of action to perform. Must be a string that is one of the supported actions.",
28
+ json_schema_extra={"example": "change_heading_to"},
29
+ )
30
+ value: typing.Any = Field(
31
+ description=(
32
+ "The value associated with the action. The type of this value depends on the action being performed."
33
+ ),
34
+ json_schema_extra={"example": 90},
35
+ )
36
+ sector: str = Field(
37
+ description="The sector that the action is being performed in.",
38
+ json_schema_extra={"example": "sector_1"},
39
+ )
@@ -0,0 +1,23 @@
1
+ # FastAPI metadata tags for use in Swagger autodocs.
2
+ tags_metadata = [
3
+ {
4
+ "name": "Control",
5
+ "description": "High level control of BluebirdATC.",
6
+ },
7
+ {
8
+ "name": "Scenarios",
9
+ "description": "Routes to get information about scenarios.",
10
+ },
11
+ {
12
+ "name": "Evolve",
13
+ "description": "Routes to start, stop and evolve the simulation.",
14
+ },
15
+ {
16
+ "name": "State",
17
+ "description": "Routes to get the state of the simulation.",
18
+ },
19
+ {
20
+ "name": "Submit",
21
+ "description": "Routes to submit actions, plans and more to the simulation.",
22
+ },
23
+ ]
@@ -0,0 +1,19 @@
1
+ """
2
+ This module contains all the routers for the different features available in BluebirdATC. Note that, unlike in compiled
3
+ programming languages where disabled features are not included in the machine code, not including certain routers just
4
+ means that the endpoint is not available but the logic will continue to be available in BluebirdATC,
5
+ and therefore logged.
6
+
7
+ All the following routers are independent on the implementation of the storage of the runners and get this through the
8
+ fastapi dependency. See the respective documentation in ../runnerabc.py.
9
+
10
+ Note the endpoint for loading a run, that the HMI and most clients expect, is not included here as they are dependent on
11
+ the implementation of the store, which is dependent on the use case. An example of this endpoint, and any other
12
+ endpoints not included within the groups defined here are available in ../routes.py
13
+ """
14
+
15
+ from .core import core_router
16
+
17
+ __all__ = [
18
+ "core_router",
19
+ ]
@@ -0,0 +1,295 @@
1
+ import typing
2
+ from datetime import datetime, timezone
3
+
4
+ import pandas as pd
5
+ from bluebird_dt.events.event_logger import SimStartStop
6
+ from bluebird_dt.simulator.common import list_sim_scenario_categories, list_sim_scenarios
7
+ from fastapi import APIRouter
8
+
9
+ from bluebird_api.models import ActionInput, RunnerStore
10
+
11
+ from ..runnerabc import RunnerDep
12
+
13
+ core_router = APIRouter()
14
+
15
+
16
+ # explicitly store background tasks
17
+ background_tasks = set()
18
+
19
+
20
+ @core_router.get("/", tags=["Control"])
21
+ async def index() -> str:
22
+ """
23
+ Verify that the API is running.
24
+ """
25
+
26
+ return "Hello, BluebirdATC!"
27
+
28
+
29
+ @core_router.get("/list_scenario_categories", tags=["Scenarios"])
30
+ async def list_scenario_categories(): # noqa: ANN201
31
+ """
32
+ List the scenario categories.
33
+ """
34
+
35
+ return list_sim_scenario_categories()
36
+
37
+
38
+ @core_router.get("/list_scenarios/{category}", tags=["Scenarios"])
39
+ async def list_scenarios(category: str): # noqa: ANN201
40
+ """
41
+ List the scenarios in a given category.
42
+ """
43
+
44
+ return list_sim_scenarios(category)
45
+
46
+
47
+ @core_router.post("/close", tags=["Control"])
48
+ async def close(runner: RunnerDep) -> bool:
49
+ """
50
+ Unload a given simulator scenario.
51
+ """
52
+
53
+ await runner.delete()
54
+ RunnerStore.current_runner = None
55
+ return True
56
+
57
+
58
+ @core_router.post("/evolve/{time_delta}", tags=["Evolve"])
59
+ async def evolve(runner: RunnerDep, time_delta: float) -> bool:
60
+ """
61
+ Evolve the simulation by a given time delta (in seconds).
62
+ Note that this steps through the sim in steps of sim.evolve_period.
63
+ """
64
+ if time_delta <= 0.0:
65
+ raise Exception("Time delta must be positive.")
66
+
67
+ # evolve the sim in steps of self.evolve_period until time_delta is reached
68
+ update_amount = runner.evolve_period
69
+
70
+ while time_delta > 0.0:
71
+ runner.sim.evolve(update_amount)
72
+ time_delta -= update_amount
73
+
74
+ return True
75
+
76
+
77
+ @core_router.post("/start/{tick_frequency_period}", tags=["Evolve"])
78
+ async def start(runner: RunnerDep, tick_frequency_period: float) -> bool:
79
+ """
80
+ Start the simulation running with a given tick_frequency_period (in seconds).
81
+ """
82
+
83
+ if tick_frequency_period <= 0.0:
84
+ raise Exception("tick_frequency_period must be positive.")
85
+
86
+ runner.tick_frequency_period = tick_frequency_period
87
+ runner.sim.manager.event_logger.log_sim_event(
88
+ SimStartStop(event="clocks on", simulation_datetime=runner.sim.manager.environment.datetime)
89
+ )
90
+ runner.log_simrate()
91
+ runner.running = True
92
+
93
+ return True
94
+
95
+
96
+ @core_router.get("/environment", tags=["State"])
97
+ async def complete_environment( # noqa: ANN201
98
+ runner: RunnerDep, no_airspace: bool = False, last_n_observations: int = 0
99
+ ):
100
+ """
101
+ Get the all of the environment data.
102
+ """
103
+ if RunnerStore.current_runner is None or runner.sim is None:
104
+ return {"exists": False}
105
+ # HMI doesn't need to reload the environment again
106
+ runner.sim.manager.reload_environment = False
107
+
108
+ return runner.sim.environment(
109
+ sim_time=runner.sim.manager.environment.time,
110
+ sector_id=None,
111
+ no_airspace=no_airspace,
112
+ last_n_observations=last_n_observations,
113
+ )
114
+
115
+
116
+ @core_router.get("/environment/{sector_id}", tags=["State"])
117
+ async def environment( # noqa: ANN201
118
+ runner: RunnerDep, sector_id: str | None = None, no_airspace: bool = False, last_n_observations: int = 0
119
+ ):
120
+ """
121
+ Get the environment data for a given sector.
122
+ """
123
+ if RunnerStore.current_runner is None or runner.sim is None:
124
+ return {"exists": False}
125
+ # HMI doesn't need to reload the environment again
126
+ runner.sim.manager.reload_environment = False
127
+
128
+ return runner.sim.environment(
129
+ sim_time=runner.sim.manager.environment.time,
130
+ sector_id=sector_id,
131
+ no_airspace=no_airspace,
132
+ last_n_observations=last_n_observations,
133
+ )
134
+
135
+
136
+ @core_router.get("/static_data", tags=["State"])
137
+ async def static_data( # noqa: ANN201
138
+ runner: RunnerDep,
139
+ ):
140
+ """
141
+ Get the static data for the scenario.
142
+ """
143
+ if RunnerStore.current_runner is None or runner.sim is None:
144
+ return {"exists": False}
145
+ return {"exists": True} | runner.sim.static_data(
146
+ sim_time=runner.sim.manager.environment.time,
147
+ )
148
+
149
+
150
+ @core_router.get("/dynamic_data/{sector_id}", tags=["State"])
151
+ async def dynamic_data( # noqa: ANN201
152
+ runner: RunnerDep, sector_id: str | None = None
153
+ ):
154
+ """
155
+ Get the dynamic data for the scenario.
156
+ """
157
+ if RunnerStore.current_runner is None or runner.sim is None:
158
+ return {"exists": False}
159
+ sim_time = runner.sim.manager.environment.time
160
+ if sector_id.lower() == "none" or sector_id.lower() == "all":
161
+ return {"exists": True} | runner.sim.dynamic_data(sim_time)
162
+
163
+ return {"exists": True} | runner.sim.dynamic_data(sim_time=sim_time, sector_id=sector_id)
164
+
165
+
166
+ @core_router.post("/actions", tags=["Submit"])
167
+ async def actions(runner: RunnerDep, action_input: list[ActionInput]) -> bool:
168
+ """
169
+ Send actions to the simulator. The POST body should be a JSON list of
170
+ actions.
171
+ """
172
+
173
+ timed_actions = []
174
+
175
+ for act in action_input:
176
+ agent = act.agent
177
+ callsign = act.callsign
178
+ kind = act.kind
179
+ value = act.value
180
+ sector = act.sector
181
+
182
+ action_time = (
183
+ datetime.fromtimestamp(
184
+ runner.sim.manager.environment.time,
185
+ tz=timezone.utc,
186
+ ).isoformat(timespec="microseconds")[:-6]
187
+ ).replace("T", " ")
188
+
189
+ print(f"{action_time} : {agent} : {callsign} -> {kind} = {value}")
190
+
191
+ action: dict[str, typing.Any] = {
192
+ "agent": agent,
193
+ "callsign": callsign,
194
+ "kind": kind,
195
+ "value": value,
196
+ "time": action_time,
197
+ "sector": sector,
198
+ }
199
+
200
+ timed_actions.append(action)
201
+
202
+ runner.sim.action(timed_actions)
203
+
204
+ return True
205
+
206
+
207
+ @core_router.get("/status", tags=["Control"])
208
+ async def runner_status(runner: RunnerDep): # noqa: ANN201
209
+ """
210
+ Get the current state of the current run.
211
+ """
212
+ if RunnerStore.current_runner is None:
213
+ return {"exists": False}
214
+ return {
215
+ "exists": runner.scenario_name is not None,
216
+ "iterations": runner.tick,
217
+ "category": runner.category,
218
+ "scenario": runner.scenario_name,
219
+ "running": runner.running,
220
+ "evolve_period": runner.evolve_period,
221
+ "tick_frequency_period": runner.tick_frequency_period,
222
+ "kill": runner.kill,
223
+ "reload": runner.sim.manager.reload_environment if runner.sim is not None else False,
224
+ "time_of_next_tick": runner.time_of_next_tick.isoformat(),
225
+ }
226
+
227
+
228
+ @core_router.post("/save", tags=["Control"])
229
+ async def save(runner: RunnerDep) -> bool:
230
+ """
231
+ Save sim state to JSON file.
232
+ """
233
+ runner.sim.save()
234
+
235
+ return True
236
+
237
+
238
+ @core_router.post("/evolve_period/{evolve_period}", tags=["Evolve"])
239
+ async def set_evolve_period(runner: RunnerDep, evolve_period: float) -> bool:
240
+ """
241
+ Change the update period.
242
+ """
243
+
244
+ if evolve_period <= 0.0:
245
+ raise Exception("Evolve period must be positive.")
246
+
247
+ runner.evolve_period = evolve_period
248
+
249
+ return True
250
+
251
+
252
+ @core_router.post("/tick_frequency/{tick_frequency}", tags=["Evolve"])
253
+ async def set_tick_frequency(runner: RunnerDep, tick_frequency: float) -> bool:
254
+ """
255
+ Change the update tick frequency.
256
+ """
257
+
258
+ if tick_frequency <= 0.0:
259
+ raise Exception("Tick frequency must be positive.")
260
+
261
+ runner.tick_frequency_period = tick_frequency
262
+
263
+ return True
264
+
265
+
266
+ @core_router.post("/pause", tags=["Evolve"])
267
+ async def pause(runner: RunnerDep) -> bool:
268
+ """
269
+ Pause the simulation.
270
+ """
271
+ runner.running = False
272
+
273
+ return True
274
+
275
+
276
+ @core_router.post("/rewind/{new_time}", tags=["Control"])
277
+ async def rewind(runner: RunnerDep, new_time: str) -> bool:
278
+ """
279
+ Rewind sim to a previous time
280
+ """
281
+
282
+ date_format = "%Y-%m-%d %H:%M:%S"
283
+ rewind_to_time = pd.to_datetime(new_time, format=date_format)
284
+ runner.sim.manager.rewind_to_time(rewind_to_time)
285
+
286
+ return True
287
+
288
+
289
+ @core_router.get("/selected_aircraft/{sector_id}", tags=["State"])
290
+ async def get_selected_aircraft(sector_id: str, runner: RunnerDep) -> dict:
291
+ """
292
+ Get the currently selected aircraft for a given sector.
293
+ """
294
+
295
+ return runner.hmi.get(sector_id, {"selected_aircraft": ""})
bluebird_api/routes.py ADDED
@@ -0,0 +1,50 @@
1
+ """
2
+ The routes module builds the router for the proided endpoint and adds any endpoints only available for
3
+ BluebirdATC, including loading which is implementation dependent.
4
+ """
5
+
6
+ import asyncio
7
+
8
+ from fastapi import APIRouter, Depends, Request
9
+
10
+ from bluebird_api.models import RunnerStore
11
+ from bluebird_api.routers.core import background_tasks
12
+ from bluebird_api.runner import Runner
13
+
14
+ from .routers import (
15
+ core_router,
16
+ )
17
+
18
+
19
+ async def simulator(request: Request):
20
+ """
21
+ The simulator dependency, available to endpoints using the RunnerDep dependency, provides access to the runner.
22
+ """
23
+
24
+ request.state.runner = RunnerStore.current_runner
25
+
26
+
27
+ router = APIRouter(dependencies=[Depends(simulator)])
28
+
29
+ router.include_router(core_router)
30
+
31
+
32
+ @router.post("/load/{category}/{scenario_name}", tags=["Control"])
33
+ async def load(category: str, scenario_name: str) -> bool: # noqa: ARG001
34
+ """
35
+ End any existing run, then create a new Runner and load a given simulator scenario.
36
+ """
37
+
38
+ if RunnerStore.current_runner is not None:
39
+ await RunnerStore.current_runner.delete()
40
+
41
+ RunnerStore.current_runner = Runner(category, scenario_name)
42
+
43
+ # start the task
44
+ task = asyncio.create_task(RunnerStore.current_runner.run_main())
45
+
46
+ # add the task to the background tasks set and have it auto-remove its reference from the set when done
47
+ background_tasks.add(task)
48
+ task.add_done_callback(background_tasks.remove)
49
+
50
+ return True
bluebird_api/runner.py ADDED
@@ -0,0 +1,27 @@
1
+ """
2
+ The runner implementation for the general BluebirdATC API. This runner is responsible for the initialisation of the
3
+ simulator class.
4
+ """
5
+
6
+ import asyncio
7
+
8
+ from bluebird_dt.simulator import Simulator
9
+ from typing_extensions import override
10
+
11
+ from bluebird_api.runnerabc import RunnerABC
12
+
13
+
14
+ class Runner(RunnerABC):
15
+ def __init__(self, category: str, scenario_name: str):
16
+ super().__init__(category, scenario_name)
17
+
18
+ @override
19
+ def initialise_simulator(self, *args, **kwargs) -> Simulator: # noqa: ANN002, ANN003
20
+ return Simulator.from_category(*args, **kwargs)
21
+
22
+ @override
23
+ async def delete(self):
24
+ self.kill = True
25
+ self.sim.save()
26
+ self.sim = None
27
+ await asyncio.sleep(3)
@@ -0,0 +1,148 @@
1
+ """
2
+ This module provides the interfaces used in all endpoints to obtain data from the runner and simulator that is being
3
+ requested.
4
+
5
+ The first thing available is the RunnerABC class, which is implemented for BluebirdATC in ./runner.py.
6
+
7
+ Secondly, and most importantly, this module provides the FastAPI dependency for the runner, used to access the simulator
8
+ instance, RunnerDep.
9
+ To use this dependency, create an endpoint as would be done normally.
10
+ As one of the arguments to the function, include the RunnerDep type alias as shown in the example below and
11
+ it will be available to interact with.
12
+
13
+ >>> from ..runnerabc import RunnerDep
14
+ >>> @core_router.post("/close", tags=["Control"])
15
+ >>> async def close(runner: RunnerDep) -> bool:
16
+ >>> await runner.delete()
17
+ >>> return True
18
+
19
+ If during resolution of the runner, for example trying to find the runner, the runner is not available,
20
+ a HTTP error 404 (Not found) will be returned before even running the function above.
21
+ """
22
+
23
+ import asyncio
24
+ import typing
25
+ from abc import ABC, abstractmethod
26
+ from collections import defaultdict
27
+ from datetime import datetime, timedelta
28
+
29
+ from bluebird_dt.events.event_logger import SimRateUpdate
30
+ from bluebird_dt.logger import logger
31
+ from bluebird_dt.simulator.simulator import Simulator
32
+ from fastapi import Depends, HTTPException, Request
33
+
34
+
35
+ class RunnerABC(ABC):
36
+ kill: bool
37
+ category: str
38
+ scenario_name: str
39
+ sim: Simulator
40
+ running: bool
41
+ evolve_period: float
42
+ tick_frequency_period: float
43
+ tick: int
44
+ hmi: dict
45
+
46
+ @abstractmethod
47
+ def initialise_simulator(self, *args, **kwargs) -> Simulator: # noqa: ANN002, ANN003
48
+ """
49
+ Function to instantiate the simulator class, or variations of it for each use case.
50
+
51
+ This function is designed to be a transparent function taking all the arguments passed into the constructor of
52
+ the runner class from the load endpoint.
53
+ """
54
+ pass
55
+
56
+ @abstractmethod
57
+ async def delete(self):
58
+ pass
59
+
60
+ def log_simrate(self):
61
+ """
62
+ Creates an entry in the file logs with the current tick frequency and evolve period of the runner.
63
+
64
+ See documentation for the event_logger.log_sim_event for more information.
65
+ """
66
+ self.sim.manager.event_logger.log_sim_event(
67
+ SimRateUpdate(
68
+ simulation_datetime=self.sim.manager.environment.datetime,
69
+ tick_frequency=self.tick_frequency_period,
70
+ evolve_period=self.evolve_period,
71
+ )
72
+ )
73
+
74
+ def __init__(self, category: str, scenario_name: str, log_name: str | None = None):
75
+ """
76
+ Constructor of the Runner classes.
77
+
78
+ Although the argumets of this function are currently hardcoded, if a usecase requires different ones they will
79
+ be replaced by args and kwargs placeholders.
80
+
81
+ Arguments
82
+ ---------
83
+ category: str
84
+ The category of scenario to load
85
+ scenario_name: str
86
+ The specific scenario of the category to load
87
+ log_name: str
88
+ The name to store the logs for the run.
89
+ """
90
+ self.category = category
91
+ self.scenario_name = scenario_name
92
+ self.sim = self.initialise_simulator(self.category, self.scenario_name, log_filename=log_name)
93
+ self.running = False
94
+ self.evolve_period = 6.0
95
+ self.tick_frequency_period = 6.0
96
+ self.kill = False
97
+ self.time_of_next_tick = datetime.min
98
+ self.tick = 0
99
+ self.hmi = defaultdict(lambda: {"selected_aircraft": ""})
100
+
101
+ # allow 'None' and 'ALL' sectors to also select aircraft
102
+ self.hmi["None"] = {"selected_aircraft": ""}
103
+ self.hmi["ALL"] = {"selected_aircraft": ""}
104
+ # try a lock to avoid concurrency bugs
105
+ self._sim_lock = asyncio.Lock()
106
+
107
+ async def run_main(self):
108
+ self.time_of_next_tick = datetime.now()
109
+
110
+ while True:
111
+ if self.running and datetime.now() >= self.time_of_next_tick:
112
+ async with self._sim_lock:
113
+ start_time = datetime.now()
114
+ self.time_of_next_tick = start_time + timedelta(seconds=self.tick_frequency_period)
115
+
116
+ self.sim.evolve(self.evolve_period)
117
+ self.tick += 1
118
+ logger.info(f"evolve time: {datetime.now() - start_time}")
119
+
120
+ if self.kill:
121
+ self.category = None
122
+ self.scenario_name = None
123
+ break
124
+
125
+ await asyncio.sleep(0.1)
126
+
127
+
128
+ async def runner(request: Request) -> RunnerABC: # noqa: ARG001
129
+ """
130
+ Function taking the runner information from the state, and making it available for the endpoint that uses it. See
131
+ module documentation for more details on usage, and an example.
132
+
133
+ This FastAPI dependency will throw a HTTP exception if the runner isinstance is not found, therefore this does not
134
+ need to be handled by the individual endpoint.
135
+ """
136
+ runner = request.state.runner
137
+
138
+ if runner is None:
139
+ raise HTTPException(404, "Runner instance not found")
140
+
141
+ if not isinstance(runner, RunnerABC):
142
+ raise HTTPException(500, "The runner passed is not a valid type.")
143
+
144
+ request.state.runner = None
145
+ return runner
146
+
147
+
148
+ RunnerDep = typing.Annotated[RunnerABC, Depends(runner)]
@@ -0,0 +1,37 @@
1
+ Metadata-Version: 2.4
2
+ Name: bluebird-api
3
+ Version: 0.1.0
4
+ Summary: A REST API for the Project Bluebird Digital Twin
5
+ Author: Project Bluebird
6
+ License-Expression: AGPL-3.0-or-later
7
+ Requires-Python: <3.15,>=3.10
8
+ Requires-Dist: bluebird-dt
9
+ Requires-Dist: fastapi<0.137,>=0.136.1
10
+ Requires-Dist: lxml-html-clean>=0.4.4
11
+ Requires-Dist: uvicorn<0.47,>=0.46.0
12
+ Description-Content-Type: text/markdown
13
+
14
+ ## The REST API for BluebirdATC
15
+
16
+ It is possible to run the BluebirdATC digital twin in a server process, such that the simulation will evolve at regular time intervals, and Agents and/or frontend visualization software can interact with it via HTTP requests.
17
+ In particular, users can:
18
+ * Query available scenario categories and scenarios.
19
+ * Load a selected scenario.
20
+ * Evolve the simulation by a specified time interval.
21
+ * Obtain the current state of the `Environment`.
22
+ * Submit `Actions` to individual aircraft.
23
+ * Save logfiles with data on all steps of the simulation.
24
+
25
+ In order to run the app, with all the correct dependencies for this feature, from the `BluebirdATC/bluebird-api` directory, run the command:
26
+
27
+ ```shell
28
+ uv run uvicorn bluebird_api:app --port 8000
29
+ ```
30
+
31
+ You should then be able to go to [http://localhost:8000](http://localhost:8000) in a web browser, and see the message "Hello, BluebirdATC!".
32
+
33
+ To see the full list and description of API endpoints, with the application running, go to [http://localhost:8000/docs](http://localhost:8000/docs).
34
+
35
+ ## Frontend visualisation
36
+
37
+ The app also serves the frontend visualization (more details on that can be found [here](../bluebird-hmi/README.md)), at the URL [http://localhost:8000/hmi](http://localhost:8000/hmi).
@@ -0,0 +1,11 @@
1
+ bluebird_api/__init__.py,sha256=MknZUHlacEtAxkoDnRM3seRpPc-NoU3ZBrXWhq51GUU,2425
2
+ bluebird_api/models.py,sha256=WDn5DY3e9-vkEYiJUMcXLFTPfXm9GMaErgCFLcBV0y0,1310
3
+ bluebird_api/route_tags.py,sha256=O7lt8V_lICSDaiv_bCBtloZyIkuFtrSIoP7rVzAl3Ko,631
4
+ bluebird_api/routes.py,sha256=Ta-olnEcMiDVrIgSWBDs7StLbzXJ_CCI0NXQIUoIkNM,1457
5
+ bluebird_api/runner.py,sha256=0S2IdAcOzbROARSx1OySp6xBM2qwM3VEmoo3fQw34v8,731
6
+ bluebird_api/runnerabc.py,sha256=BV9Z2hPYrE3-ay0TDZtqlOPHIduuDKqqQ9ApOHvIpEk,5310
7
+ bluebird_api/routers/__init__.py,sha256=1ta7f7k3a6GaAKIYniQs4xquL7OwXJtAsipaWfS7POs,946
8
+ bluebird_api/routers/core.py,sha256=k2RIdhLVzF5mjs-iuLlsidtYXDj_d_tSD_P09QT8--4,8320
9
+ bluebird_api-0.1.0.dist-info/METADATA,sha256=kd1F1ZW5AcL1T6J-ysWUW5clAT4tGRyn16E-EIFr4RA,1664
10
+ bluebird_api-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
11
+ bluebird_api-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any