Skip to content

Commit

Permalink
Replication New API
Browse files Browse the repository at this point in the history
  • Loading branch information
themylogin committed Feb 17, 2025
1 parent f65e685 commit f7478aa
Show file tree
Hide file tree
Showing 10 changed files with 394 additions and 378 deletions.
4 changes: 2 additions & 2 deletions src/middlewared/middlewared/api/base/jsonschema.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ def get_json_schema(model):
schema = replace_refs(schema, schema.get("$defs", {}))
schema = add_attrs(schema)

return [schema["properties"][field] for field in model.model_fields]
return [schema["properties"][name] for name in model.schema_model_fields()]


def replace_refs(data, defs=None):
Expand All @@ -25,7 +25,7 @@ def replace_refs(data, defs=None):
def add_attrs(schema):
# FIXME: This is only here for backwards compatibility and should be removed eventually
if isinstance(schema, dict):
if isinstance(schema.get("properties"), dict):
if schema.get("type") == "object" and isinstance(schema.get("properties"), dict):
schema = {
**schema,
"_attrs_order_": list(schema["properties"].keys()),
Expand Down
13 changes: 12 additions & 1 deletion src/middlewared/middlewared/api/base/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

from pydantic import BaseModel as PydanticBaseModel, ConfigDict, create_model, Field, model_serializer, Secret
from pydantic._internal._model_construction import ModelMetaclass
from pydantic.json_schema import SkipJsonSchema
from pydantic.main import IncEx

from middlewared.api.base.types.string import SECRET_VALUE, LongStringWrapper
Expand All @@ -18,8 +19,10 @@


class _NotRequired:...
NotRequired = _NotRequired()


"""Use as the default value for fields that may be excluded from the model."""
NotRequired = _NotRequired()


class _NotRequiredMixin(PydanticBaseModel):
Expand Down Expand Up @@ -146,6 +149,14 @@ def _model_dump_fallback(self, context, value):

return value

@classmethod
def schema_model_fields(cls):
return {
name: field
for name, field in cls.model_fields.items()
if not any(isinstance(metadata, SkipJsonSchema) for metadata in field.metadata)
}

@classmethod
def from_previous(cls, value):
"""
Expand Down
2 changes: 1 addition & 1 deletion src/middlewared/middlewared/api/base/server/doc.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ def _dump_method_schemas(self, method: Method):
**accepts_json_schema["properties"][field],
"title": field,
}
for field in method.methodobj.new_style_accepts.model_fields
for field in method.methodobj.new_style_accepts.schema_model_fields()
],
"items": False,
},
Expand Down
1 change: 1 addition & 0 deletions src/middlewared/middlewared/api/base/types/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from .fc import * # noqa
from .filesystem import * # noqa
from .iscsi import * # noqa
from .list import * # noqa
from .network import * # noqa
from .string import * # noqa
from .urls import * # noqa
Expand Down
18 changes: 18 additions & 0 deletions src/middlewared/middlewared/api/base/types/list.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from typing import Annotated, Hashable, List, TypeVar

from pydantic_core import PydanticCustomError

from pydantic import AfterValidator, Field

__all__ = ['UniqueList']

T = TypeVar('T', bound=Hashable)


def _validate_unique_list(v: list[T]) -> list[T]:
if len(v) != len(set(v)):
raise PydanticCustomError('unique_list', 'List must be unique')
return v


UniqueList = Annotated[List[T], AfterValidator(_validate_unique_list), Field(json_schema_extra={'uniqueItems': True})]
1 change: 1 addition & 0 deletions src/middlewared/middlewared/api/v25_10_0/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
from .privilege import * # noqa
from .rdma import * # noqa
from .rdma_interface import * # noqa
from .replication import * # noqa
from .reporting import * # noqa
from .reporting_exporters import * # noqa
from .smartctl import * # noqa
Expand Down
39 changes: 31 additions & 8 deletions src/middlewared/middlewared/api/v25_10_0/common.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
from datetime import datetime, time
from typing import Annotated, Self

from middlewared.api.base import BaseModel
from middlewared.api.base import BaseModel, TimeString
from middlewared.utils import filters
from middlewared.utils.cron import croniter_for_schedule

from pydantic import AfterValidator, model_validator

__all__ = ["QueryFilters", "QueryOptions", "QueryArgs", "GenericQueryResult"]
__all__ = ["QueryFilters", "QueryOptions", "QueryArgs", "GenericQueryResult", "CronModel", "TimeCronModel"]

filter_obj = filters()

Expand Down Expand Up @@ -51,18 +52,18 @@ class GenericQueryResult(BaseModel):
class CronModel(BaseModel):
"""
Each field can either be a single value or a comma-separated list of values.
A \"*\" represents the full list of values.
A "*" represents the full list of values.
"""
minute: str = "*"
"""\"00\" - \"59\""""
""""00" - "59\""""
hour: str = "*"
"""\"00\" - \"23\""""
""""00" - "23\""""
dom: str = "*"
"""\"1\" - \"31\""""
""""1" - "31\""""
month: str = "*"
"""\"1\" (January) - \"12\" (December)"""
""""1" (January) - "12" (December)"""
dow: str = "*"
"""\"1\" (Monday) - \"7\" (Sunday)"""
""""1" (Monday) - "7" (Sunday)"""

@model_validator(mode="after")
def validate_attrs(self):
Expand All @@ -72,3 +73,25 @@ def validate_attrs(self):
raise ValueError(f"Please ensure fields match cron syntax - {e}")

return self


class TimeCronModel(CronModel):
begin: TimeString = "00:00"
end: TimeString = "23:59"

@model_validator(mode="after")
def validate_time(self):
begin = time(*map(int, self.begin.split(":")))
end = time(*map(int, self.begin.split(":")))

assert begin <= end, "Begin time should be less than or equal to end time"

iter_ = croniter_for_schedule(self.model_dump())
for i in range(24 * 60):
d = iter_.get_next(datetime)
if begin <= d.time() <= end:
break
else:
assert False, "Specified schedule does not match specified time interval"

return self
7 changes: 5 additions & 2 deletions src/middlewared/middlewared/api/v25_10_0/pool_snapshottask.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@


__all__ = [
"PoolSnapshotTaskEntry", "PoolSnapshotTaskCreateArgs", "PoolSnapshotTaskCreateResult",
"PoolSnapshotTaskDBEntry", "PoolSnapshotTaskEntry", "PoolSnapshotTaskCreateArgs", "PoolSnapshotTaskCreateResult",
"PoolSnapshotTaskUpdateArgs", "PoolSnapshotTaskUpdateResult", "PoolSnapshotTaskDeleteArgs",
"PoolSnapshotTaskDeleteResult", "PoolSnapshotTaskMaxCountArgs", "PoolSnapshotTaskMaxCountResult",
"PoolSnapshotTaskMaxTotalCountArgs", "PoolSnapshotTaskMaxTotalCountResult", "PoolSnapshotTaskRunArgs",
Expand Down Expand Up @@ -47,8 +47,11 @@ class PoolSnapshotTaskDeleteOptions(BaseModel):
fixate_removal_date: bool = False


class PoolSnapshotTaskEntry(PoolSnapshotTaskCreate):
class PoolSnapshotTaskDBEntry(PoolSnapshotTaskCreate):
id: int


class PoolSnapshotTaskEntry(PoolSnapshotTaskDBEntry):
vmware_sync: bool
state: Any

Expand Down
Loading

0 comments on commit f7478aa

Please sign in to comment.