Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
a3fcd5f
Fix ORM pydantic schemas
edan-bainglass Oct 8, 2025
85da446
Add Kpoints constructor
edan-bainglass Oct 8, 2025
2eedd02
Fix `Node.from_model`
edan-bainglass Oct 8, 2025
1c767b9
Missed cast
edan-bainglass Oct 8, 2025
fd7dd82
Discard direct `orm` import from `cmdline` package
edan-bainglass Oct 8, 2025
201d7c1
Allow unstored entity (de)serialization
edan-bainglass Oct 8, 2025
017f9fe
Update tests
edan-bainglass Oct 8, 2025
fa9582a
Fix computer type
edan-bainglass Oct 8, 2025
9c1ba17
Fix some typing issues
edan-bainglass Oct 9, 2025
b729290
Allow (de)serialization of unstored nodes
edan-bainglass Oct 10, 2025
45d02b0
Add guard for unhandled attributes at parent `Data` constructor
edan-bainglass Oct 10, 2025
5bdf506
Implement constructors for `KpointsData` and `BandsData`
edan-bainglass Oct 10, 2025
fa1fba7
Fix tests
edan-bainglass Oct 10, 2025
78c3ea5
Serialize/validate arrays as numpy arrays, not bytes
edan-bainglass Oct 10, 2025
959221d
Fix typing
edan-bainglass Oct 10, 2025
8079411
Fix unhandled attributes check
edan-bainglass Oct 10, 2025
b637cb4
Add `array_labels` back to `BandsData.Model` without constructor hand…
edan-bainglass Oct 10, 2025
fda8bc1
Nitpick some classes
edan-bainglass Oct 10, 2025
546e076
Fix docstring
edan-bainglass Oct 10, 2025
2a77c72
Remove user/ctime/mtime default factories from read-only fields
edan-bainglass Oct 11, 2025
193af72
Include attributes in node input model
edan-bainglass Oct 11, 2025
b7c8a65
Remove some unrelated changes
edan-bainglass Oct 12, 2025
2a37eff
Make repo content serialization opt-in
edan-bainglass Nov 4, 2025
48c605e
Restore array base64 serialization
edan-bainglass Nov 4, 2025
d461378
Discard explicit quotation for annotations
edan-bainglass Nov 4, 2025
807b238
Implement repo methods to extract object size and objects as a zipfil…
edan-bainglass Nov 4, 2025
4930ca0
Add field validator to transform computer pk to label
edan-bainglass Nov 4, 2025
96bd7fa
Fix `orm_to_model` type
edan-bainglass Nov 4, 2025
1ac8553
Fix types
edan-bainglass Nov 4, 2025
35b1030
Mark `InstalledCode.Model.computer` as an attribute
edan-bainglass Nov 5, 2025
aa85ff1
Fix `_prepare_yaml`
edan-bainglass Nov 5, 2025
67a2dd7
Update regression tests
edan-bainglass Nov 5, 2025
22453f9
Centralize model field collection
edan-bainglass Nov 5, 2025
4b03637
Move unhandled node attributes gate to `Node` class
edan-bainglass Nov 6, 2025
e8eddd2
Add `attributes` to `Node` constructor
edan-bainglass Nov 6, 2025
09fe76f
Fix code fields
edan-bainglass Nov 6, 2025
712ab72
Fix centralized model field collection
edan-bainglass Nov 6, 2025
e77d0d9
Fix tests
edan-bainglass Nov 6, 2025
a502917
Enable `ArrayNode` numpy array POST payloads via model validator
edan-bainglass Nov 6, 2025
8d532c7
Allow arguments in `orm_to_model`
edan-bainglass Nov 6, 2025
a24dc2e
Fix formatting
edan-bainglass Nov 6, 2025
b2a7bae
Fix `repository_path` default in portable code
edan-bainglass Nov 6, 2025
b1a65ba
Fix iterable array posting
edan-bainglass Nov 7, 2025
9d60f29
Fix model inheritance
edan-bainglass Nov 7, 2025
b05ccdf
Fix `from_serialized`
edan-bainglass Nov 7, 2025
636195a
Rename `InputModel` and `as_input_model` to `CreateModel` and `as_cre…
edan-bainglass Nov 8, 2025
d654e3a
Discard `exclude_from_cli`
edan-bainglass Nov 11, 2025
aba646a
Exclude `attributes` from ORM
edan-bainglass Nov 11, 2025
84ce866
Discard `unstored` from `from_serialized`
edan-bainglass Nov 12, 2025
2a87bd5
Discard `unstored` serialization parameter
edan-bainglass Nov 12, 2025
b69c1c7
Always pass `kwargs` to `orm_to_model`
edan-bainglass Nov 12, 2025
0285a39
Discard redundant class method in favor of correct model selection
edan-bainglass Nov 12, 2025
bc8a5d3
Use `is_stored` for model selection
edan-bainglass Nov 12, 2025
435d423
Restore serialization of node `extras`
edan-bainglass Nov 19, 2025
94cad38
Avoid using JSON Schema fields in `CreateModel` construction
edan-bainglass Nov 19, 2025
dfc5734
Fix `readOnly` logic in `MetadataField`
edan-bainglass Nov 19, 2025
c3f6c55
Cleanup `CreateModel` definition
edan-bainglass Nov 30, 2025
ce17b7b
Add serialization for `JsonSerializableProtocol` model field type
edan-bainglass Nov 30, 2025
440e49a
Refactor entity models out of entity classes
edan-bainglass Nov 30, 2025
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
24 changes: 11 additions & 13 deletions docs/source/nitpick-exceptions
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,16 @@ py:class json.encoder.JSONEncoder
py:class EXPOSED_TYPE
py:class EVENT_CALLBACK_TYPE
py:class datetime
py:class UUID
py:class types.LambdaType
py:meth tempfile.TemporaryDirectory

### AiiDA
py:class ReturnType
py:class SelfType
py:class CollectionType
py:class EntityCollectionType
py:class EntityType
py:class EntityModelType
py:class EntityClsType
py:class ProjectType
py:class BackendEntityType
Expand All @@ -50,8 +52,9 @@ py:class UserDefinedType
py:class LinkAnnotateType
py:obj ReturnType
py:obj SelfType
py:obj CollectionType
py:obj EntityCollectionType
py:obj EntityType
py:obj EntityModelType
py:obj BackendEntityType
py:obj BackendNodeType
py:obj TransactionType
Expand All @@ -61,21 +64,19 @@ py:class aiida.common.ReturnType
py:class aiida.common.SelfType
py:class aiida.common.lang.MethodType
py:class aiida.cmdline.params._shims.C
py:class aiida.orm.entitites.CollectionType
py:class aiida.orm.entitites.EntityType
py:class aiida.orm.entitites.BackendEntityType
py:class aiida.orm.groups.SelfType
py:class aiida.orm.implementation.entitites.EntityType
py:class aiida.engine.processes.functions.FunctionType
py:class aiida.engine.processes.workchains.workchain.MethodType
py:class aiida.orm.entities.EntityInputModel
py:class aiida.orm.entities.EntityType
py:class aiida.orm.entities.EntityModelType
py:class aiida.orm.entities.BackendEntityType
py:class aiida.orm.entities.CollectionType
py:class aiida.orm.entities.EntityCollectionType
py:class aiida.orm.implementation.nodes.BackendNodeType
py:class aiida.orm.implementation.storage_backend.TransactionType
py:class aiida.orm.implementation.entities.EntityType
py:class aiida.orm.nodes.data.enum.EnumType
py:class aiida.orm.nodes.NodeType
py:class aiida.orm.nodes.node.NodeType
py:class aiida.storage.psql_dos.orm.ModelType
py:class aiida.storage.psql_dos.orm.SelfType
py:class aiida.storage.psql_dos.orm.entities.SelfType
Expand All @@ -88,18 +89,15 @@ py:class aiida.common.lang.ReturnType
py:obj aiida.common.ReturnType
py:obj aiida.common.SelfType
py:obj aiida.common.lang.ReturnType
py:obj aiida.orm.entitites.CollectionType
py:obj aiida.orm.entitites.EntityType
py:obj aiida.orm.entitites.BackendEntityType
py:obj aiida.orm.groups.SelfType
py:obj aiida.orm.entities.CollectionType
py:obj aiida.orm.entities.EntityCollectionType
py:obj aiida.orm.entities.BackendEntityType
py:obj aiida.orm.entities.EntityType
py:obj aiida.orm.entities.EntityModelType
py:obj aiida.orm.implementation.entities.EntityType
py:obj aiida.orm.implementation.nodes.BackendNodeType
py:obj aiida.orm.implementation.storage_backend.TransactionType
py:obj aiida.orm.nodes.node.NodeType
py:obj aiida.orm.nodes.NodeType
py:obj aiida.storage.psql_dos.orm.ModelType
py:obj aiida.storage.psql_dos.orm.SelfType
py:obj aiida.storage.psql_dos.orm.entities.ModelType
Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -331,7 +331,7 @@ enable_error_code = [
'truthy-bool'
]
plugins = [
'pydantic.mypy',
# 'pydantic.mypy',
'sqlalchemy.ext.mypy.plugin'
]
scripts_are_modules = true
Expand Down Expand Up @@ -480,7 +480,7 @@ select = [
# Mark some classes as generic, per https://docs.astral.sh/ruff/settings/#lint_pyflakes_extend-generics
# Needed due to https://github.com/astral-sh/ruff/issues/9298
[tool.ruff.lint.pyflakes]
extend-generics = ["aiida.orm.entities.Entity", "aiida.orm.entities.Collection"]
extend-generics = ["aiida.orm.entities.Entity", "aiida.orm.entities.EntityCollection", "aiida.orm.entities.EntityModel"]

[tool.tox]
legacy_tox_ini = """
Expand Down
42 changes: 15 additions & 27 deletions src/aiida/cmdline/commands/cmd_code.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@
###########################################################################
"""`verdi code` command."""

from __future__ import annotations

import pathlib
import warnings
from collections import defaultdict
from functools import partial
from typing import Any
from typing import TYPE_CHECKING, Any

import click

Expand All @@ -26,16 +28,20 @@
from aiida.cmdline.utils.decorators import with_dbenv
from aiida.common import exceptions

if TYPE_CHECKING:
from aiida.orm import Code


@verdi.group('code')
def verdi_code():
"""Setup and manage codes."""


def create_code(ctx: click.Context, cls, **kwargs) -> None:
def create_code(ctx: click.Context, cls: Code, **kwargs) -> None:
"""Create a new `Code` instance."""
try:
instance = cls._from_model(cls.Model(**kwargs))
model = cls.CreateModel(**kwargs)
instance = cls.from_model(model) # type: ignore[arg-type]
except (TypeError, ValueError) as exception:
echo.echo_critical(f'Failed to create instance `{cls}`: {exception}')

Expand Down Expand Up @@ -226,7 +232,6 @@ def code_duplicate(ctx, code, non_interactive, **kwargs):
def show(code):
"""Display detailed information for a code."""
from aiida.cmdline import is_verbose
from aiida.common.pydantic import get_metadata

table = []

Expand All @@ -235,38 +240,21 @@ def show(code):
table.append(['UUID', code.uuid])
table.append(['Type', code.entry_point.name])

for field_name, field_info in code.Model.model_fields.items():
# Skip fields excluded from CLI
if get_metadata(
field_info,
key='exclude_from_cli',
default=False,
):
for field_name, field_info in code.CreateModel.model_fields.items():
# We don't show extras for codes
if field_name == 'extras':
continue

# Skip fields that are not stored in the attributes column
# NOTE: this also catches e.g., filepath_files for PortableCode, which is actually a "misuse"
# of the is_attribute metadata flag, as there it is flagging that the field is not stored at all!
# TODO (edan-bainglass) consider improving this by introducing a new metadata flag or reworking PortableCode
# TODO see also Dict and InstalledCode for other potential misuses of is_attribute
if not get_metadata(
field_info,
key='is_attribute',
default=True,
):
# FIXME resolve this hardcoded special case properly
if field_name == 'filepath_files':
continue

value = getattr(code, field_name)

# Special handling for computer field to show additional info
if field_name == 'computer':
value = f'{value.label} ({value.hostname}), pk: {value.pk}'

# Use the field's title as display name.
# This allows for custom titles (class-cased by default from Pydantic).
display_name = field_info.title

table.append([display_name, value])
table.append([field_info.title, value])

if is_verbose():
table.append(['Calculations', len(code.base.links.get_outgoing().all())])
Expand Down
12 changes: 7 additions & 5 deletions src/aiida/cmdline/groups/dynamic.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,8 +97,9 @@ def call_command(self, ctx: click.Context, cls: t.Any, non_interactive: bool, **

if hasattr(cls, 'Model'):
# The plugin defines a pydantic model: use it to validate the provided arguments
Model = getattr(cls, 'CreateModel', cls.Model) # noqa: N806
try:
cls.Model(**kwargs)
Model(**kwargs)
except ValidationError as exception:
param_hint = [
f'--{loc.replace("_", "-")}' # type: ignore[union-attr]
Expand Down Expand Up @@ -153,8 +154,6 @@ def list_options(self, entry_point: str) -> list[t.Callable[[FC], FC]]:
"""
from pydantic_core import PydanticUndefined

from aiida.common.pydantic import get_metadata

cls = self.factory(entry_point)

if not hasattr(cls, 'Model'):
Expand All @@ -168,10 +167,13 @@ def list_options(self, entry_point: str) -> list[t.Callable[[FC], FC]]:
options_spec = self.factory(entry_point).get_cli_options() # type: ignore[union-attr]
return [self.create_option(*item) for item in options_spec]

Model = getattr(cls, 'CreateModel', cls.Model) # noqa: N806

options_spec = {}

for key, field_info in cls.Model.model_fields.items():
if get_metadata(field_info, 'exclude_from_cli'):
for key, field_info in Model.model_fields.items():
# We do not prompt for extras
if key == 'extras':
continue

default = field_info.default_factory if field_info.default is PydanticUndefined else field_info.default
Expand Down
4 changes: 2 additions & 2 deletions src/aiida/common/datastructures.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,8 +167,8 @@ class CalcInfo(DefaultFieldsAttributeDict):
max_wallclock_seconds: None | int
max_memory_kb: None | int
rerunnable: bool
retrieve_list: None | list[str | tuple[str, str, str]]
retrieve_temporary_list: None | list[str | tuple[str, str, str]]
retrieve_list: None | list[str | tuple[str, str, int]]
retrieve_temporary_list: None | list[str | tuple[str, str, int]]
local_copy_list: None | list[tuple[str, str, str]]
remote_copy_list: None | list[tuple[str, str, str]]
remote_symlink_list: None | list[tuple[str, str, str]]
Expand Down
22 changes: 11 additions & 11 deletions src/aiida/common/pydantic.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
from __future__ import annotations

import typing as t
from pathlib import Path

from pydantic import Field
from pydantic_core import PydanticUndefined
Expand Down Expand Up @@ -34,11 +33,10 @@ def MetadataField( # noqa: N802
priority: int = 0,
short_name: str | None = None,
option_cls: t.Any | None = None,
orm_class: type[Entity[t.Any, t.Any]] | str | None = None,
orm_to_model: t.Callable[[Entity[t.Any, t.Any], Path], t.Any] | None = None,
model_to_orm: t.Callable[['BaseModel'], t.Any] | None = None,
orm_class: type[Entity[t.Any, t.Any, t.Any]] | str | None = None,
orm_to_model: t.Callable[[Entity[t.Any, t.Any, t.Any], dict[str, t.Any]], t.Any] | None = None,
model_to_orm: t.Callable[[BaseModel], t.Any] | None = None,
exclude_to_orm: bool = False,
exclude_from_cli: bool = False,
is_attribute: bool = True,
is_subscriptable: bool = False,
**kwargs: t.Any,
Expand Down Expand Up @@ -66,7 +64,7 @@ class Model(BaseModel):
:param short_name: Optional short name to use for an option on a command line interface.
:param option_cls: The :class:`click.Option` class to use to construct the option.
:param orm_class: The class, or entry point name thereof, to which the field should be converted. If this field is
defined, the value of this field should acccept an integer which will automatically be converted to an instance
defined, the value of this field should accept an integer which will automatically be converted to an instance
of said ORM class using ``orm_class.collection.get(id={field_value})``. This is useful, for example, where a
field represents an instance of a different entity, such as an instance of ``User``. The serialized data would
store the ``pk`` of the user, but the ORM entity instance would receive the actual ``User`` instance with that
Expand All @@ -75,11 +73,14 @@ class Model(BaseModel):
:param model_to_orm: Optional callable to convert the value of a field from a model instance to an ORM instance.
:param exclude_to_orm: When set to ``True``, this field value will not be passed to the ORM entity constructor
through ``Entity.from_model``.
:param exclude_from_cli: When set to ``True``, this field value will not be exposed on the CLI command that is
dynamically generated to create a new instance.
:param is_attribute: Whether the field is stored as an attribute.
:param is_subscriptable: Whether the field can be indexed like a list or dictionary.
:param is_attribute: Whether the field is stored as an attribute. Used by `QbFields`.
:param is_subscriptable: Whether the field can be indexed like a list or dictionary. Used by `QbFields`.
"""
if exclude_to_orm:
extra = kwargs.pop('json_schema_extra', {})
extra.update({'readOnly': True})
kwargs['json_schema_extra'] = extra

field_info = Field(default, **kwargs)

for key, value in (
Expand All @@ -90,7 +91,6 @@ class Model(BaseModel):
('orm_to_model', orm_to_model),
('model_to_orm', model_to_orm),
('exclude_to_orm', exclude_to_orm),
('exclude_from_cli', exclude_from_cli),
('is_attribute', is_attribute),
('is_subscriptable', is_subscriptable),
):
Expand Down
2 changes: 1 addition & 1 deletion src/aiida/orm/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,14 +45,14 @@
'CifData',
'Code',
'CodeEntityLoader',
'Collection',
'Comment',
'Computer',
'ComputerEntityLoader',
'ContainerizedCode',
'Data',
'Dict',
'Entity',
'EntityCollection',
'EntityExtras',
'EntityTypes',
'EnumData',
Expand Down
Loading
Loading