Skip to content

Commit 6943d85

Browse files
Implement read-only mode
1 parent 8f1b4cf commit 6943d85

File tree

13 files changed

+127
-65
lines changed

13 files changed

+127
-65
lines changed

aiida_restapi/__init__.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
"""AiiDA REST API for data queries and workflow managment."""
1+
"""AiiDA REST API for data queries and workflow management."""
22

33
__version__ = '0.1.0a1'
4-
5-
from .main import app # noqa: F401

aiida_restapi/cli/__init__.py

Whitespace-only changes.

aiida_restapi/cli/main.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import click
2+
import uvicorn
3+
4+
from aiida_restapi.main import create_app
5+
6+
7+
@click.group()
8+
def cli() -> None:
9+
"""AiiDA REST API management CLI."""
10+
pass
11+
12+
13+
@cli.command()
14+
@click.option('--host', default='127.0.0.1', show_default=True, help='Host to bind.')
15+
@click.option('--port', default=8000, show_default=True, type=int, help='Port to bind.')
16+
@click.option('--read-only', is_flag=True, help='Run the REST API in read-only mode.')
17+
def start(read_only: bool, host: str, port: int) -> None:
18+
"""Start the AiiDA REST API service."""
19+
click.echo(f'Starting REST API (read_only={read_only}) on {host}:{port}')
20+
app = create_app(read_only=read_only)
21+
uvicorn.run(app, host=host, port=port)

aiida_restapi/main.py

Lines changed: 28 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,42 @@
11
"""Declaration of FastAPI application."""
22

3+
import typing as t
4+
35
from fastapi import FastAPI, Request
46
from fastapi.responses import HTMLResponse
57

68
from aiida_restapi.graphql import main
79
from aiida_restapi.routers import auth, computers, daemon, groups, nodes, submit, users
810
from aiida_restapi.utils import generate_endpoints_table
911

10-
app = FastAPI()
12+
13+
def generate_endpoints_table_endpoint(app: FastAPI) -> t.Callable[[Request], HTMLResponse]:
14+
"""Generate an endpoint that lists all registered API routes."""
15+
16+
def list_endpoints(request: Request) -> HTMLResponse:
17+
"""Return an HTML table of all registered API routes."""
18+
return HTMLResponse(
19+
content=generate_endpoints_table(
20+
str(request.base_url).rstrip('/'),
21+
app.routes,
22+
),
23+
)
24+
25+
return list_endpoints
1126

1227

13-
@app.get('/', response_class=HTMLResponse)
14-
def list_endpoints(request: Request) -> HTMLResponse:
15-
"""Return an HTML table of all registered API routes."""
16-
return HTMLResponse(
17-
content=generate_endpoints_table(
18-
str(request.base_url).rstrip('/'),
19-
app.routes,
20-
),
21-
)
28+
def create_app(read_only: bool = False) -> FastAPI:
29+
"""Create the FastAPI application and include the routers."""
30+
app = FastAPI()
31+
app.include_router(auth.router)
2232

33+
for module in (computers, daemon, groups, nodes, submit, users):
34+
if read_router := getattr(module, 'read_router', None):
35+
app.include_router(read_router)
36+
if not read_only and (write_router := getattr(module, 'write_router', None)):
37+
app.include_router(write_router)
2338

24-
app.include_router(auth.router)
25-
app.include_router(computers.router)
26-
app.include_router(daemon.router)
27-
app.include_router(nodes.router)
28-
app.include_router(groups.router)
29-
app.include_router(users.router)
30-
app.include_router(submit.router)
39+
app.add_route('/graphql', main.app)
40+
app.add_route('/', lambda request: generate_endpoints_table_endpoint(app)(request))
3141

32-
app.add_route('/graphql', main.app, name='graphql', methods=['POST'])
42+
return app

aiida_restapi/routers/computers.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,13 @@
1515

1616
from .auth import UserInDB, get_current_active_user
1717

