Skip to content

Commit 8664af0

Browse files
Implement read-only mode
1 parent fc55ef9 commit 8664af0

File tree

12 files changed

+111
-63
lines changed

12 files changed

+111
-63
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: 26 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,26 +7,34 @@
77
from aiida_restapi.routers import auth, computers, daemon, groups, nodes, submit, users
88
from aiida_restapi.utils import generate_endpoints_table
99

10-
app = FastAPI()
1110

11+
def generate_endpoints_table_endpoint(app: FastAPI):
12+
"""Generate an endpoint that lists all registered API routes."""
1213

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-
)
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+
)
2222

23+
return list_endpoints
2324

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)
3125

32-
app.add_route('/graphql', main.app, name='graphql', methods=['POST'])
26+
def create_app(read_only: bool = False) -> FastAPI:
27+
"""Create the FastAPI application and include the routers."""
28+
app = FastAPI()
29+
app.include_router(auth.router)
30+
31+
for module in (computers, daemon, groups, nodes, submit, users):
32+
if read_router := getattr(module, 'read_router', None):
33+
app.include_router(read_router)
34+
if not read_only and (write_router := getattr(module, 'write_router', None)):
35+
app.include_router(write_router)
36+
37+
app.add_route('/graphql', main.app)
38+
app.add_route('/', lambda request: generate_endpoints_table_endpoint(app)(request))
39+
40+
return app

aiida_restapi/routers/computers.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,14 @@
1212

1313
from .auth import UserInDB, get_current_active_user
1414

15-
router = APIRouter()
15+
read_router = APIRouter()
16+
write_router = APIRouter()
1617

1718

1819
repository = EntityRepository[orm.Computer, orm.Computer.Model](orm.Computer)
1920

2021

