Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
9443d03
Leverage aiida-core pydantic models
edan-bainglass Dec 22, 2025
bd5b70f
Fix test typing and fixture usage
edan-bainglass Dec 3, 2025
4d600b5
Implement read-only mode
edan-bainglass Dec 19, 2025
97e8510
Implement server router
edan-bainglass Dec 3, 2025
3818bb5
Add `/nodes/statistics/user` endpoint to REST API
agoscinski Nov 21, 2024
963e753
Add `with_dbenv` decorator to node statistics endpoint
edan-bainglass Dec 2, 2025
49c4191
Explicitly define node statistics shape
edan-bainglass Dec 19, 2025
612b8b2
Update test
edan-bainglass Dec 19, 2025
701b22c
Update docstring
edan-bainglass Dec 19, 2025
dec5de4
Implement QB endpoint
edan-bainglass Dec 19, 2025
f970768
Implement node links endpoint
edan-bainglass Dec 22, 2025
7240a06
Switch from pk to uuid for entity identifier
edan-bainglass Dec 22, 2025
d051b98
Add prefixes at the router layer
edan-bainglass Dec 22, 2025
f3034cb
Reorder routes in server endpoints endpoint
edan-bainglass Dec 28, 2025
7748146
Rename repository module as services
edan-bainglass Dec 28, 2025
91a9c88
Discard docstrings
edan-bainglass Dec 29, 2025
065de36
Centralize error handling
edan-bainglass Dec 29, 2025
dfb90c4
Consolidate models
edan-bainglass Jan 1, 2026
cf71cb8
Rename `PaginatedResults.results` to `data`
edan-bainglass Jan 1, 2026
5640322
Peripheral cleanup
edan-bainglass Jan 1, 2026
1fbbd13
Fix docs
edan-bainglass Jan 1, 2026
b2f8a3a
Fix entrypoints
edan-bainglass Jan 2, 2026
72de288
Add missing tests
edan-bainglass Jan 2, 2026
3f606f0
Organize node endpoints
edan-bainglass Jan 2, 2026
b9e01d7
Fix tests
edan-bainglass Jan 2, 2026
72d002e
Fix field name with an alias
edan-bainglass Jan 17, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions aiida_restapi/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
"""AiiDA REST API for data queries and workflow managment."""
"""AiiDA REST API for data queries and workflow management."""

__version__ = '0.1.0a1'

from .main import app # noqa: F401
File renamed without changes.
31 changes: 31 additions & 0 deletions aiida_restapi/cli/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import os

import click
import uvicorn


@click.group()
def cli() -> None:
"""AiiDA REST API management CLI."""


@cli.command()
@click.option('--host', default='127.0.0.1', show_default=True)
@click.option('--port', default=8000, show_default=True, type=int)
@click.option('--read-only', is_flag=True)
@click.option('--watch', is_flag=True)
def start(read_only: bool, watch: bool, host: str, port: int) -> None:
"""Start the AiiDA REST API service."""

os.environ['AIIDA_RESTAPI_READ_ONLY'] = '1' if read_only else '0'

click.echo(f'Starting REST API (read_only={read_only}, watch={watch}) on {host}:{port}')

uvicorn.run(
'aiida_restapi.main:create_app',
host=host,
port=port,
reload=watch,
reload_dirs=['aiida_restapi'],
factory=True,
)
Empty file.
87 changes: 87 additions & 0 deletions aiida_restapi/common/errors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
"""Common error models.

These are used in ReDoc and OpenAPI documentation to describe error responses.
"""

import pydantic as pdt


class JsonDecodingError(pdt.BaseModel):
detail: str = pdt.Field(
description='The provided JSON input is invalid.',
examples=['Malformed JSON string'],
)


class NonExistentError(pdt.BaseModel):
detail: str = pdt.Field(
description='The requested resource does not exist.',
examples=['No result was found'],
)


class MultipleObjectsError(pdt.BaseModel):
detail: str = pdt.Field(
description='Multiple resources were found when exactly one was expected.',
examples=['Multiple results were found'],
)