18-
router = APIRouter()
18+
read_router = APIRouter()
19+
write_router = APIRouter()
1920

2021
repository = EntityRepository[orm.Computer, orm.Computer.Model](orm.Computer)
2122

2223

23-
@router.get('/computers/schema')
24+
@read_router.get('/computers/schema')
2425
async def get_computers_schema(
2526
which: t.Literal['get', 'post'] = Query(
2627
'get',
@@ -39,7 +40,7 @@ async def get_computers_schema(
3940
raise HTTPException(status_code=422, detail=str(err)) from err
4041

4142

42-
@router.get('/computers/projectable_properties', response_model=list[str])
43+
@read_router.get('/computers/projectable_properties', response_model=list[str])
4344
async def get_computer_projectable_properties() -> list[str]:
4445
"""Get projectable properties for AiiDA computers.
4546
@@ -48,7 +49,7 @@ async def get_computer_projectable_properties() -> list[str]:
4849
return repository.get_projectable_properties()
4950

5051

51-
@router.get(
52+
@read_router.get(
5253
'/computers',
5354
response_model=PaginatedResults[orm.Computer.Model],
5455
response_model_exclude_none=True,
@@ -66,7 +67,7 @@ async def get_computers(
6667
return repository.get_entities(queries)
6768

6869

69-
@router.get(
70+
@read_router.get(
7071
'/computers/{computer_id}',
7172
response_model=orm.Computer.Model,
7273
response_model_exclude_none=True,
@@ -86,7 +87,7 @@ async def get_computer(computer_id: int) -> orm.Computer.Model:
8687
raise HTTPException(status_code=404, detail=f'Could not find a Computer with id {computer_id}')
8788

8889

89-
@router.get('/computers/{computer_id}/metadata', response_model=dict[str, t.Any])
90+
@read_router.get('/computers/{computer_id}/metadata', response_model=dict[str, t.Any])
9091
@with_dbenv()
9192
async def get_computer_metadata(computer_id: int) -> dict[str, t.Any]:
9293
"""Get metadata of an AiiDA computer by id.
@@ -102,7 +103,7 @@ async def get_computer_metadata(computer_id: int) -> dict[str, t.Any]:
102103
raise HTTPException(status_code=404, detail=f'Could not find a Computer with id {computer_id}')
103104

104105

105-
@router.post(
106+
@write_router.post(
106107
'/computers',
107108
response_model=orm.Computer.Model,
108109
response_model_exclude_none=True,

aiida_restapi/routers/daemon.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@
1111

1212
from .auth import UserInDB, get_current_active_user
1313

14-
router = APIRouter()
14+
read_router = APIRouter()
15+
write_router = APIRouter()
1516

1617

1718
class DaemonStatusModel(BaseModel):
@@ -21,7 +22,7 @@ class DaemonStatusModel(BaseModel):
2122
num_workers: t.Optional[int] = Field(description='The number of workers if the daemon is running.')
2223

2324

24-
@router.get(
25+
@read_router.get(
2526
'/daemon/status',
2627
response_model=DaemonStatusModel,
2728
)
@@ -44,7 +45,7 @@ async def get_daemon_status() -> DaemonStatusModel:
4445
return DaemonStatusModel(running=True, num_workers=response['numprocesses'])
4546

4647

47-
@router.post(
48+
@write_router.post(
4849
'/daemon/start',
4950
response_model=DaemonStatusModel,
5051
)
@@ -71,7 +72,7 @@ async def get_daemon_start(
7172
return DaemonStatusModel(running=True, num_workers=response['numprocesses'])
7273

7374

74-
@router.post(
75+
@write_router.post(
7576
'/daemon/stop',
7677
response_model=DaemonStatusModel,
7778
)

aiida_restapi/routers/groups.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,13 @@
1515

1616
from .auth import UserInDB, get_current_active_user
1717

18-
router = APIRouter()
18+
read_router = APIRouter()
19+
write_router = APIRouter()
1920

2021
repository = EntityRepository[orm.Group, orm.Group.Model](orm.Group)
2122

2223

23-
@router.get('/groups/schema')
24+
@read_router.get('/groups/schema')
2425
async def get_groups_schema(
2526
which: t.Literal['get', 'post'] = Query(
2627
'get',
@@ -39,7 +40,7 @@ async def get_groups_schema(
3940
raise HTTPException(status_code=422, detail=str(err)) from err
4041

4142

42-
@router.get('/groups/projectable_properties', response_model=list[str])
43+
@read_router.get('/groups/projectable_properties', response_model=list[str])
4344
async def get_group_projectable_properties() -> list[str]:
4445
"""Get projectable properties for AiiDA groups.
4546
@@ -48,7 +49,7 @@ async def get_group_projectable_properties() -> list[str]:
4849
return repository.get_projectable_properties()
4950

5051

51-
@router.get(
52+
@read_router.get(
5253
'/groups',
5354
response_model=PaginatedResults[orm.Group.Model],
5455
response_model_exclude_none=True,
@@ -66,7 +67,7 @@ async def get_groups(
6667
return repository.get_entities(queries)
6768

6869

69-
@router.get(
70+
@read_router.get(
7071
'/groups/{group_id}',
7172
response_model=orm.Group.Model,
7273
response_model_exclude_none=True,
@@ -89,7 +90,7 @@ async def get_group(group_id: int) -> orm.Group.Model:
8990
raise HTTPException(status_code=500, detail=str(err))
9091

9192

92-
@router.get(
93+
@read_router.get(
9394
'/groups/{group_id}/extras',
9495
response_model=dict[str, t.Any],
9596
)
@@ -110,7 +111,7 @@ async def get_group_extras(group_id: int) -> dict[str, t.Any]:
110111
raise HTTPException(status_code=500, detail=str(err)) from err
111112

112113

113-
@router.post(
114+
@write_router.post(
114115
'/groups',
115116
response_model=orm.Group.Model,
116117
response_model_exclude_none=True,

aiida_restapi/routers/nodes.py

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@
2121

2222
from .auth import UserInDB, get_current_active_user
2323

24-
router = APIRouter()
24+
read_router = APIRouter()
25+
write_router = APIRouter()
2526

2627
repository = NodeRepository[orm.Node, orm.Node.Model](orm.Node)
2728
model_registry = NodeModelRegistry()
@@ -34,7 +35,7 @@
3435
NodeModelUnion = model_registry.ModelUnion
3536

3637

37-
@router.get('/nodes/schema')
38+
@read_router.get('/nodes/schema')
3839
async def get_nodes_schema(
3940
node_type: str | None = Query(
4041
None,
@@ -63,7 +64,7 @@ async def get_nodes_schema(
6364
raise HTTPException(status_code=422, detail=str(exception)) from exception
6465

6566

66-
@router.get('/nodes/projectable_properties')
67+
@read_router.get('/nodes/projectable_properties')
6768
@with_dbenv()
6869
async def get_node_projectable_properties(
6970
node_type: str | None = Query(
@@ -84,7 +85,7 @@ async def get_node_projectable_properties(
8485
raise HTTPException(status_code=422, detail=str(err)) from err
8586

8687

87-
@router.get('/nodes/download_formats')
88+
@read_router.get('/nodes/download_formats')
8889
async def get_nodes_download_formats() -> dict[str, t.Any]:
8990
"""Get download formats for AiiDA nodes.
9091
@@ -100,7 +101,7 @@ async def get_nodes_download_formats() -> dict[str, t.Any]:
100101
raise HTTPException(status_code=500, detail=str(err)) from err
101102

102103

103-
@router.get(
104+
@read_router.get(
104105
'/nodes',
105106
response_model=PaginatedResults[orm.Node.Model],
106107
response_model_exclude_none=True,
@@ -128,7 +129,7 @@ class NodeType(pdt.BaseModel):
128129
node_schema: str = pdt.Field(description='The URL to access the schema of this node type.')
129130

130131

131-
@router.get('/nodes/types', response_model=list[NodeType])
132+
@read_router.get('/nodes/types', response_model=list[NodeType])
132133
async def get_node_types() -> list:
133134
"""Get all node types in machine-actionable format.
134135
@@ -159,7 +160,7 @@ async def get_node_types() -> list:
159160
]
160161

161162

162-
@router.get(
163+
@read_router.get(
163164
'/nodes/{node_id}',
164165
response_model=orm.Node.Model,
165166
response_model_exclude_none=True,
@@ -182,7 +183,7 @@ async def get_node(node_id: int) -> orm.Node.Model:
182183
raise HTTPException(status_code=500, detail=str(err)) from err
183184

184185

185-
@router.get(
186+
@read_router.get(
186187
'/nodes/{node_id}/attributes',
187188
response_model=dict[str, t.Any],
188189
)
@@ -203,7 +204,7 @@ async def get_node_attributes(node_id: int) -> dict[str, t.Any]:
203204
raise HTTPException(status_code=500, detail=str(err)) from err
204205

205206

206-
@router.get(
207+
@read_router.get(
207208
'/nodes/{node_id}/extras',
208209
response_model=dict[str, t.Any],
209210
)
@@ -224,7 +225,7 @@ async def get_node_extras(node_id: int) -> dict[str, t.Any]:
224225
raise HTTPException(status_code=500, detail=str(err)) from err
225226

226227

227-
@router.get('/nodes/{node_id}/download')
228+
@read_router.get('/nodes/{node_id}/download')
228229
@with_dbenv()
229230
async def download_node(
230231
node_id: int,
@@ -307,7 +308,7 @@ class RepoDirMetadata(pdt.BaseModel):
307308
MetadataType = t.Union[RepoFileMetadata, RepoDirMetadata]
308309

309310

310-
@router.get(
311+
@read_router.get(
311312
'/nodes/{node_id}/repo/metadata',
312313
response_model=dict[str, MetadataType],
313314
)
@@ -328,7 +329,7 @@ async def get_node_repo_file_metadata(node_id: int) -> dict[str, dict]:
328329
raise HTTPException(status_code=500, detail=str(err)) from err
329330

330331

331-
@router.get('/nodes/{node_id}/repo/contents')
332+
@read_router.get('/nodes/{node_id}/repo/contents')
332333
@with_dbenv()
333334
async def get_node_repo_file_contents(
334335
node_id: int,
@@ -384,7 +385,7 @@ def zip_stream() -> t.Generator[bytes, None, None]:
384385
return StreamingResponse(zip_stream(), media_type='application/zip', headers=headers)
385386

386387

387-
@router.post(
388+
@write_router.post(
388389
'/nodes',
389390
response_model=orm.Node.Model,
390391
response_model_exclude_none=True,
@@ -411,7 +412,7 @@ async def create_node(
411412
raise HTTPException(status_code=500, detail=str(exception)) from exception
412413

413414

414-
@router.post(
415+
@write_router.post(
415416
'/nodes/file-upload',
416417
response_model=orm.Node.Model,
417418
response_model_exclude_none=True,

aiida_restapi/routers/submit.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

1414
from .auth import UserInDB, get_current_active_user
1515

16-
router = APIRouter()
16+
write_router = APIRouter()
1717

1818

1919
def process_inputs(inputs: dict[str, t.Any]) -> dict[str, t.Any]:
@@ -58,7 +58,7 @@ def validate_inputs(cls, inputs: dict[str, t.Any]) -> dict[str, t.Any]:
5858
return process_inputs(inputs)
5959

6060

61-
@router.post(
61+
@write_router.post(
6262
'/submit',
6363
response_model=orm.Node.Model,
6464
response_model_exclude_none=True,

0 commit comments

Comments
 (0)