21-
@router.get('/computers/schema')
22+
@read_router.get('/computers/schema')
2223
async def get_computers_schema(
2324
which: t.Literal['get', 'post'] | None = Query(
2425
None,
@@ -43,7 +44,7 @@ async def get_computers_schema(
4344
raise HTTPException(status_code=400, detail='Parameter "which" must be either "get" or "post"')
4445

4546

46-
@router.get('/computers/projectable_properties', response_model=list[str])
47+
@read_router.get('/computers/projectable_properties', response_model=list[str])
4748
async def get_computer_projectable_properties() -> list[str]:
4849
"""Get projectable properties for AiiDA computers.
4950
@@ -52,7 +53,7 @@ async def get_computer_projectable_properties() -> list[str]:
5253
return repository.get_projectable_properties()
5354

5455

55-
@router.get(
56+
@read_router.get(
5657
'/computers',
5758
response_model=PaginatedResults[orm.Computer.Model],
5859
response_model_exclude_none=True,
@@ -69,7 +70,7 @@ async def get_computers(
6970
return repository.get_entities(queries)
7071

7172

72-
@router.get(
73+
@read_router.get(
7374
'/computers/{comp_id}',
7475
response_model=orm.Computer.Model,
7576
response_model_exclude_none=True,
@@ -88,7 +89,7 @@ async def get_computer(comp_id: int) -> orm.Computer.Model:
8889
raise HTTPException(status_code=404, detail=f'Could not find any Computer with id {comp_id}')
8990

9091

91-
@router.post(
92+
@write_router.post(
9293
'/computers',
9394
response_model=orm.Computer.Model,
9495
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: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,13 @@
1212

1313
from .auth import UserInDB, get_current_active_user
1414

15-
router = APIRouter()
15+
read_router = APIRouter()
16+
write_router = APIRouter()
1617

1718
repository = EntityRepository[orm.Group, orm.Group.Model](orm.Group)
1819

1920

20-
@router.get('/groups/schema')
21+
@read_router.get('/groups/schema')
2122
async def get_groups_schema(
2223
which: t.Literal['get', 'post'] | None = Query(
2324
None,
@@ -42,7 +43,7 @@ async def get_groups_schema(
4243
raise HTTPException(status_code=400, detail='Parameter "which" must be either "get" or "post"')
4344

4445

45-
@router.get('/groups/projectable_properties', response_model=list[str])
46+
@read_router.get('/groups/projectable_properties', response_model=list[str])
4647
async def get_group_projectable_properties() -> list[str]:
4748
"""Get projectable properties for AiiDA groups.
4849
@@ -51,7 +52,7 @@ async def get_group_projectable_properties() -> list[str]:
5152
return repository.get_projectable_properties()
5253

5354

54-
@router.get(
55+
@read_router.get(
5556
'/groups',
5657
response_model=PaginatedResults[orm.Group.Model],
5758
response_model_exclude_none=True,
@@ -68,7 +69,7 @@ async def get_groups(
6869
return repository.get_entities(queries)
6970

7071

71-
@router.get(
72+
@read_router.get(
7273
'/groups/{group_id}',
7374
response_model=orm.Group.Model,
7475
response_model_exclude_none=True,
@@ -87,7 +88,7 @@ async def get_group(group_id: int) -> orm.Group.Model:
8788
raise HTTPException(status_code=404, detail=f'Could not find any Group with id {group_id}')
8889

8990

90-
@router.post(
91+
@write_router.post(
9192
'/groups',
9293
response_model=orm.Group.Model,
9394
response_model_exclude_none=True,

aiida_restapi/routers/nodes.py

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,15 @@
1818

1919
from .auth import UserInDB, get_current_active_user
2020

21-
router = APIRouter()
21+
read_router = APIRouter()
22+
write_router = APIRouter()
2223

2324

2425
repository = NodeRepository[orm.Node, orm.Node.Model](orm.Node)
2526
model_registry = NodeModelRegistry()
2627

2728

28-
@router.get('/nodes/schema')
29+
@read_router.get('/nodes/schema')
2930
async def get_nodes_schema(
3031
which: t.Literal['get', 'post'] | None = Query(
3132
None,
@@ -57,7 +58,7 @@ def generate_create_models() -> dict[str, dict[str, t.Any]]:
5758
)
5859

5960

60-
@router.get('/nodes/projectable_properties')
61+
@read_router.get('/nodes/projectable_properties')
6162
@with_dbenv()
6263
async def get_node_projectable_properties() -> list[str]:
6364
"""Get projectable properties for AiiDA nodes.
@@ -67,7 +68,7 @@ async def get_node_projectable_properties() -> list[str]:
6768
return repository.get_projectable_properties()
6869

6970

70-
@router.get('/nodes/download_formats')
71+
@read_router.get('/nodes/download_formats')
7172
async def get_nodes_download_formats() -> dict[str, t.Any]:
7273
"""Get download formats for AiiDA nodes.
7374
@@ -76,7 +77,7 @@ async def get_nodes_download_formats() -> dict[str, t.Any]:
7677
return resources.get_all_download_formats()
7778

7879

79-
@router.get(
80+
@read_router.get(
8081
'/nodes',
8182
response_model=PaginatedResults[orm.Node.Model],
8283
response_model_exclude_none=True,
@@ -93,7 +94,7 @@ async def get_nodes(
9394
return repository.get_entities(queries)
9495

9596

96-
@router.get('/nodes/types')
97+
@read_router.get('/nodes/types')
9798
async def get_nodes_types() -> list:
9899
"""
99100
Return all node types in a machine-actionable format:
@@ -123,7 +124,7 @@ async def get_nodes_types() -> list:
123124
]
124125

125126

126-
@router.get(
127+
@read_router.get(
127128
'/nodes/types/{node_type}',
128129
response_model=PaginatedResults[orm.Node.Model],
129130
response_model_exclude_none=True,
@@ -149,7 +150,7 @@ async def get_nodes_by_type(
149150
raise HTTPException(status_code=400, detail=str(exception)) from exception
150151

151152

152-
@router.get('/nodes/types/{node_type}/projectable_properties')
153+
@read_router.get('/nodes/types/{node_type}/projectable_properties')
153154
@with_dbenv()
154155
async def get_node_class_projectable_properties(node_type: str) -> list[str]:
155156
"""Get projectable properties of a given AiiDA node class.
@@ -166,7 +167,7 @@ async def get_node_class_projectable_properties(node_type: str) -> list[str]:
166167
raise HTTPException(status_code=400, detail=str(exception)) from exception
167168

168169

169-
@router.get('/nodes/types/{node_type}/schema')
170+
@read_router.get('/nodes/types/{node_type}/schema')
170171
async def get_node_class_schema(
171172
node_type: str,
172173
which: t.Literal['get', 'post'] | None = Query(
@@ -199,7 +200,7 @@ async def get_node_class_schema(
199200
raise HTTPException(status_code=404, detail='Parameter "which" must be either "get" or "post"')
200201

201202

202-
@router.get(
203+
@read_router.get(
203204
'/nodes/{node_id}',
204205
response_model=orm.Node.Model,
205206
response_model_exclude_none=True,
@@ -218,7 +219,7 @@ async def get_node(node_id: int) -> orm.Node.Model:
218219
raise HTTPException(status_code=404, detail=f'Could not find any Node with id {node_id}')
219220

220221

221-
@router.get('/nodes/{node_id}/download')
222+
@read_router.get('/nodes/{node_id}/download')
222223
@with_dbenv()
223224
async def download_node(
224225
node_id: int,
@@ -269,7 +270,7 @@ def stream() -> t.Generator[bytes, None, None]:
269270
)
270271

271272

272-
@router.get('/nodes/{node_id}/repo/contents')
273+
@write_router.get('/nodes/{node_id}/repo/contents')
273274
@with_dbenv()
274275
async def get_node_repo_file_contents(
275276
node_id: int,
@@ -315,7 +316,7 @@ def zip_stream() -> t.Generator[bytes, None, None]:
315316
return StreamingResponse(zip_stream(), media_type='application/zip')
316317

317318

318-
@router.post(
319+
@write_router.post(
319320
'/nodes',
320321
response_model=orm.Node.Model,
321322
response_model_exclude_none=True,
@@ -342,7 +343,7 @@ async def create_node(
342343

343344

344345
# TODO what about folderdata?
345-
@router.post(
346+
@write_router.post(
346347
'/nodes/file-upload',
347348
response_model=orm.Node.Model,
348349
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]:
@@ -61,7 +61,7 @@ def validate_inputs(cls, inputs: dict[str, t.Any]) -> dict[str, t.Any]:
6161
return process_inputs(inputs)
6262

6363

64-
@router.post(
64+
@write_router.post(
6565
'/submit',
6666
response_model=orm.Node.Model,
6767
response_model_exclude_none=True,

0 commit comments

Comments
 (0)