class StoringNotAllowedError(pdt.BaseModel):
detail: str = pdt.Field(
description='Attempt to store the new resource was rejected.',
examples=['Resource cannot be stored'],
)


class InvalidInputError(pdt.BaseModel):
detail: str = pdt.Field(
description='The input provided was invalid.',
examples=['Input value out of range'],
)


class InvalidNodeTypeError(pdt.BaseModel):
detail: str = pdt.Field(
description='The node type does not exist.',
examples=['Unknown node type: "data.nonexistent.NonExistentNode"'],
)


class InvalidLicenseError(pdt.BaseModel):
detail: str = pdt.Field(
description='The operation is not permitted due to licensing restrictions.',
examples=['Operation not permitted under current license'],
)


class DaemonError(pdt.BaseModel):
detail: str = pdt.Field(
description='An error occurred while interacting with the AiiDA daemon.',
examples=[
'The daemon is not running',
'Failed to start daemon',
'The daemon is already running',
],
)


class InvalidOperationError(pdt.BaseModel):
detail: str = pdt.Field(
description='The requested operation is invalid.',
examples=['Cannot submit a process with `store_provenance=False`'],
)


class RequestValidationError(pdt.BaseModel):
detail: str = pdt.Field(
description='The request parameters failed validation.',
examples=['Invalid query parameter: "sort"'],
)


class QueryBuilderError(pdt.BaseModel):
detail: str = pdt.Field(
description='An error occurred while processing the QueryBuilder request.',
examples=['Invalid QueryBuilder query structure'],
)
5 changes: 5 additions & 0 deletions aiida_restapi/common/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
"""Common exception classes."""


class QueryBuilderException(Exception):
"""Exception raised for errors during QueryBuilder execution."""
20 changes: 20 additions & 0 deletions aiida_restapi/common/pagination.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
"""Pagination utilities."""

from __future__ import annotations

import typing as t

import pydantic as pdt

ResultType = t.TypeVar('ResultType')

__all__ = [
'PaginatedResults',
]


class PaginatedResults(pdt.BaseModel, t.Generic[ResultType]):
total: int
page: int
page_size: int
data: list[ResultType]
78 changes: 78 additions & 0 deletions aiida_restapi/common/query.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
"""REST API query utilities."""

from __future__ import annotations

import json
import typing as t

import pydantic as pdt
from fastapi import Query


class QueryParams(pdt.BaseModel):
filters: dict[str, t.Any] = pdt.Field(
default_factory=dict,
description='AiiDA QueryBuilder filters',
examples=[
{'node_type': {'==': 'data.core.int.Int.'}},
{'attributes.value': {'>': 42}},
],
)
order_by: str | list[str] | dict[str, t.Any] | None = pdt.Field(
None,
description='Fields to sort by',
examples=[
{'attributes.value': 'desc'},
],
)
page_size: pdt.PositiveInt = pdt.Field(
10,
description='Number of results per page',
examples=[10],
)
page: pdt.PositiveInt = pdt.Field(
1,
description='Page number',
examples=[1],
)


def query_params(
filters: str | None = Query(
None,
description='AiiDA QueryBuilder filters as JSON string',
),
order_by: str | None = Query(
None,
description='Comma-separated list of fields to sort by',
),
page_size: pdt.PositiveInt = Query(
10,
description='Number of results per page',
),
page: pdt.PositiveInt = Query(
1,
description='Page number',
),
) -> QueryParams:
"""Parse query parameters into a structured object.

:param filters: AiiDA QueryBuilder filters as JSON string.
:param order_by: Comma-separated string of fields to sort by.
:param page_size: Number of results per page.
:param page: Page number.
:return: Structured query parameters.
:raises HTTPException: If filters cannot be parsed as JSON.
"""
query_filters: dict[str, t.Any] = {}
query_order_by: str | list[str] | dict[str, t.Any] | None = None
if filters:
query_filters = json.loads(filters)
if order_by:
query_order_by = json.loads(order_by)
return QueryParams(
filters=query_filters,
order_by=query_order_by,
page_size=page_size,
page=page,
)
13 changes: 13 additions & 0 deletions aiida_restapi/common/types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
"""Common type variables."""

from __future__ import annotations

import typing as t

from aiida import orm

EntityType = t.TypeVar('EntityType', bound='orm.Entity')
EntityModelType = t.TypeVar('EntityModelType', bound='orm.Entity.Model')

NodeType = t.TypeVar('NodeType', bound='orm.Node')
NodeModelType = t.TypeVar('NodeModelType', bound='orm.Node.Model')
7 changes: 7 additions & 0 deletions aiida_restapi/config.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
"""Configuration of API"""

from aiida_restapi import __version__

# to get a string like this run:
# openssl rand -hex 32
SECRET_KEY = '09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7'
Expand All @@ -18,3 +20,8 @@
'disabled': False,
}
}

API_CONFIG = {
'PREFIX': '/api/v0',
'VERSION': __version__,
}
31 changes: 0 additions & 31 deletions aiida_restapi/exceptions.py

This file was deleted.

2 changes: 1 addition & 1 deletion aiida_restapi/graphql/comments.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import graphene as gr
from aiida.orm import Comment

from aiida_restapi.filter_syntax import parse_filter_str
from aiida_restapi.graphql.filter_syntax import parse_filter_str

from .orm_factories import (
ENTITY_DICT_TYPE,
Expand Down
2 changes: 1 addition & 1 deletion aiida_restapi/graphql/computers.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import graphene as gr
from aiida.orm import Computer

from aiida_restapi.filter_syntax import parse_filter_str
from aiida_restapi.graphql.filter_syntax import parse_filter_str
from aiida_restapi.graphql.plugins import QueryPlugin

from .nodes import NodesQuery
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@

from lark import Lark, Token, Tree

from . import static
from .utils import parse_date
from .. import static
from ..utils import parse_date

FILTER_GRAMMAR = resources.open_text(static, 'filter_grammar.lark')

Expand Down
2 changes: 1 addition & 1 deletion aiida_restapi/graphql/groups.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import graphene as gr
from aiida.orm import Group

from aiida_restapi.filter_syntax import parse_filter_str
from aiida_restapi.graphql.filter_syntax import parse_filter_str
from aiida_restapi.graphql.nodes import NodesQuery
from aiida_restapi.graphql.plugins import QueryPlugin

Expand Down
2 changes: 1 addition & 1 deletion aiida_restapi/graphql/logs.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import graphene as gr
from aiida.orm import Log

from aiida_restapi.filter_syntax import parse_filter_str
from aiida_restapi.graphql.filter_syntax import parse_filter_str

from .orm_factories import (
ENTITY_DICT_TYPE,
Expand Down
2 changes: 1 addition & 1 deletion aiida_restapi/graphql/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import graphene as gr
from aiida import orm

from aiida_restapi.filter_syntax import parse_filter_str
from aiida_restapi.graphql.filter_syntax import parse_filter_str
from aiida_restapi.graphql.plugins import QueryPlugin

from .comments import CommentsQuery
Expand Down
2 changes: 1 addition & 1 deletion aiida_restapi/graphql/orm_factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from graphql import GraphQLError
from pydantic import Json

from aiida_restapi.aiida_db_mappings import ORM_MAPPING, get_model_from_orm
from aiida_restapi.graphql.aiida_db_mappings import ORM_MAPPING, get_model_from_orm

from .config import ENTITY_LIMIT
from .utils import JSON, selected_field_names_naive
Expand Down
2 changes: 1 addition & 1 deletion aiida_restapi/graphql/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import graphene as gr
from aiida.orm import User

from aiida_restapi.filter_syntax import parse_filter_str
from aiida_restapi.graphql.filter_syntax import parse_filter_str

from .nodes import NodesQuery
from .orm_factories import (
Expand Down
Loading