diff --git a/AGENTS.md b/AGENTS.md index 898d2eb51..1e3a0b7ae 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -6,12 +6,13 @@ Thrive (codenamed Jupiter) is a life planning tool with a monorepo containing: -| Service | Port | Tech | -|---|---|---| -| **WebAPI** (backend) | 8004 | Python/FastAPI, SQLite | -| **Public API** | 8020 | Python/FastAPI (proxies to WebAPI) | -| **WebUI** (frontend) | 10020 | TypeScript/Remix/React | -| **Docs** | 8000 | Python/MkDocs | +| Service | Tech | +|---|---| +| **WebAPI** (backend) | Python/FastAPI, SQLite | +| **WebUI** (frontend) | TypeScript/Remix/React | +| **API** | Python/FastAPI | +| **MCP** | Python/FastAPI | +| **Docs** | Python/MkDocs | ### Tool versions @@ -26,7 +27,7 @@ mise run prepare ### Running services -Start all 4 services (WebAPI, API, WebUI, Docs) via mise: +Start all 5 services (WebAPI, API, MCP, WebUI, Docs) via mise: ```bash mise run run:srv --instance diff --git a/gen/py/webapi-client/jupiter_webapi_client/api/infra/__init__.py b/gen/py/webapi-client/jupiter_webapi_client/api/infra/__init__.py new file mode 100644 index 000000000..2d7c0b23d --- /dev/null +++ b/gen/py/webapi-client/jupiter_webapi_client/api/infra/__init__.py @@ -0,0 +1 @@ +"""Contains endpoint functions for accessing the API""" diff --git a/gen/py/webapi-client/jupiter_webapi_client/api/infra/get_entity_mutation_history.py b/gen/py/webapi-client/jupiter_webapi_client/api/infra/get_entity_mutation_history.py new file mode 100644 index 000000000..82b018d35 --- /dev/null +++ b/gen/py/webapi-client/jupiter_webapi_client/api/infra/get_entity_mutation_history.py @@ -0,0 +1,202 @@ +from http import HTTPStatus +from typing import Any + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.error_response import ErrorResponse +from ...models.get_entity_mutation_history_args import GetEntityMutationHistoryArgs +from ...models.get_entity_mutation_history_result import GetEntityMutationHistoryResult +from ...types import UNSET, Response, Unset + + +def _get_kwargs( + *, + body: GetEntityMutationHistoryArgs | Unset = UNSET, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/get-entity-mutation-history", + } + + if not isinstance(body, Unset): + _kwargs["json"] = body.to_dict() + + headers["Content-Type"] = "application/json" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> ErrorResponse | GetEntityMutationHistoryResult | None: + if response.status_code == 200: + response_200 = GetEntityMutationHistoryResult.from_dict(response.json()) + + return response_200 + + if response.status_code == 400: + response_400 = ErrorResponse.from_dict(response.json()) + + return response_400 + + if response.status_code == 401: + response_401 = ErrorResponse.from_dict(response.json()) + + return response_401 + + if response.status_code == 404: + response_404 = ErrorResponse.from_dict(response.json()) + + return response_404 + + if response.status_code == 406: + response_406 = ErrorResponse.from_dict(response.json()) + + return response_406 + + if response.status_code == 409: + response_409 = ErrorResponse.from_dict(response.json()) + + return response_409 + + if response.status_code == 410: + response_410 = ErrorResponse.from_dict(response.json()) + + return response_410 + + if response.status_code == 422: + response_422 = ErrorResponse.from_dict(response.json()) + + return response_422 + + if response.status_code == 426: + response_426 = ErrorResponse.from_dict(response.json()) + + return response_426 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[ErrorResponse | GetEntityMutationHistoryResult]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + *, + client: AuthenticatedClient, + body: GetEntityMutationHistoryArgs | Unset = UNSET, +) -> Response[ErrorResponse | GetEntityMutationHistoryResult]: + """Use case for loading the history of mutations for an entity. + + Args: + body (GetEntityMutationHistoryArgs | Unset): Arguments for the entity mutation history. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[ErrorResponse | GetEntityMutationHistoryResult] + """ + + kwargs = _get_kwargs( + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + *, + client: AuthenticatedClient, + body: GetEntityMutationHistoryArgs | Unset = UNSET, +) -> ErrorResponse | GetEntityMutationHistoryResult | None: + """Use case for loading the history of mutations for an entity. + + Args: + body (GetEntityMutationHistoryArgs | Unset): Arguments for the entity mutation history. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + ErrorResponse | GetEntityMutationHistoryResult + """ + + return sync_detailed( + client=client, + body=body, + ).parsed + + +async def asyncio_detailed( + *, + client: AuthenticatedClient, + body: GetEntityMutationHistoryArgs | Unset = UNSET, +) -> Response[ErrorResponse | GetEntityMutationHistoryResult]: + """Use case for loading the history of mutations for an entity. + + Args: + body (GetEntityMutationHistoryArgs | Unset): Arguments for the entity mutation history. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[ErrorResponse | GetEntityMutationHistoryResult] + """ + + kwargs = _get_kwargs( + body=body, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + *, + client: AuthenticatedClient, + body: GetEntityMutationHistoryArgs | Unset = UNSET, +) -> ErrorResponse | GetEntityMutationHistoryResult | None: + """Use case for loading the history of mutations for an entity. + + Args: + body (GetEntityMutationHistoryArgs | Unset): Arguments for the entity mutation history. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + ErrorResponse | GetEntityMutationHistoryResult + """ + + return ( + await asyncio_detailed( + client=client, + body=body, + ) + ).parsed diff --git a/gen/py/webapi-client/jupiter_webapi_client/api/infra/get_mutation_entity_events.py b/gen/py/webapi-client/jupiter_webapi_client/api/infra/get_mutation_entity_events.py new file mode 100644 index 000000000..ea129e95b --- /dev/null +++ b/gen/py/webapi-client/jupiter_webapi_client/api/infra/get_mutation_entity_events.py @@ -0,0 +1,206 @@ +from http import HTTPStatus +from typing import Any + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.error_response import ErrorResponse +from ...models.get_mutation_entity_events_args import GetMutationEntityEventsArgs +from ...models.get_mutation_entity_events_result import GetMutationEntityEventsResult +from ...types import UNSET, Response, Unset + + +def _get_kwargs( + *, + body: GetMutationEntityEventsArgs | Unset = UNSET, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/get-mutation-entity-events", + } + + if not isinstance(body, Unset): + _kwargs["json"] = body.to_dict() + + headers["Content-Type"] = "application/json" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> ErrorResponse | GetMutationEntityEventsResult | None: + if response.status_code == 200: + response_200 = GetMutationEntityEventsResult.from_dict(response.json()) + + return response_200 + + if response.status_code == 400: + response_400 = ErrorResponse.from_dict(response.json()) + + return response_400 + + if response.status_code == 401: + response_401 = ErrorResponse.from_dict(response.json()) + + return response_401 + + if response.status_code == 404: + response_404 = ErrorResponse.from_dict(response.json()) + + return response_404 + + if response.status_code == 406: + response_406 = ErrorResponse.from_dict(response.json()) + + return response_406 + + if response.status_code == 409: + response_409 = ErrorResponse.from_dict(response.json()) + + return response_409 + + if response.status_code == 410: + response_410 = ErrorResponse.from_dict(response.json()) + + return response_410 + + if response.status_code == 422: + response_422 = ErrorResponse.from_dict(response.json()) + + return response_422 + + if response.status_code == 426: + response_426 = ErrorResponse.from_dict(response.json()) + + return response_426 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[ErrorResponse | GetMutationEntityEventsResult]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + *, + client: AuthenticatedClient, + body: GetMutationEntityEventsArgs | Unset = UNSET, +) -> Response[ErrorResponse | GetMutationEntityEventsResult]: + """Use case for loading all entity events produced by a mutation. + + Args: + body (GetMutationEntityEventsArgs | Unset): Arguments for getting entity events from a + mutation. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[ErrorResponse | GetMutationEntityEventsResult] + """ + + kwargs = _get_kwargs( + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + *, + client: AuthenticatedClient, + body: GetMutationEntityEventsArgs | Unset = UNSET, +) -> ErrorResponse | GetMutationEntityEventsResult | None: + """Use case for loading all entity events produced by a mutation. + + Args: + body (GetMutationEntityEventsArgs | Unset): Arguments for getting entity events from a + mutation. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + ErrorResponse | GetMutationEntityEventsResult + """ + + return sync_detailed( + client=client, + body=body, + ).parsed + + +async def asyncio_detailed( + *, + client: AuthenticatedClient, + body: GetMutationEntityEventsArgs | Unset = UNSET, +) -> Response[ErrorResponse | GetMutationEntityEventsResult]: + """Use case for loading all entity events produced by a mutation. + + Args: + body (GetMutationEntityEventsArgs | Unset): Arguments for getting entity events from a + mutation. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[ErrorResponse | GetMutationEntityEventsResult] + """ + + kwargs = _get_kwargs( + body=body, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + *, + client: AuthenticatedClient, + body: GetMutationEntityEventsArgs | Unset = UNSET, +) -> ErrorResponse | GetMutationEntityEventsResult | None: + """Use case for loading all entity events produced by a mutation. + + Args: + body (GetMutationEntityEventsArgs | Unset): Arguments for getting entity events from a + mutation. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + ErrorResponse | GetMutationEntityEventsResult + """ + + return ( + await asyncio_detailed( + client=client, + body=body, + ) + ).parsed diff --git a/gen/py/webapi-client/jupiter_webapi_client/api/infra/get_mutation_invocation_history.py b/gen/py/webapi-client/jupiter_webapi_client/api/infra/get_mutation_invocation_history.py new file mode 100644 index 000000000..dc7053b1e --- /dev/null +++ b/gen/py/webapi-client/jupiter_webapi_client/api/infra/get_mutation_invocation_history.py @@ -0,0 +1,206 @@ +from http import HTTPStatus +from typing import Any + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.error_response import ErrorResponse +from ...models.get_mutation_invocation_history_args import GetMutationInvocationHistoryArgs +from ...models.get_mutation_invocation_history_result import GetMutationInvocationHistoryResult +from ...types import UNSET, Response, Unset + + +def _get_kwargs( + *, + body: GetMutationInvocationHistoryArgs | Unset = UNSET, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/get-mutation-invocation-history", + } + + if not isinstance(body, Unset): + _kwargs["json"] = body.to_dict() + + headers["Content-Type"] = "application/json" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> ErrorResponse | GetMutationInvocationHistoryResult | None: + if response.status_code == 200: + response_200 = GetMutationInvocationHistoryResult.from_dict(response.json()) + + return response_200 + + if response.status_code == 400: + response_400 = ErrorResponse.from_dict(response.json()) + + return response_400 + + if response.status_code == 401: + response_401 = ErrorResponse.from_dict(response.json()) + + return response_401 + + if response.status_code == 404: + response_404 = ErrorResponse.from_dict(response.json()) + + return response_404 + + if response.status_code == 406: + response_406 = ErrorResponse.from_dict(response.json()) + + return response_406 + + if response.status_code == 409: + response_409 = ErrorResponse.from_dict(response.json()) + + return response_409 + + if response.status_code == 410: + response_410 = ErrorResponse.from_dict(response.json()) + + return response_410 + + if response.status_code == 422: + response_422 = ErrorResponse.from_dict(response.json()) + + return response_422 + + if response.status_code == 426: + response_426 = ErrorResponse.from_dict(response.json()) + + return response_426 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[ErrorResponse | GetMutationInvocationHistoryResult]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + *, + client: AuthenticatedClient, + body: GetMutationInvocationHistoryArgs | Unset = UNSET, +) -> Response[ErrorResponse | GetMutationInvocationHistoryResult]: + """Use case for loading the history of mutation invocations for a user and workspace. + + Args: + body (GetMutationInvocationHistoryArgs | Unset): Arguments for the mutation invocation + history. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[ErrorResponse | GetMutationInvocationHistoryResult] + """ + + kwargs = _get_kwargs( + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + *, + client: AuthenticatedClient, + body: GetMutationInvocationHistoryArgs | Unset = UNSET, +) -> ErrorResponse | GetMutationInvocationHistoryResult | None: + """Use case for loading the history of mutation invocations for a user and workspace. + + Args: + body (GetMutationInvocationHistoryArgs | Unset): Arguments for the mutation invocation + history. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + ErrorResponse | GetMutationInvocationHistoryResult + """ + + return sync_detailed( + client=client, + body=body, + ).parsed + + +async def asyncio_detailed( + *, + client: AuthenticatedClient, + body: GetMutationInvocationHistoryArgs | Unset = UNSET, +) -> Response[ErrorResponse | GetMutationInvocationHistoryResult]: + """Use case for loading the history of mutation invocations for a user and workspace. + + Args: + body (GetMutationInvocationHistoryArgs | Unset): Arguments for the mutation invocation + history. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[ErrorResponse | GetMutationInvocationHistoryResult] + """ + + kwargs = _get_kwargs( + body=body, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + *, + client: AuthenticatedClient, + body: GetMutationInvocationHistoryArgs | Unset = UNSET, +) -> ErrorResponse | GetMutationInvocationHistoryResult | None: + """Use case for loading the history of mutation invocations for a user and workspace. + + Args: + body (GetMutationInvocationHistoryArgs | Unset): Arguments for the mutation invocation + history. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + ErrorResponse | GetMutationInvocationHistoryResult + """ + + return ( + await asyncio_detailed( + client=client, + body=body, + ) + ).parsed diff --git a/gen/py/webapi-client/jupiter_webapi_client/models/__init__.py b/gen/py/webapi-client/jupiter_webapi_client/models/__init__.py index bc9a1ed73..3bc1d5d25 100644 --- a/gen/py/webapi-client/jupiter_webapi_client/models/__init__.py +++ b/gen/py/webapi-client/jupiter_webapi_client/models/__init__.py @@ -235,6 +235,7 @@ from .env import Env from .error_detail_item import ErrorDetailItem from .error_response import ErrorResponse +from .event_entry import EventEntry from .feature_control import FeatureControl from .gc_do_all_args import GCDoAllArgs from .gc_do_args import GCDoArgs @@ -248,6 +249,12 @@ from .gen_load_runs_result import GenLoadRunsResult from .gen_log import GenLog from .gen_log_entry import GenLogEntry +from .get_entity_mutation_history_args import GetEntityMutationHistoryArgs +from .get_entity_mutation_history_result import GetEntityMutationHistoryResult +from .get_mutation_entity_events_args import GetMutationEntityEventsArgs +from .get_mutation_entity_events_result import GetMutationEntityEventsResult +from .get_mutation_invocation_history_args import GetMutationInvocationHistoryArgs +from .get_mutation_invocation_history_result import GetMutationInvocationHistoryResult from .get_summaries_args import GetSummariesArgs from .get_summaries_result import GetSummariesResult from .goal import Goal @@ -302,6 +309,7 @@ from .habit_update_args_skip_rule import HabitUpdateArgsSkipRule from .heading_block import HeadingBlock from .heading_block_kind import HeadingBlockKind +from .history_entry import HistoryEntry from .home_config import HomeConfig from .home_config_load_args import HomeConfigLoadArgs from .home_config_load_result import HomeConfigLoadResult @@ -353,6 +361,7 @@ from .inbox_tasks_summary import InboxTasksSummary from .init_args import InitArgs from .init_result import InitResult +from .invocation_history_entry import InvocationHistoryEntry from .journal import Journal from .journal_archive_args import JournalArchiveArgs from .journal_change_time_config_args import JournalChangeTimeConfigArgs @@ -1172,6 +1181,7 @@ "Env", "ErrorDetailItem", "ErrorResponse", + "EventEntry", "FeatureControl", "GCDoAllArgs", "GCDoArgs", @@ -1185,6 +1195,12 @@ "GenLoadRunsResult", "GenLog", "GenLogEntry", + "GetEntityMutationHistoryArgs", + "GetEntityMutationHistoryResult", + "GetMutationEntityEventsArgs", + "GetMutationEntityEventsResult", + "GetMutationInvocationHistoryArgs", + "GetMutationInvocationHistoryResult", "GetSummariesArgs", "GetSummariesResult", "Goal", @@ -1239,6 +1255,7 @@ "HabitUpdateArgsSkipRule", "HeadingBlock", "HeadingBlockKind", + "HistoryEntry", "HomeConfig", "HomeConfigLoadArgs", "HomeConfigLoadResult", @@ -1290,6 +1307,7 @@ "InboxTaskUpdateResult", "InitArgs", "InitResult", + "InvocationHistoryEntry", "Journal", "JournalArchiveArgs", "JournalChangeTimeConfigArgs", diff --git a/gen/py/webapi-client/jupiter_webapi_client/models/event_entry.py b/gen/py/webapi-client/jupiter_webapi_client/models/event_entry.py new file mode 100644 index 000000000..5af744e59 --- /dev/null +++ b/gen/py/webapi-client/jupiter_webapi_client/models/event_entry.py @@ -0,0 +1,118 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="EventEntry") + + +@_attrs_define +class EventEntry: + """A single entity event produced by a mutation. + + Attributes: + entity_name (str): + event_kind (str): + event_name (str): + timestamp (str): A timestamp in the application. + source (str): + user_ref_id (str): A generic entity id. + entity_version (int): + data (str): + """ + + entity_name: str + event_kind: str + event_name: str + timestamp: str + source: str + user_ref_id: str + entity_version: int + data: str + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + entity_name = self.entity_name + + event_kind = self.event_kind + + event_name = self.event_name + + timestamp = self.timestamp + + source = self.source + + user_ref_id = self.user_ref_id + + entity_version = self.entity_version + + data = self.data + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "entity_name": entity_name, + "event_kind": event_kind, + "event_name": event_name, + "timestamp": timestamp, + "source": source, + "user_ref_id": user_ref_id, + "entity_version": entity_version, + "data": data, + } + ) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + entity_name = d.pop("entity_name") + + event_kind = d.pop("event_kind") + + event_name = d.pop("event_name") + + timestamp = d.pop("timestamp") + + source = d.pop("source") + + user_ref_id = d.pop("user_ref_id") + + entity_version = d.pop("entity_version") + + data = d.pop("data") + + event_entry = cls( + entity_name=entity_name, + event_kind=event_kind, + event_name=event_name, + timestamp=timestamp, + source=source, + user_ref_id=user_ref_id, + entity_version=entity_version, + data=data, + ) + + event_entry.additional_properties = d + return event_entry + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/gen/py/webapi-client/jupiter_webapi_client/models/get_entity_mutation_history_args.py b/gen/py/webapi-client/jupiter_webapi_client/models/get_entity_mutation_history_args.py new file mode 100644 index 000000000..ffd5dc2ae --- /dev/null +++ b/gen/py/webapi-client/jupiter_webapi_client/models/get_entity_mutation_history_args.py @@ -0,0 +1,113 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..models.named_entity_tag import NamedEntityTag +from ..types import UNSET, Unset + +T = TypeVar("T", bound="GetEntityMutationHistoryArgs") + + +@_attrs_define +class GetEntityMutationHistoryArgs: + """Arguments for the entity mutation history. + + Attributes: + entity_type (NamedEntityTag): A tag for all known entities. + entity_ref_id (str): A generic entity id. + retrieve_offset (int | None | Unset): + retrieve_limit (int | None | Unset): + """ + + entity_type: NamedEntityTag + entity_ref_id: str + retrieve_offset: int | None | Unset = UNSET + retrieve_limit: int | None | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + entity_type = self.entity_type.value + + entity_ref_id = self.entity_ref_id + + retrieve_offset: int | None | Unset + if isinstance(self.retrieve_offset, Unset): + retrieve_offset = UNSET + else: + retrieve_offset = self.retrieve_offset + + retrieve_limit: int | None | Unset + if isinstance(self.retrieve_limit, Unset): + retrieve_limit = UNSET + else: + retrieve_limit = self.retrieve_limit + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "entity_type": entity_type, + "entity_ref_id": entity_ref_id, + } + ) + if retrieve_offset is not UNSET: + field_dict["retrieve_offset"] = retrieve_offset + if retrieve_limit is not UNSET: + field_dict["retrieve_limit"] = retrieve_limit + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + entity_type = NamedEntityTag(d.pop("entity_type")) + + entity_ref_id = d.pop("entity_ref_id") + + def _parse_retrieve_offset(data: object) -> int | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(int | None | Unset, data) + + retrieve_offset = _parse_retrieve_offset(d.pop("retrieve_offset", UNSET)) + + def _parse_retrieve_limit(data: object) -> int | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(int | None | Unset, data) + + retrieve_limit = _parse_retrieve_limit(d.pop("retrieve_limit", UNSET)) + + get_entity_mutation_history_args = cls( + entity_type=entity_type, + entity_ref_id=entity_ref_id, + retrieve_offset=retrieve_offset, + retrieve_limit=retrieve_limit, + ) + + get_entity_mutation_history_args.additional_properties = d + return get_entity_mutation_history_args + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/gen/py/webapi-client/jupiter_webapi_client/models/get_entity_mutation_history_result.py b/gen/py/webapi-client/jupiter_webapi_client/models/get_entity_mutation_history_result.py new file mode 100644 index 000000000..86ff6afab --- /dev/null +++ b/gen/py/webapi-client/jupiter_webapi_client/models/get_entity_mutation_history_result.py @@ -0,0 +1,110 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +if TYPE_CHECKING: + from ..models.history_entry import HistoryEntry + from ..models.user import User + + +T = TypeVar("T", bound="GetEntityMutationHistoryResult") + + +@_attrs_define +class GetEntityMutationHistoryResult: + """Results for the entity mutation history. + + Attributes: + entries (list[HistoryEntry]): + users (list[User]): + total_cnt (int): + page_size (int): + """ + + entries: list[HistoryEntry] + users: list[User] + total_cnt: int + page_size: int + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + entries = [] + for entries_item_data in self.entries: + entries_item = entries_item_data.to_dict() + entries.append(entries_item) + + users = [] + for users_item_data in self.users: + users_item = users_item_data.to_dict() + users.append(users_item) + + total_cnt = self.total_cnt + + page_size = self.page_size + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "entries": entries, + "users": users, + "total_cnt": total_cnt, + "page_size": page_size, + } + ) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.history_entry import HistoryEntry + from ..models.user import User + + d = dict(src_dict) + entries = [] + _entries = d.pop("entries") + for entries_item_data in _entries: + entries_item = HistoryEntry.from_dict(entries_item_data) + + entries.append(entries_item) + + users = [] + _users = d.pop("users") + for users_item_data in _users: + users_item = User.from_dict(users_item_data) + + users.append(users_item) + + total_cnt = d.pop("total_cnt") + + page_size = d.pop("page_size") + + get_entity_mutation_history_result = cls( + entries=entries, + users=users, + total_cnt=total_cnt, + page_size=page_size, + ) + + get_entity_mutation_history_result.additional_properties = d + return get_entity_mutation_history_result + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/gen/py/webapi-client/jupiter_webapi_client/models/get_mutation_entity_events_args.py b/gen/py/webapi-client/jupiter_webapi_client/models/get_mutation_entity_events_args.py new file mode 100644 index 000000000..62b7d1d19 --- /dev/null +++ b/gen/py/webapi-client/jupiter_webapi_client/models/get_mutation_entity_events_args.py @@ -0,0 +1,62 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="GetMutationEntityEventsArgs") + + +@_attrs_define +class GetMutationEntityEventsArgs: + """Arguments for getting entity events from a mutation. + + Attributes: + mutation_id (str): A mutation id for a particular user action. + """ + + mutation_id: str + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + mutation_id = self.mutation_id + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "mutation_id": mutation_id, + } + ) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + mutation_id = d.pop("mutation_id") + + get_mutation_entity_events_args = cls( + mutation_id=mutation_id, + ) + + get_mutation_entity_events_args.additional_properties = d + return get_mutation_entity_events_args + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/gen/py/webapi-client/jupiter_webapi_client/models/get_mutation_entity_events_result.py b/gen/py/webapi-client/jupiter_webapi_client/models/get_mutation_entity_events_result.py new file mode 100644 index 000000000..665376e44 --- /dev/null +++ b/gen/py/webapi-client/jupiter_webapi_client/models/get_mutation_entity_events_result.py @@ -0,0 +1,102 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +if TYPE_CHECKING: + from ..models.event_entry import EventEntry + from ..models.user import User + + +T = TypeVar("T", bound="GetMutationEntityEventsResult") + + +@_attrs_define +class GetMutationEntityEventsResult: + """Results for the mutation entity events. + + Attributes: + mutation_name (str): + entries (list[EventEntry]): + users (list[User]): + """ + + mutation_name: str + entries: list[EventEntry] + users: list[User] + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + mutation_name = self.mutation_name + + entries = [] + for entries_item_data in self.entries: + entries_item = entries_item_data.to_dict() + entries.append(entries_item) + + users = [] + for users_item_data in self.users: + users_item = users_item_data.to_dict() + users.append(users_item) + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "mutation_name": mutation_name, + "entries": entries, + "users": users, + } + ) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.event_entry import EventEntry + from ..models.user import User + + d = dict(src_dict) + mutation_name = d.pop("mutation_name") + + entries = [] + _entries = d.pop("entries") + for entries_item_data in _entries: + entries_item = EventEntry.from_dict(entries_item_data) + + entries.append(entries_item) + + users = [] + _users = d.pop("users") + for users_item_data in _users: + users_item = User.from_dict(users_item_data) + + users.append(users_item) + + get_mutation_entity_events_result = cls( + mutation_name=mutation_name, + entries=entries, + users=users, + ) + + get_mutation_entity_events_result.additional_properties = d + return get_mutation_entity_events_result + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/gen/py/webapi-client/jupiter_webapi_client/models/get_mutation_invocation_history_args.py b/gen/py/webapi-client/jupiter_webapi_client/models/get_mutation_invocation_history_args.py new file mode 100644 index 000000000..b71f369c5 --- /dev/null +++ b/gen/py/webapi-client/jupiter_webapi_client/models/get_mutation_invocation_history_args.py @@ -0,0 +1,94 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +T = TypeVar("T", bound="GetMutationInvocationHistoryArgs") + + +@_attrs_define +class GetMutationInvocationHistoryArgs: + """Arguments for the mutation invocation history. + + Attributes: + retrieve_offset (int | None | Unset): + retrieve_limit (int | None | Unset): + """ + + retrieve_offset: int | None | Unset = UNSET + retrieve_limit: int | None | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + retrieve_offset: int | None | Unset + if isinstance(self.retrieve_offset, Unset): + retrieve_offset = UNSET + else: + retrieve_offset = self.retrieve_offset + + retrieve_limit: int | None | Unset + if isinstance(self.retrieve_limit, Unset): + retrieve_limit = UNSET + else: + retrieve_limit = self.retrieve_limit + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({}) + if retrieve_offset is not UNSET: + field_dict["retrieve_offset"] = retrieve_offset + if retrieve_limit is not UNSET: + field_dict["retrieve_limit"] = retrieve_limit + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + + def _parse_retrieve_offset(data: object) -> int | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(int | None | Unset, data) + + retrieve_offset = _parse_retrieve_offset(d.pop("retrieve_offset", UNSET)) + + def _parse_retrieve_limit(data: object) -> int | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(int | None | Unset, data) + + retrieve_limit = _parse_retrieve_limit(d.pop("retrieve_limit", UNSET)) + + get_mutation_invocation_history_args = cls( + retrieve_offset=retrieve_offset, + retrieve_limit=retrieve_limit, + ) + + get_mutation_invocation_history_args.additional_properties = d + return get_mutation_invocation_history_args + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/gen/py/webapi-client/jupiter_webapi_client/models/get_mutation_invocation_history_result.py b/gen/py/webapi-client/jupiter_webapi_client/models/get_mutation_invocation_history_result.py new file mode 100644 index 000000000..e7e1fc549 --- /dev/null +++ b/gen/py/webapi-client/jupiter_webapi_client/models/get_mutation_invocation_history_result.py @@ -0,0 +1,110 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +if TYPE_CHECKING: + from ..models.invocation_history_entry import InvocationHistoryEntry + from ..models.user import User + + +T = TypeVar("T", bound="GetMutationInvocationHistoryResult") + + +@_attrs_define +class GetMutationInvocationHistoryResult: + """Results for the mutation invocation history. + + Attributes: + entries (list[InvocationHistoryEntry]): + users (list[User]): + total_cnt (int): + page_size (int): + """ + + entries: list[InvocationHistoryEntry] + users: list[User] + total_cnt: int + page_size: int + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + entries = [] + for entries_item_data in self.entries: + entries_item = entries_item_data.to_dict() + entries.append(entries_item) + + users = [] + for users_item_data in self.users: + users_item = users_item_data.to_dict() + users.append(users_item) + + total_cnt = self.total_cnt + + page_size = self.page_size + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "entries": entries, + "users": users, + "total_cnt": total_cnt, + "page_size": page_size, + } + ) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.invocation_history_entry import InvocationHistoryEntry + from ..models.user import User + + d = dict(src_dict) + entries = [] + _entries = d.pop("entries") + for entries_item_data in _entries: + entries_item = InvocationHistoryEntry.from_dict(entries_item_data) + + entries.append(entries_item) + + users = [] + _users = d.pop("users") + for users_item_data in _users: + users_item = User.from_dict(users_item_data) + + users.append(users_item) + + total_cnt = d.pop("total_cnt") + + page_size = d.pop("page_size") + + get_mutation_invocation_history_result = cls( + entries=entries, + users=users, + total_cnt=total_cnt, + page_size=page_size, + ) + + get_mutation_invocation_history_result.additional_properties = d + return get_mutation_invocation_history_result + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/gen/py/webapi-client/jupiter_webapi_client/models/history_entry.py b/gen/py/webapi-client/jupiter_webapi_client/models/history_entry.py new file mode 100644 index 000000000..1e0460efb --- /dev/null +++ b/gen/py/webapi-client/jupiter_webapi_client/models/history_entry.py @@ -0,0 +1,134 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="HistoryEntry") + + +@_attrs_define +class HistoryEntry: + """An instance of the history. + + Attributes: + mutation_id (str): A mutation id for a particular user action. + entity_name (str): + mutation_name (str): + event_kind (str): + event_name (str): + timestamp (str): A timestamp in the application. + source (str): + user_ref_id (str): A generic entity id. + entity_version (int): + data (str): + """ + + mutation_id: str + entity_name: str + mutation_name: str + event_kind: str + event_name: str + timestamp: str + source: str + user_ref_id: str + entity_version: int + data: str + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + mutation_id = self.mutation_id + + entity_name = self.entity_name + + mutation_name = self.mutation_name + + event_kind = self.event_kind + + event_name = self.event_name + + timestamp = self.timestamp + + source = self.source + + user_ref_id = self.user_ref_id + + entity_version = self.entity_version + + data = self.data + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "mutation_id": mutation_id, + "entity_name": entity_name, + "mutation_name": mutation_name, + "event_kind": event_kind, + "event_name": event_name, + "timestamp": timestamp, + "source": source, + "user_ref_id": user_ref_id, + "entity_version": entity_version, + "data": data, + } + ) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + mutation_id = d.pop("mutation_id") + + entity_name = d.pop("entity_name") + + mutation_name = d.pop("mutation_name") + + event_kind = d.pop("event_kind") + + event_name = d.pop("event_name") + + timestamp = d.pop("timestamp") + + source = d.pop("source") + + user_ref_id = d.pop("user_ref_id") + + entity_version = d.pop("entity_version") + + data = d.pop("data") + + history_entry = cls( + mutation_id=mutation_id, + entity_name=entity_name, + mutation_name=mutation_name, + event_kind=event_kind, + event_name=event_name, + timestamp=timestamp, + source=source, + user_ref_id=user_ref_id, + entity_version=entity_version, + data=data, + ) + + history_entry.additional_properties = d + return history_entry + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/gen/py/webapi-client/jupiter_webapi_client/models/invocation_history_entry.py b/gen/py/webapi-client/jupiter_webapi_client/models/invocation_history_entry.py new file mode 100644 index 000000000..1d6c2842a --- /dev/null +++ b/gen/py/webapi-client/jupiter_webapi_client/models/invocation_history_entry.py @@ -0,0 +1,132 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +T = TypeVar("T", bound="InvocationHistoryEntry") + + +@_attrs_define +class InvocationHistoryEntry: + """A single mutation invocation history entry. + + Attributes: + mutation_id (str): A mutation id for a particular user action. + mutation_name (str): + timestamp (str): A timestamp in the application. + source (str): + user_ref_id (str): A generic entity id. + result (str): + args_str (str): + error_str (None | str | Unset): + """ + + mutation_id: str + mutation_name: str + timestamp: str + source: str + user_ref_id: str + result: str + args_str: str + error_str: None | str | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + mutation_id = self.mutation_id + + mutation_name = self.mutation_name + + timestamp = self.timestamp + + source = self.source + + user_ref_id = self.user_ref_id + + result = self.result + + args_str = self.args_str + + error_str: None | str | Unset + if isinstance(self.error_str, Unset): + error_str = UNSET + else: + error_str = self.error_str + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "mutation_id": mutation_id, + "mutation_name": mutation_name, + "timestamp": timestamp, + "source": source, + "user_ref_id": user_ref_id, + "result": result, + "args_str": args_str, + } + ) + if error_str is not UNSET: + field_dict["error_str"] = error_str + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + mutation_id = d.pop("mutation_id") + + mutation_name = d.pop("mutation_name") + + timestamp = d.pop("timestamp") + + source = d.pop("source") + + user_ref_id = d.pop("user_ref_id") + + result = d.pop("result") + + args_str = d.pop("args_str") + + def _parse_error_str(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + error_str = _parse_error_str(d.pop("error_str", UNSET)) + + invocation_history_entry = cls( + mutation_id=mutation_id, + mutation_name=mutation_name, + timestamp=timestamp, + source=source, + user_ref_id=user_ref_id, + result=result, + args_str=args_str, + error_str=error_str, + ) + + invocation_history_entry.additional_properties = d + return invocation_history_entry + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/gen/ts/webapi-client/gen/ApiClient.ts b/gen/ts/webapi-client/gen/ApiClient.ts index 4e7d6fbae..dd1c3e742 100644 --- a/gen/ts/webapi-client/gen/ApiClient.ts +++ b/gen/ts/webapi-client/gen/ApiClient.ts @@ -18,6 +18,7 @@ import { GenService } from './services/GenService'; import { HabitsService } from './services/HabitsService'; import { HomeService } from './services/HomeService'; import { InboxTasksService } from './services/InboxTasksService'; +import { InfraService } from './services/InfraService'; import { JournalsService } from './services/JournalsService'; import { LifePlanService } from './services/LifePlanService'; import { McpKeyService } from './services/McpKeyService'; @@ -55,6 +56,7 @@ export class ApiClient { public readonly habits: HabitsService; public readonly home: HomeService; public readonly inboxTasks: InboxTasksService; + public readonly infra: InfraService; public readonly journals: JournalsService; public readonly lifePlan: LifePlanService; public readonly mcpKey: McpKeyService; @@ -103,6 +105,7 @@ export class ApiClient { this.habits = new HabitsService(this.request); this.home = new HomeService(this.request); this.inboxTasks = new InboxTasksService(this.request); + this.infra = new InfraService(this.request); this.journals = new JournalsService(this.request); this.lifePlan = new LifePlanService(this.request); this.mcpKey = new McpKeyService(this.request); diff --git a/gen/ts/webapi-client/gen/index.ts b/gen/ts/webapi-client/gen/index.ts index 6676636d2..cfebc42fc 100644 --- a/gen/ts/webapi-client/gen/index.ts +++ b/gen/ts/webapi-client/gen/index.ts @@ -195,6 +195,7 @@ export type { EntitySummary } from './models/EntitySummary'; export { Env } from './models/Env'; export type { ErrorDetailItem } from './models/ErrorDetailItem'; export type { ErrorResponse } from './models/ErrorResponse'; +export type { EventEntry } from './models/EventEntry'; export { FeatureControl } from './models/FeatureControl'; export type { GCDoAllArgs } from './models/GCDoAllArgs'; export type { GCDoArgs } from './models/GCDoArgs'; @@ -208,6 +209,12 @@ export type { GenLoadRunsArgs } from './models/GenLoadRunsArgs'; export type { GenLoadRunsResult } from './models/GenLoadRunsResult'; export type { GenLog } from './models/GenLog'; export type { GenLogEntry } from './models/GenLogEntry'; +export type { GetEntityMutationHistoryArgs } from './models/GetEntityMutationHistoryArgs'; +export type { GetEntityMutationHistoryResult } from './models/GetEntityMutationHistoryResult'; +export type { GetMutationEntityEventsArgs } from './models/GetMutationEntityEventsArgs'; +export type { GetMutationEntityEventsResult } from './models/GetMutationEntityEventsResult'; +export type { GetMutationInvocationHistoryArgs } from './models/GetMutationInvocationHistoryArgs'; +export type { GetMutationInvocationHistoryResult } from './models/GetMutationInvocationHistoryResult'; export type { GetSummariesArgs } from './models/GetSummariesArgs'; export type { GetSummariesResult } from './models/GetSummariesResult'; export type { Goal } from './models/Goal'; @@ -244,6 +251,7 @@ export type { HabitSuspendArgs } from './models/HabitSuspendArgs'; export type { HabitUnsuspendArgs } from './models/HabitUnsuspendArgs'; export type { HabitUpdateArgs } from './models/HabitUpdateArgs'; export { HeadingBlock } from './models/HeadingBlock'; +export type { HistoryEntry } from './models/HistoryEntry'; export type { HomeConfig } from './models/HomeConfig'; export type { HomeConfigLoadArgs } from './models/HomeConfigLoadArgs'; export type { HomeConfigLoadResult } from './models/HomeConfigLoadResult'; @@ -286,6 +294,7 @@ export type { InboxTaskUpdateResult } from './models/InboxTaskUpdateResult'; export type { InitArgs } from './models/InitArgs'; export type { InitResult } from './models/InitResult'; export type { Instance } from './models/Instance'; +export type { InvocationHistoryEntry } from './models/InvocationHistoryEntry'; export type { Journal } from './models/Journal'; export type { JournalArchiveArgs } from './models/JournalArchiveArgs'; export type { JournalChangeTimeConfigArgs } from './models/JournalChangeTimeConfigArgs'; @@ -383,6 +392,7 @@ export type { MilestoneUpdateArgs } from './models/MilestoneUpdateArgs'; export type { MOTD } from './models/MOTD'; export type { MOTDGetForTodayArgs } from './models/MOTDGetForTodayArgs'; export type { MOTDGetForTodayResult } from './models/MOTDGetForTodayResult'; +export type { MutationId } from './models/MutationId'; export { NamedEntityTag } from './models/NamedEntityTag'; export type { NestedResult } from './models/NestedResult'; export type { NestedResultPerSource } from './models/NestedResultPerSource'; @@ -691,6 +701,7 @@ export type { TodoTaskRemoveArgs } from './models/TodoTaskRemoveArgs'; export type { TodoTaskSummary } from './models/TodoTaskSummary'; export type { TodoTaskUpdateArgs } from './models/TodoTaskUpdateArgs'; export type { TodoTaskUpdateResult } from './models/TodoTaskUpdateResult'; +export type { TraceId } from './models/TraceId'; export type { Universe } from './models/Universe'; export type { URL } from './models/URL'; export type { User } from './models/User'; @@ -777,6 +788,7 @@ export { GenService } from './services/GenService'; export { HabitsService } from './services/HabitsService'; export { HomeService } from './services/HomeService'; export { InboxTasksService } from './services/InboxTasksService'; +export { InfraService } from './services/InfraService'; export { JournalsService } from './services/JournalsService'; export { LifePlanService } from './services/LifePlanService'; export { McpKeyService } from './services/McpKeyService'; diff --git a/gen/ts/webapi-client/gen/models/EventEntry.ts b/gen/ts/webapi-client/gen/models/EventEntry.ts new file mode 100644 index 000000000..1c7a85e69 --- /dev/null +++ b/gen/ts/webapi-client/gen/models/EventEntry.ts @@ -0,0 +1,20 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { EntityId } from './EntityId'; +import type { Timestamp } from './Timestamp'; +/** + * A single entity event produced by a mutation. + */ +export type EventEntry = { + entity_name: string; + event_kind: string; + event_name: string; + timestamp: Timestamp; + source: string; + user_ref_id: EntityId; + entity_version: number; + data: string; +}; + diff --git a/gen/ts/webapi-client/gen/models/GetEntityMutationHistoryArgs.ts b/gen/ts/webapi-client/gen/models/GetEntityMutationHistoryArgs.ts new file mode 100644 index 000000000..97ff1f1e9 --- /dev/null +++ b/gen/ts/webapi-client/gen/models/GetEntityMutationHistoryArgs.ts @@ -0,0 +1,16 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { EntityId } from './EntityId'; +import type { NamedEntityTag } from './NamedEntityTag'; +/** + * Arguments for the entity mutation history. + */ +export type GetEntityMutationHistoryArgs = { + entity_type: NamedEntityTag; + entity_ref_id: EntityId; + retrieve_offset?: (number | null); + retrieve_limit?: (number | null); +}; + diff --git a/gen/ts/webapi-client/gen/models/GetEntityMutationHistoryResult.ts b/gen/ts/webapi-client/gen/models/GetEntityMutationHistoryResult.ts new file mode 100644 index 000000000..88f8cfd0e --- /dev/null +++ b/gen/ts/webapi-client/gen/models/GetEntityMutationHistoryResult.ts @@ -0,0 +1,16 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { HistoryEntry } from './HistoryEntry'; +import type { User } from './User'; +/** + * Results for the entity mutation history. + */ +export type GetEntityMutationHistoryResult = { + entries: Array; + users: Array; + total_cnt: number; + page_size: number; +}; + diff --git a/gen/ts/webapi-client/gen/models/GetMutationEntityEventsArgs.ts b/gen/ts/webapi-client/gen/models/GetMutationEntityEventsArgs.ts new file mode 100644 index 000000000..7cc85a85e --- /dev/null +++ b/gen/ts/webapi-client/gen/models/GetMutationEntityEventsArgs.ts @@ -0,0 +1,12 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { MutationId } from './MutationId'; +/** + * Arguments for getting entity events from a mutation. + */ +export type GetMutationEntityEventsArgs = { + mutation_id: MutationId; +}; + diff --git a/gen/ts/webapi-client/gen/models/GetMutationEntityEventsResult.ts b/gen/ts/webapi-client/gen/models/GetMutationEntityEventsResult.ts new file mode 100644 index 000000000..3e4dee4d1 --- /dev/null +++ b/gen/ts/webapi-client/gen/models/GetMutationEntityEventsResult.ts @@ -0,0 +1,15 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { EventEntry } from './EventEntry'; +import type { User } from './User'; +/** + * Results for the mutation entity events. + */ +export type GetMutationEntityEventsResult = { + mutation_name: string; + entries: Array; + users: Array; +}; + diff --git a/gen/ts/webapi-client/gen/models/GetMutationInvocationHistoryArgs.ts b/gen/ts/webapi-client/gen/models/GetMutationInvocationHistoryArgs.ts new file mode 100644 index 000000000..117f701b5 --- /dev/null +++ b/gen/ts/webapi-client/gen/models/GetMutationInvocationHistoryArgs.ts @@ -0,0 +1,12 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +/** + * Arguments for the mutation invocation history. + */ +export type GetMutationInvocationHistoryArgs = { + retrieve_offset?: (number | null); + retrieve_limit?: (number | null); +}; + diff --git a/gen/ts/webapi-client/gen/models/GetMutationInvocationHistoryResult.ts b/gen/ts/webapi-client/gen/models/GetMutationInvocationHistoryResult.ts new file mode 100644 index 000000000..e2d976049 --- /dev/null +++ b/gen/ts/webapi-client/gen/models/GetMutationInvocationHistoryResult.ts @@ -0,0 +1,16 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { InvocationHistoryEntry } from './InvocationHistoryEntry'; +import type { User } from './User'; +/** + * Results for the mutation invocation history. + */ +export type GetMutationInvocationHistoryResult = { + entries: Array; + users: Array; + total_cnt: number; + page_size: number; +}; + diff --git a/gen/ts/webapi-client/gen/models/HistoryEntry.ts b/gen/ts/webapi-client/gen/models/HistoryEntry.ts new file mode 100644 index 000000000..aece86ef2 --- /dev/null +++ b/gen/ts/webapi-client/gen/models/HistoryEntry.ts @@ -0,0 +1,23 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { EntityId } from './EntityId'; +import type { MutationId } from './MutationId'; +import type { Timestamp } from './Timestamp'; +/** + * An instance of the history. + */ +export type HistoryEntry = { + mutation_id: MutationId; + entity_name: string; + mutation_name: string; + event_kind: string; + event_name: string; + timestamp: Timestamp; + source: string; + user_ref_id: EntityId; + entity_version: number; + data: string; +}; + diff --git a/gen/ts/webapi-client/gen/models/InvocationHistoryEntry.ts b/gen/ts/webapi-client/gen/models/InvocationHistoryEntry.ts new file mode 100644 index 000000000..908bd9136 --- /dev/null +++ b/gen/ts/webapi-client/gen/models/InvocationHistoryEntry.ts @@ -0,0 +1,21 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { EntityId } from './EntityId'; +import type { MutationId } from './MutationId'; +import type { Timestamp } from './Timestamp'; +/** + * A single mutation invocation history entry. + */ +export type InvocationHistoryEntry = { + mutation_id: MutationId; + mutation_name: string; + timestamp: Timestamp; + source: string; + user_ref_id: EntityId; + result: string; + args_str: string; + error_str?: (string | null); +}; + diff --git a/gen/ts/webapi-client/gen/models/MutationId.ts b/gen/ts/webapi-client/gen/models/MutationId.ts new file mode 100644 index 000000000..b4f7e07c5 --- /dev/null +++ b/gen/ts/webapi-client/gen/models/MutationId.ts @@ -0,0 +1,8 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +/** + * A mutation id for a particular user action. + */ +export type MutationId = string; diff --git a/gen/ts/webapi-client/gen/models/TraceId.ts b/gen/ts/webapi-client/gen/models/TraceId.ts new file mode 100644 index 000000000..349b34c0d --- /dev/null +++ b/gen/ts/webapi-client/gen/models/TraceId.ts @@ -0,0 +1,8 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +/** + * A trace id for a particular user action. + */ +export type TraceId = string; diff --git a/gen/ts/webapi-client/gen/services/InfraService.ts b/gen/ts/webapi-client/gen/services/InfraService.ts new file mode 100644 index 000000000..2b88f8636 --- /dev/null +++ b/gen/ts/webapi-client/gen/services/InfraService.ts @@ -0,0 +1,93 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { GetEntityMutationHistoryArgs } from '../models/GetEntityMutationHistoryArgs'; +import type { GetEntityMutationHistoryResult } from '../models/GetEntityMutationHistoryResult'; +import type { GetMutationEntityEventsArgs } from '../models/GetMutationEntityEventsArgs'; +import type { GetMutationEntityEventsResult } from '../models/GetMutationEntityEventsResult'; +import type { GetMutationInvocationHistoryArgs } from '../models/GetMutationInvocationHistoryArgs'; +import type { GetMutationInvocationHistoryResult } from '../models/GetMutationInvocationHistoryResult'; +import type { CancelablePromise } from '../core/CancelablePromise'; +import type { BaseHttpRequest } from '../core/BaseHttpRequest'; +export class InfraService { + constructor(public readonly httpRequest: BaseHttpRequest) {} + /** + * Use case for loading the history of mutations for an entity. + * @param requestBody The input data + * @returns GetEntityMutationHistoryResult Successful response + * @throws ApiError + */ + public getEntityMutationHistory( + requestBody?: GetEntityMutationHistoryArgs, + ): CancelablePromise { + return this.httpRequest.request({ + method: 'POST', + url: '/get-entity-mutation-history', + body: requestBody, + mediaType: 'application/json', + errors: { + 400: `Error response for EntityAlreadyExistsError`, + 401: `Error response for ExpiredAuthTokenError`, + 404: `Error response for EntityNotFoundError`, + 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, + 409: `Error response for TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, + 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, InvalidLoginCredentialsError, InvalidAPIKeyError, AspectInSignificantUseError, ContactInSignificantUseError`, + 426: `Error response for InvalidAuthTokenError`, + }, + }); + } + /** + * Use case for loading all entity events produced by a mutation. + * @param requestBody The input data + * @returns GetMutationEntityEventsResult Successful response + * @throws ApiError + */ + public getMutationEntityEvents( + requestBody?: GetMutationEntityEventsArgs, + ): CancelablePromise { + return this.httpRequest.request({ + method: 'POST', + url: '/get-mutation-entity-events', + body: requestBody, + mediaType: 'application/json', + errors: { + 400: `Error response for EntityAlreadyExistsError`, + 401: `Error response for ExpiredAuthTokenError`, + 404: `Error response for EntityNotFoundError`, + 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, + 409: `Error response for TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, + 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, InvalidLoginCredentialsError, InvalidAPIKeyError, AspectInSignificantUseError, ContactInSignificantUseError`, + 426: `Error response for InvalidAuthTokenError`, + }, + }); + } + /** + * Use case for loading the history of mutation invocations for a user and workspace. + * @param requestBody The input data + * @returns GetMutationInvocationHistoryResult Successful response + * @throws ApiError + */ + public getMutationInvocationHistory( + requestBody?: GetMutationInvocationHistoryArgs, + ): CancelablePromise { + return this.httpRequest.request({ + method: 'POST', + url: '/get-mutation-invocation-history', + body: requestBody, + mediaType: 'application/json', + errors: { + 400: `Error response for EntityAlreadyExistsError`, + 401: `Error response for ExpiredAuthTokenError`, + 404: `Error response for EntityNotFoundError`, + 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, + 409: `Error response for TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, + 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, InvalidLoginCredentialsError, InvalidAPIKeyError, AspectInSignificantUseError, ContactInSignificantUseError`, + 426: `Error response for InvalidAuthTokenError`, + }, + }); + } +} diff --git a/itests/package.mise.toml b/itests/package.mise.toml index b8121a642..04db6175b 100644 --- a/itests/package.mise.toml +++ b/itests/package.mise.toml @@ -2,7 +2,7 @@ hide = true run = ''' #!/usr/bin/env bash -sudo playwright install-deps +playwright install-deps playwright install ''' diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 84241ca00..f7e1fcd7c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -250,8 +250,8 @@ importers: specifier: ^3.1.0 version: 3.1.0 vite: - specifier: ^6.2.2 - version: 6.4.1(@types/node@24.10.0)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.1) + specifier: ^6.4.2 + version: 6.4.2(@types/node@24.10.0)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.1) vite-plugin-handlebars: specifier: ^1.5.0 version: 1.6.0 @@ -296,8 +296,8 @@ importers: specifier: ^7.1.3 version: 7.1.3(@types/node@24.10.0)(typescript@5.9.3) vite: - specifier: ^6.2.2 - version: 6.4.1(@types/node@24.10.0)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.1) + specifier: ^6.4.2 + version: 6.4.2(@types/node@24.10.0)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.1) vite-plugin-handlebars: specifier: ^1.5.0 version: 1.6.0 @@ -439,10 +439,10 @@ importers: devDependencies: '@remix-run/dev': specifier: ^2.16.3 - version: 2.17.2(@remix-run/react@2.17.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3))(@remix-run/serve@2.17.2(typescript@5.9.3))(@types/node@24.10.0)(babel-plugin-macros@3.1.0)(jiti@2.6.1)(terser@5.44.1)(ts-node@10.9.2(@types/node@24.10.0)(typescript@5.9.3))(typescript@5.9.3)(vite@6.4.1(@types/node@24.10.0)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.1))(yaml@2.8.1) + version: 2.17.2(@remix-run/react@2.17.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3))(@remix-run/serve@2.17.2(typescript@5.9.3))(@types/node@24.10.0)(babel-plugin-macros@3.1.0)(jiti@2.6.1)(terser@5.44.1)(ts-node@10.9.2(@types/node@24.10.0)(typescript@5.9.3))(typescript@5.9.3)(vite@6.4.2(@types/node@24.10.0)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.1))(yaml@2.8.1) '@remix-run/v1-route-convention': specifier: ^0.1.4 - version: 0.1.4(@remix-run/dev@2.17.2(@remix-run/react@2.17.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3))(@remix-run/serve@2.17.2(typescript@5.9.3))(@types/node@24.10.0)(babel-plugin-macros@3.1.0)(jiti@2.6.1)(terser@5.44.1)(ts-node@10.9.2(@types/node@24.10.0)(typescript@5.9.3))(typescript@5.9.3)(vite@6.4.1(@types/node@24.10.0)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.1))(yaml@2.8.1)) + version: 0.1.4(@remix-run/dev@2.17.2(@remix-run/react@2.17.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3))(@remix-run/serve@2.17.2(typescript@5.9.3))(@types/node@24.10.0)(babel-plugin-macros@3.1.0)(jiti@2.6.1)(terser@5.44.1)(ts-node@10.9.2(@types/node@24.10.0)(typescript@5.9.3))(typescript@5.9.3)(vite@6.4.2(@types/node@24.10.0)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.1))(yaml@2.8.1)) '@types/js-cookie': specifier: ^3.0.6 version: 3.0.6 @@ -456,8 +456,8 @@ importers: specifier: ^18.0.8 version: 18.3.7(@types/react@18.3.26) vite: - specifier: ^6.4.1 - version: 6.4.1(@types/node@24.10.0)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.1) + specifier: ^6.4.2 + version: 6.4.2(@types/node@24.10.0)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.1) packages: @@ -3018,143 +3018,128 @@ packages: react: optional: true - '@rollup/rollup-android-arm-eabi@4.53.1': - resolution: {integrity: sha512-bxZtughE4VNVJlL1RdoSE545kc4JxL7op57KKoi59/gwuU5rV6jLWFXXc8jwgFoT6vtj+ZjO+Z2C5nrY0Cl6wA==} + '@rollup/rollup-android-arm-eabi@4.60.1': + resolution: {integrity: sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==} cpu: [arm] os: [android] - '@rollup/rollup-android-arm64@4.53.1': - resolution: {integrity: sha512-44a1hreb02cAAfAKmZfXVercPFaDjqXCK+iKeVOlJ9ltvnO6QqsBHgKVPTu+MJHSLLeMEUbeG2qiDYgbFPU48g==} + '@rollup/rollup-android-arm64@4.60.1': + resolution: {integrity: sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==} cpu: [arm64] os: [android] - '@rollup/rollup-darwin-arm64@4.53.1': - resolution: {integrity: sha512-usmzIgD0rf1syoOZ2WZvy8YpXK5G1V3btm3QZddoGSa6mOgfXWkkv+642bfUUldomgrbiLQGrPryb7DXLovPWQ==} + '@rollup/rollup-darwin-arm64@4.60.1': + resolution: {integrity: sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==} cpu: [arm64] os: [darwin] - '@rollup/rollup-darwin-arm64@4.59.0': - resolution: {integrity: sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==} - cpu: [arm64] - os: [darwin] - - '@rollup/rollup-darwin-x64@4.53.1': - resolution: {integrity: sha512-is3r/k4vig2Gt8mKtTlzzyaSQ+hd87kDxiN3uDSDwggJLUV56Umli6OoL+/YZa/KvtdrdyNfMKHzL/P4siOOmg==} + '@rollup/rollup-darwin-x64@4.60.1': + resolution: {integrity: sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==} cpu: [x64] os: [darwin] - '@rollup/rollup-darwin-x64@4.59.0': - resolution: {integrity: sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==} - cpu: [x64] - os: [darwin] - - '@rollup/rollup-freebsd-arm64@4.53.1': - resolution: {integrity: sha512-QJ1ksgp/bDJkZB4daldVmHaEQkG4r8PUXitCOC2WRmRaSaHx5RwPoI3DHVfXKwDkB+Sk6auFI/+JHacTekPRSw==} + '@rollup/rollup-freebsd-arm64@4.60.1': + resolution: {integrity: sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==} cpu: [arm64] os: [freebsd] - '@rollup/rollup-freebsd-x64@4.53.1': - resolution: {integrity: sha512-J6ma5xgAzvqsnU6a0+jgGX/gvoGokqpkx6zY4cWizRrm0ffhHDpJKQgC8dtDb3+MqfZDIqs64REbfHDMzxLMqQ==} + '@rollup/rollup-freebsd-x64@4.60.1': + resolution: {integrity: sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==} cpu: [x64] os: [freebsd] - '@rollup/rollup-linux-arm-gnueabihf@4.53.1': - resolution: {integrity: sha512-JzWRR41o2U3/KMNKRuZNsDUAcAVUYhsPuMlx5RUldw0E4lvSIXFUwejtYz1HJXohUmqs/M6BBJAUBzKXZVddbg==} + '@rollup/rollup-linux-arm-gnueabihf@4.60.1': + resolution: {integrity: sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm-musleabihf@4.53.1': - resolution: {integrity: sha512-L8kRIrnfMrEoHLHtHn+4uYA52fiLDEDyezgxZtGUTiII/yb04Krq+vk3P2Try+Vya9LeCE9ZHU8CXD6J9EhzHQ==} + '@rollup/rollup-linux-arm-musleabihf@4.60.1': + resolution: {integrity: sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm64-gnu@4.53.1': - resolution: {integrity: sha512-ysAc0MFRV+WtQ8li8hi3EoFi7us6d1UzaS/+Dp7FYZfg3NdDljGMoVyiIp6Ucz7uhlYDBZ/zt6XI0YEZbUO11Q==} + '@rollup/rollup-linux-arm64-gnu@4.60.1': + resolution: {integrity: sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-arm64-gnu@4.59.0': - resolution: {integrity: sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==} + '@rollup/rollup-linux-arm64-musl@4.60.1': + resolution: {integrity: sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-arm64-musl@4.53.1': - resolution: {integrity: sha512-UV6l9MJpDbDZZ/fJvqNcvO1PcivGEf1AvKuTcHoLjVZVFeAMygnamCTDikCVMRnA+qJe+B3pSbgX2+lBMqgBhA==} - cpu: [arm64] + '@rollup/rollup-linux-loong64-gnu@4.60.1': + resolution: {integrity: sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==} + cpu: [loong64] os: [linux] - '@rollup/rollup-linux-loong64-gnu@4.53.1': - resolution: {integrity: sha512-UDUtelEprkA85g95Q+nj3Xf0M4hHa4DiJ+3P3h4BuGliY4NReYYqwlc0Y8ICLjN4+uIgCEvaygYlpf0hUj90Yg==} + '@rollup/rollup-linux-loong64-musl@4.60.1': + resolution: {integrity: sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==} cpu: [loong64] os: [linux] - '@rollup/rollup-linux-ppc64-gnu@4.53.1': - resolution: {integrity: sha512-vrRn+BYhEtNOte/zbc2wAUQReJXxEx2URfTol6OEfY2zFEUK92pkFBSXRylDM7aHi+YqEPJt9/ABYzmcrS4SgQ==} + '@rollup/rollup-linux-ppc64-gnu@4.60.1': + resolution: {integrity: sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-ppc64-musl@4.60.1': + resolution: {integrity: sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==} cpu: [ppc64] os: [linux] - '@rollup/rollup-linux-riscv64-gnu@4.53.1': - resolution: {integrity: sha512-gto/1CxHyi4A7YqZZNznQYrVlPSaodOBPKM+6xcDSCMVZN/Fzb4K+AIkNz/1yAYz9h3Ng+e2fY9H6bgawVq17w==} + '@rollup/rollup-linux-riscv64-gnu@4.60.1': + resolution: {integrity: sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==} cpu: [riscv64] os: [linux] - '@rollup/rollup-linux-riscv64-musl@4.53.1': - resolution: {integrity: sha512-KZ6Vx7jAw3aLNjFR8eYVcQVdFa/cvBzDNRFM3z7XhNNunWjA03eUrEwJYPk0G8V7Gs08IThFKcAPS4WY/ybIrQ==} + '@rollup/rollup-linux-riscv64-musl@4.60.1': + resolution: {integrity: sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==} cpu: [riscv64] os: [linux] - '@rollup/rollup-linux-s390x-gnu@4.53.1': - resolution: {integrity: sha512-HvEixy2s/rWNgpwyKpXJcHmE7om1M89hxBTBi9Fs6zVuLU4gOrEMQNbNsN/tBVIMbLyysz/iwNiGtMOpLAOlvA==} + '@rollup/rollup-linux-s390x-gnu@4.60.1': + resolution: {integrity: sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==} cpu: [s390x] os: [linux] - '@rollup/rollup-linux-x64-gnu@4.53.1': - resolution: {integrity: sha512-E/n8x2MSjAQgjj9IixO4UeEUeqXLtiA7pyoXCFYLuXpBA/t2hnbIdxHfA7kK9BFsYAoNU4st1rHYdldl8dTqGA==} + '@rollup/rollup-linux-x64-gnu@4.60.1': + resolution: {integrity: sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==} cpu: [x64] os: [linux] - '@rollup/rollup-linux-x64-gnu@4.59.0': - resolution: {integrity: sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==} + '@rollup/rollup-linux-x64-musl@4.60.1': + resolution: {integrity: sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==} cpu: [x64] os: [linux] - '@rollup/rollup-linux-x64-musl@4.53.1': - resolution: {integrity: sha512-IhJ087PbLOQXCN6Ui/3FUkI9pWNZe/Z7rEIVOzMsOs1/HSAECCvSZ7PkIbkNqL/AZn6WbZvnoVZw/qwqYMo4/w==} + '@rollup/rollup-openbsd-x64@4.60.1': + resolution: {integrity: sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==} cpu: [x64] - os: [linux] + os: [openbsd] - '@rollup/rollup-openharmony-arm64@4.53.1': - resolution: {integrity: sha512-0++oPNgLJHBblreu0SFM7b3mAsBJBTY0Ksrmu9N6ZVrPiTkRgda52mWR7TKhHAsUb9noCjFvAw9l6ZO1yzaVbA==} + '@rollup/rollup-openharmony-arm64@4.60.1': + resolution: {integrity: sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==} cpu: [arm64] os: [openharmony] - '@rollup/rollup-win32-arm64-msvc@4.53.1': - resolution: {integrity: sha512-VJXivz61c5uVdbmitLkDlbcTk9Or43YC2QVLRkqp86QoeFSqI81bNgjhttqhKNMKnQMWnecOCm7lZz4s+WLGpQ==} + '@rollup/rollup-win32-arm64-msvc@4.60.1': + resolution: {integrity: sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==} cpu: [arm64] os: [win32] - '@rollup/rollup-win32-arm64-msvc@4.59.0': - resolution: {integrity: sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==} - cpu: [arm64] - os: [win32] - - '@rollup/rollup-win32-ia32-msvc@4.53.1': - resolution: {integrity: sha512-NmZPVTUOitCXUH6erJDzTQ/jotYw4CnkMDjCYRxNHVD9bNyfrGoIse684F9okwzKCV4AIHRbUkeTBc9F2OOH5Q==} + '@rollup/rollup-win32-ia32-msvc@4.60.1': + resolution: {integrity: sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==} cpu: [ia32] os: [win32] - '@rollup/rollup-win32-x64-gnu@4.53.1': - resolution: {integrity: sha512-2SNj7COIdAf6yliSpLdLG8BEsp5lgzRehgfkP0Av8zKfQFKku6JcvbobvHASPJu4f3BFxej5g+HuQPvqPhHvpQ==} + '@rollup/rollup-win32-x64-gnu@4.60.1': + resolution: {integrity: sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==} cpu: [x64] os: [win32] - '@rollup/rollup-win32-x64-msvc@4.53.1': - resolution: {integrity: sha512-rLarc1Ofcs3DHtgSzFO31pZsCh8g05R2azN1q3fF+H423Co87My0R+tazOEvYVKXSLh8C4LerMK41/K7wlklcg==} - cpu: [x64] - os: [win32] - - '@rollup/rollup-win32-x64-msvc@4.59.0': - resolution: {integrity: sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==} + '@rollup/rollup-win32-x64-msvc@4.60.1': + resolution: {integrity: sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==} cpu: [x64] os: [win32] @@ -3660,11 +3645,12 @@ packages: '@xmldom/xmldom@0.7.13': resolution: {integrity: sha512-lm2GW5PkosIzccsaZIz7tp8cPADSIlIHWDFTR1N0SzfinhhYgeIQjFMz4rYzanCScr3DqQLeomUDArp6MWKm+g==} engines: {node: '>=10.0.0'} - deprecated: this version is no longer supported, please update to at least 0.8.* + deprecated: this version has critical issues, please update to the latest version '@xmldom/xmldom@0.8.11': resolution: {integrity: sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==} engines: {node: '>=10.0.0'} + deprecated: this version has critical issues, please update to the latest version '@xtuc/ieee754@1.2.0': resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==} @@ -4081,6 +4067,9 @@ packages: brace-expansion@2.0.2: resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} + brace-expansion@2.0.3: + resolution: {integrity: sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==} + braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} @@ -7028,8 +7017,8 @@ packages: minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} - minimatch@5.1.6: - resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} + minimatch@5.1.9: + resolution: {integrity: sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==} engines: {node: '>=10'} minimatch@7.4.6: @@ -7063,6 +7052,10 @@ packages: resolution: {integrity: sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==} engines: {node: '>= 8'} + minipass-flush@1.0.7: + resolution: {integrity: sha512-TbqTz9cUwWyHS2Dy89P3ocAGUGxKjjLuR9z8w4WUTGAVgEj17/4nhgo2Du56i0Fm3Pm30g4iA8Lcqctc76jCzA==} + engines: {node: '>= 8'} + minipass-pipeline@1.2.4: resolution: {integrity: sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==} engines: {node: '>=8'} @@ -7590,8 +7583,12 @@ packages: resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} engines: {node: '>=8.6'} - picomatch@4.0.3: - resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + picomatch@2.3.2: + resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} + engines: {node: '>=8.6'} + + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} engines: {node: '>=12'} pidtree@0.6.0: @@ -7722,6 +7719,10 @@ packages: resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} engines: {node: ^10 || ^12 || >=14} + postcss@8.5.8: + resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==} + engines: {node: ^10 || ^12 || >=14} + postject@1.0.0-alpha.6: resolution: {integrity: sha512-b9Eb8h2eVqNE8edvKdwqkrY6O7kAwmI8kcnBv1NScolYJbo59XUF0noFq+lxbC1yN20bmC0WBEbDC5H/7ASb0A==} engines: {node: '>=14.0.0'} @@ -8211,8 +8212,8 @@ packages: engines: {node: '>=10.0.0'} hasBin: true - rollup@4.53.1: - resolution: {integrity: sha512-n2I0V0lN3E9cxxMqBCT3opWOiQBzRN7UG60z/WDKqdX2zHUS/39lezBcsckZFsV6fUTSnfqI7kHf60jDAPGKug==} + rollup@4.60.1: + resolution: {integrity: sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true @@ -9193,8 +9194,8 @@ packages: terser: optional: true - vite@6.4.1: - resolution: {integrity: sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==} + vite@6.4.2: + resolution: {integrity: sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} hasBin: true peerDependencies: @@ -11600,12 +11601,12 @@ snapshots: '@oven/bun-linux-x64-musl-baseline': 1.3.10 '@oven/bun-windows-x64': 1.3.10 '@oven/bun-windows-x64-baseline': 1.3.10 - '@rollup/rollup-darwin-arm64': 4.59.0 - '@rollup/rollup-darwin-x64': 4.59.0 - '@rollup/rollup-linux-arm64-gnu': 4.59.0 - '@rollup/rollup-linux-x64-gnu': 4.59.0 - '@rollup/rollup-win32-arm64-msvc': 4.59.0 - '@rollup/rollup-win32-x64-msvc': 4.59.0 + '@rollup/rollup-darwin-arm64': 4.60.1 + '@rollup/rollup-darwin-x64': 4.60.1 + '@rollup/rollup-linux-arm64-gnu': 4.60.1 + '@rollup/rollup-linux-x64-gnu': 4.60.1 + '@rollup/rollup-win32-arm64-msvc': 4.60.1 + '@rollup/rollup-win32-x64-msvc': 4.60.1 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) @@ -11625,12 +11626,12 @@ snapshots: '@oven/bun-linux-x64-musl-baseline': 1.3.10 '@oven/bun-windows-x64': 1.3.10 '@oven/bun-windows-x64-baseline': 1.3.10 - '@rollup/rollup-darwin-arm64': 4.59.0 - '@rollup/rollup-darwin-x64': 4.59.0 - '@rollup/rollup-linux-arm64-gnu': 4.59.0 - '@rollup/rollup-linux-x64-gnu': 4.59.0 - '@rollup/rollup-win32-arm64-msvc': 4.59.0 - '@rollup/rollup-win32-x64-msvc': 4.59.0 + '@rollup/rollup-darwin-arm64': 4.60.1 + '@rollup/rollup-darwin-x64': 4.60.1 + '@rollup/rollup-linux-arm64-gnu': 4.60.1 + '@rollup/rollup-linux-x64-gnu': 4.60.1 + '@rollup/rollup-win32-arm64-msvc': 4.60.1 + '@rollup/rollup-win32-x64-msvc': 4.60.1 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) @@ -12581,7 +12582,7 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - '@remix-run/dev@2.17.2(@remix-run/react@2.17.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3))(@remix-run/serve@2.17.2(typescript@5.9.3))(@types/node@24.10.0)(babel-plugin-macros@3.1.0)(jiti@2.6.1)(terser@5.44.1)(ts-node@10.9.2(@types/node@24.10.0)(typescript@5.9.3))(typescript@5.9.3)(vite@6.4.1(@types/node@24.10.0)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.1))(yaml@2.8.1)': + '@remix-run/dev@2.17.2(@remix-run/react@2.17.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3))(@remix-run/serve@2.17.2(typescript@5.9.3))(@types/node@24.10.0)(babel-plugin-macros@3.1.0)(jiti@2.6.1)(terser@5.44.1)(ts-node@10.9.2(@types/node@24.10.0)(typescript@5.9.3))(typescript@5.9.3)(vite@6.4.2(@types/node@24.10.0)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.1))(yaml@2.8.1)': dependencies: '@babel/core': 7.28.5 '@babel/generator': 7.28.5 @@ -12643,7 +12644,7 @@ snapshots: optionalDependencies: '@remix-run/serve': 2.17.2(typescript@5.9.3) typescript: 5.9.3 - vite: 6.4.1(@types/node@24.10.0)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.1) + vite: 6.4.2(@types/node@24.10.0)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.1) transitivePeerDependencies: - '@types/node' - babel-plugin-macros @@ -12750,9 +12751,9 @@ snapshots: optionalDependencies: typescript: 5.9.3 - '@remix-run/v1-route-convention@0.1.4(@remix-run/dev@2.17.2(@remix-run/react@2.17.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3))(@remix-run/serve@2.17.2(typescript@5.9.3))(@types/node@24.10.0)(babel-plugin-macros@3.1.0)(jiti@2.6.1)(terser@5.44.1)(ts-node@10.9.2(@types/node@24.10.0)(typescript@5.9.3))(typescript@5.9.3)(vite@6.4.1(@types/node@24.10.0)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.1))(yaml@2.8.1))': + '@remix-run/v1-route-convention@0.1.4(@remix-run/dev@2.17.2(@remix-run/react@2.17.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3))(@remix-run/serve@2.17.2(typescript@5.9.3))(@types/node@24.10.0)(babel-plugin-macros@3.1.0)(jiti@2.6.1)(terser@5.44.1)(ts-node@10.9.2(@types/node@24.10.0)(typescript@5.9.3))(typescript@5.9.3)(vite@6.4.2(@types/node@24.10.0)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.1))(yaml@2.8.1))': dependencies: - '@remix-run/dev': 2.17.2(@remix-run/react@2.17.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3))(@remix-run/serve@2.17.2(typescript@5.9.3))(@types/node@24.10.0)(babel-plugin-macros@3.1.0)(jiti@2.6.1)(terser@5.44.1)(ts-node@10.9.2(@types/node@24.10.0)(typescript@5.9.3))(typescript@5.9.3)(vite@6.4.1(@types/node@24.10.0)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.1))(yaml@2.8.1) + '@remix-run/dev': 2.17.2(@remix-run/react@2.17.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3))(@remix-run/serve@2.17.2(typescript@5.9.3))(@types/node@24.10.0)(babel-plugin-macros@3.1.0)(jiti@2.6.1)(terser@5.44.1)(ts-node@10.9.2(@types/node@24.10.0)(typescript@5.9.3))(typescript@5.9.3)(vite@6.4.2(@types/node@24.10.0)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.1))(yaml@2.8.1) minimatch: 7.4.6 '@remix-run/web-blob@3.1.0': @@ -12803,88 +12804,79 @@ snapshots: - '@preact/signals-core' - preact - '@rollup/rollup-android-arm-eabi@4.53.1': + '@rollup/rollup-android-arm-eabi@4.60.1': optional: true - '@rollup/rollup-android-arm64@4.53.1': + '@rollup/rollup-android-arm64@4.60.1': optional: true - '@rollup/rollup-darwin-arm64@4.53.1': + '@rollup/rollup-darwin-arm64@4.60.1': optional: true - '@rollup/rollup-darwin-arm64@4.59.0': + '@rollup/rollup-darwin-x64@4.60.1': optional: true - '@rollup/rollup-darwin-x64@4.53.1': + '@rollup/rollup-freebsd-arm64@4.60.1': optional: true - '@rollup/rollup-darwin-x64@4.59.0': + '@rollup/rollup-freebsd-x64@4.60.1': optional: true - '@rollup/rollup-freebsd-arm64@4.53.1': + '@rollup/rollup-linux-arm-gnueabihf@4.60.1': optional: true - '@rollup/rollup-freebsd-x64@4.53.1': + '@rollup/rollup-linux-arm-musleabihf@4.60.1': optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.53.1': + '@rollup/rollup-linux-arm64-gnu@4.60.1': optional: true - '@rollup/rollup-linux-arm-musleabihf@4.53.1': + '@rollup/rollup-linux-arm64-musl@4.60.1': optional: true - '@rollup/rollup-linux-arm64-gnu@4.53.1': + '@rollup/rollup-linux-loong64-gnu@4.60.1': optional: true - '@rollup/rollup-linux-arm64-gnu@4.59.0': + '@rollup/rollup-linux-loong64-musl@4.60.1': optional: true - '@rollup/rollup-linux-arm64-musl@4.53.1': + '@rollup/rollup-linux-ppc64-gnu@4.60.1': optional: true - '@rollup/rollup-linux-loong64-gnu@4.53.1': + '@rollup/rollup-linux-ppc64-musl@4.60.1': optional: true - '@rollup/rollup-linux-ppc64-gnu@4.53.1': + '@rollup/rollup-linux-riscv64-gnu@4.60.1': optional: true - '@rollup/rollup-linux-riscv64-gnu@4.53.1': + '@rollup/rollup-linux-riscv64-musl@4.60.1': optional: true - '@rollup/rollup-linux-riscv64-musl@4.53.1': + '@rollup/rollup-linux-s390x-gnu@4.60.1': optional: true - '@rollup/rollup-linux-s390x-gnu@4.53.1': + '@rollup/rollup-linux-x64-gnu@4.60.1': optional: true - '@rollup/rollup-linux-x64-gnu@4.53.1': + '@rollup/rollup-linux-x64-musl@4.60.1': optional: true - '@rollup/rollup-linux-x64-gnu@4.59.0': + '@rollup/rollup-openbsd-x64@4.60.1': optional: true - '@rollup/rollup-linux-x64-musl@4.53.1': + '@rollup/rollup-openharmony-arm64@4.60.1': optional: true - '@rollup/rollup-openharmony-arm64@4.53.1': + '@rollup/rollup-win32-arm64-msvc@4.60.1': optional: true - '@rollup/rollup-win32-arm64-msvc@4.53.1': + '@rollup/rollup-win32-ia32-msvc@4.60.1': optional: true - '@rollup/rollup-win32-arm64-msvc@4.59.0': + '@rollup/rollup-win32-x64-gnu@4.60.1': optional: true - '@rollup/rollup-win32-ia32-msvc@4.53.1': - optional: true - - '@rollup/rollup-win32-x64-gnu@4.53.1': - optional: true - - '@rollup/rollup-win32-x64-msvc@4.53.1': - optional: true - - '@rollup/rollup-win32-x64-msvc@4.59.0': + '@rollup/rollup-win32-x64-msvc@4.60.1': optional: true '@rtsao/scc@1.1.0': {} @@ -13668,7 +13660,7 @@ snapshots: anymatch@3.1.3: dependencies: normalize-path: 3.0.0 - picomatch: 2.3.1 + picomatch: 2.3.2 appdmg@0.6.6: dependencies: @@ -13999,6 +13991,10 @@ snapshots: dependencies: balanced-match: 1.0.2 + brace-expansion@2.0.3: + dependencies: + balanced-match: 1.0.2 + braces@3.0.3: dependencies: fill-range: 7.1.1 @@ -14050,7 +14046,7 @@ snapshots: lru-cache: 7.18.3 minipass: 3.3.6 minipass-collect: 1.0.2 - minipass-flush: 1.0.5 + minipass-flush: 1.0.7 minipass-pipeline: 1.2.4 mkdirp: 1.0.4 p-map: 4.0.0 @@ -15838,9 +15834,9 @@ snapshots: dependencies: pend: 1.2.0 - fdir@6.5.0(picomatch@4.0.3): + fdir@6.5.0(picomatch@4.0.4): optionalDependencies: - picomatch: 4.0.3 + picomatch: 4.0.4 fetch-blob@3.2.0: dependencies: @@ -16199,7 +16195,7 @@ snapshots: fs.realpath: 1.0.0 inflight: 1.0.6 inherits: 2.0.4 - minimatch: 5.1.6 + minimatch: 5.1.9 once: 1.4.0 glob@9.3.5: @@ -17016,7 +17012,7 @@ snapshots: minipass: 3.3.6 minipass-collect: 1.0.2 minipass-fetch: 2.1.2 - minipass-flush: 1.0.5 + minipass-flush: 1.0.7 minipass-pipeline: 1.2.4 negotiator: 0.6.4 promise-retry: 2.0.1 @@ -17622,7 +17618,7 @@ snapshots: micromatch@4.0.8: dependencies: braces: 3.0.3 - picomatch: 2.3.1 + picomatch: 2.3.2 mime-db@1.33.0: {} @@ -17666,9 +17662,9 @@ snapshots: dependencies: brace-expansion: 1.1.12 - minimatch@5.1.6: + minimatch@5.1.9: dependencies: - brace-expansion: 2.0.2 + brace-expansion: 2.0.3 minimatch@7.4.6: dependencies: @@ -17706,6 +17702,10 @@ snapshots: dependencies: minipass: 3.3.6 + minipass-flush@1.0.7: + dependencies: + minipass: 3.3.6 + minipass-pipeline@1.2.4: dependencies: minipass: 3.3.6 @@ -18240,7 +18240,9 @@ snapshots: picomatch@2.3.1: {} - picomatch@4.0.3: {} + picomatch@2.3.2: {} + + picomatch@4.0.4: {} pidtree@0.6.0: {} @@ -18416,6 +18418,12 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + postcss@8.5.8: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + postject@1.0.0-alpha.6: dependencies: commander: 9.5.0 @@ -18734,7 +18742,7 @@ snapshots: readdirp@3.6.0: dependencies: - picomatch: 2.3.1 + picomatch: 2.3.2 rechoir@0.6.2: dependencies: @@ -18935,32 +18943,35 @@ snapshots: optionalDependencies: fsevents: 2.3.3 - rollup@4.53.1: + rollup@4.60.1: dependencies: '@types/estree': 1.0.8 optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.53.1 - '@rollup/rollup-android-arm64': 4.53.1 - '@rollup/rollup-darwin-arm64': 4.53.1 - '@rollup/rollup-darwin-x64': 4.53.1 - '@rollup/rollup-freebsd-arm64': 4.53.1 - '@rollup/rollup-freebsd-x64': 4.53.1 - '@rollup/rollup-linux-arm-gnueabihf': 4.53.1 - '@rollup/rollup-linux-arm-musleabihf': 4.53.1 - '@rollup/rollup-linux-arm64-gnu': 4.53.1 - '@rollup/rollup-linux-arm64-musl': 4.53.1 - '@rollup/rollup-linux-loong64-gnu': 4.53.1 - '@rollup/rollup-linux-ppc64-gnu': 4.53.1 - '@rollup/rollup-linux-riscv64-gnu': 4.53.1 - '@rollup/rollup-linux-riscv64-musl': 4.53.1 - '@rollup/rollup-linux-s390x-gnu': 4.53.1 - '@rollup/rollup-linux-x64-gnu': 4.53.1 - '@rollup/rollup-linux-x64-musl': 4.53.1 - '@rollup/rollup-openharmony-arm64': 4.53.1 - '@rollup/rollup-win32-arm64-msvc': 4.53.1 - '@rollup/rollup-win32-ia32-msvc': 4.53.1 - '@rollup/rollup-win32-x64-gnu': 4.53.1 - '@rollup/rollup-win32-x64-msvc': 4.53.1 + '@rollup/rollup-android-arm-eabi': 4.60.1 + '@rollup/rollup-android-arm64': 4.60.1 + '@rollup/rollup-darwin-arm64': 4.60.1 + '@rollup/rollup-darwin-x64': 4.60.1 + '@rollup/rollup-freebsd-arm64': 4.60.1 + '@rollup/rollup-freebsd-x64': 4.60.1 + '@rollup/rollup-linux-arm-gnueabihf': 4.60.1 + '@rollup/rollup-linux-arm-musleabihf': 4.60.1 + '@rollup/rollup-linux-arm64-gnu': 4.60.1 + '@rollup/rollup-linux-arm64-musl': 4.60.1 + '@rollup/rollup-linux-loong64-gnu': 4.60.1 + '@rollup/rollup-linux-loong64-musl': 4.60.1 + '@rollup/rollup-linux-ppc64-gnu': 4.60.1 + '@rollup/rollup-linux-ppc64-musl': 4.60.1 + '@rollup/rollup-linux-riscv64-gnu': 4.60.1 + '@rollup/rollup-linux-riscv64-musl': 4.60.1 + '@rollup/rollup-linux-s390x-gnu': 4.60.1 + '@rollup/rollup-linux-x64-gnu': 4.60.1 + '@rollup/rollup-linux-x64-musl': 4.60.1 + '@rollup/rollup-openbsd-x64': 4.60.1 + '@rollup/rollup-openharmony-arm64': 4.60.1 + '@rollup/rollup-win32-arm64-msvc': 4.60.1 + '@rollup/rollup-win32-ia32-msvc': 4.60.1 + '@rollup/rollup-win32-x64-gnu': 4.60.1 + '@rollup/rollup-win32-x64-msvc': 4.60.1 fsevents: 2.3.3 router@2.2.0: @@ -19602,8 +19613,8 @@ snapshots: tinyglobby@0.2.15: dependencies: - fdir: 6.5.0(picomatch@4.0.3) - picomatch: 4.0.3 + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 tmp@0.0.33: dependencies: @@ -20031,7 +20042,7 @@ snapshots: debug: 4.4.3 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 6.4.1(@types/node@24.10.0)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.1) + vite: 6.4.2(@types/node@24.10.0)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.1) transitivePeerDependencies: - '@types/node' - jiti @@ -20058,7 +20069,7 @@ snapshots: vite@2.9.18: dependencies: esbuild: 0.14.54 - postcss: 8.5.6 + postcss: 8.5.8 resolve: 1.22.11 rollup: 2.77.3 optionalDependencies: @@ -20067,20 +20078,20 @@ snapshots: vite@5.4.21(@types/node@24.10.0)(terser@5.44.1): dependencies: esbuild: 0.21.5 - postcss: 8.5.6 - rollup: 4.53.1 + postcss: 8.5.8 + rollup: 4.60.1 optionalDependencies: '@types/node': 24.10.0 fsevents: 2.3.3 terser: 5.44.1 - vite@6.4.1(@types/node@24.10.0)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.1): + vite@6.4.2(@types/node@24.10.0)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.1): dependencies: esbuild: 0.25.12 - fdir: 6.5.0(picomatch@4.0.3) - picomatch: 4.0.3 - postcss: 8.5.6 - rollup: 4.53.1 + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + postcss: 8.5.8 + rollup: 4.60.1 tinyglobby: 0.2.15 optionalDependencies: '@types/node': 24.10.0 diff --git a/src/alib/py/framework/jupiter/framework/appform/cli/appform.py b/src/alib/py/framework/jupiter/framework/appform/cli/appform.py index c3eea063a..5d33d72bd 100644 --- a/src/alib/py/framework/jupiter/framework/appform/cli/appform.py +++ b/src/alib/py/framework/jupiter/framework/appform/cli/appform.py @@ -557,6 +557,7 @@ def _add_use_case_type( global_properties=self._global_properties, time_provider=self._time_provider, realm_codec_registry=self._realm_codec_registry, + invocation_recorder=self._invocation_recorder, auth_token_stamper=self._auth_token_stamper, ), ) @@ -590,6 +591,7 @@ def _add_use_case_type( global_properties=self._global_properties, time_provider=self._time_provider, realm_codec_registry=self._realm_codec_registry, + invocation_recorder=self._invocation_recorder, auth_token_stamper=self._auth_token_stamper, ), ) diff --git a/src/alib/py/framework/jupiter/framework/appform/webapi/appform.py b/src/alib/py/framework/jupiter/framework/appform/webapi/appform.py index 3459ce740..22a0614f5 100644 --- a/src/alib/py/framework/jupiter/framework/appform/webapi/appform.py +++ b/src/alib/py/framework/jupiter/framework/appform/webapi/appform.py @@ -586,6 +586,7 @@ def _add_use_case_type( global_properties=self._global_properties, time_provider=self._request_time_provider, realm_codec_registry=self._realm_codec_registry, + invocation_recorder=self._invocation_recorder, auth_token_stamper=self._auth_token_stamper, ports=self._ports, ) @@ -627,6 +628,7 @@ def _add_use_case_type( global_properties=self._global_properties, time_provider=self._request_time_provider, realm_codec_registry=self._realm_codec_registry, + invocation_recorder=self._invocation_recorder, auth_token_stamper=self._auth_token_stamper, ports=self._ports, ) @@ -684,9 +686,9 @@ def _add_exception_handler( def _custom_openapi(self) -> dict[str, Any]: # type: ignore def build_field_name( field: dataclasses.Field[DomainThing], - field_type: type[DomainThing] | ForwardRef | str | type[ParentLink], + field_type: type[DomainThing] | ForwardRef | str | type[ParentLink[Entity]], ) -> str: - if field_type is ParentLink: + if field_type is ParentLink or get_origin(field_type) is ParentLink: return f"{field.name}_ref_id" else: return field.name @@ -715,7 +717,7 @@ def build_primitive_type(primitive_type: type[Primitive]) -> str: def build_composite_field( field: dataclasses.Field[DomainThing], - field_type: type[DomainThing] | ForwardRef | str | type[ParentLink], + field_type: type[DomainThing] | ForwardRef | str | type[ParentLink[Entity]], ) -> dict[str, Any]: if isinstance(field_type, typing._GenericAlias) and field_type.__name__ == "Literal": # type: ignore return { @@ -729,7 +731,7 @@ def build_composite_field( ) elif isinstance(field_type, str): return {"$ref": f"#/components/schemas/{field_type}"} - elif field_type is ParentLink: + elif field_type is ParentLink or get_origin(field_type) is ParentLink: return {"title": f"{field.name.capitalize()} RefId", "type": "string"} elif is_primitive_type(field_type): return { diff --git a/src/alib/py/framework/jupiter/framework/base/timestamp.py b/src/alib/py/framework/jupiter/framework/base/timestamp.py index 7642b37dd..a617bd4ba 100644 --- a/src/alib/py/framework/jupiter/framework/base/timestamp.py +++ b/src/alib/py/framework/jupiter/framework/base/timestamp.py @@ -53,6 +53,14 @@ def mins_since(self, other: "Timestamp") -> int: """Get the minutes since another timestamp.""" return self.the_ts.diff(other.the_ts).in_minutes() + def add_minutes(self, minutes: int) -> "Timestamp": + """Add these number of minutes to this timestamp.""" + return Timestamp(self.the_ts.add(minutes=minutes)) + + def subtract_minutes(self, minutes: int) -> "Timestamp": + """Subtract these number of minutes from this timestamp.""" + return Timestamp(self.the_ts.subtract(minutes=minutes)) + @property def value(self) -> DateTime: """The value as a time.""" diff --git a/src/alib/py/framework/jupiter/framework/concepts/__init__.py b/src/alib/py/framework/jupiter/framework/concepts/__init__.py new file mode 100644 index 000000000..e05c16df6 --- /dev/null +++ b/src/alib/py/framework/jupiter/framework/concepts/__init__.py @@ -0,0 +1 @@ +"""A concept is some data carrying domain object.""" diff --git a/src/alib/py/framework/jupiter/framework/concepts/registry.py b/src/alib/py/framework/jupiter/framework/concepts/registry.py new file mode 100644 index 000000000..4d4eef4c3 --- /dev/null +++ b/src/alib/py/framework/jupiter/framework/concepts/registry.py @@ -0,0 +1,17 @@ +"""A registry for concepts.""" + +import abc + +from jupiter.framework.entity import Entity + + +class ConceptNotFoundError(Exception): + """A concept was not found.""" + + +class ConceptRegistry(abc.ABC): + """A registry for concepts.""" + + @abc.abstractmethod + def get_entity_by_name(self, name: str) -> type[Entity]: + """Get an entity class by its name, or raise ConceptNotFoundError.""" diff --git a/src/alib/py/framework/jupiter/framework/concepts/standard.py b/src/alib/py/framework/jupiter/framework/concepts/standard.py new file mode 100644 index 000000000..c07daf09f --- /dev/null +++ b/src/alib/py/framework/jupiter/framework/concepts/standard.py @@ -0,0 +1,90 @@ +"""A concept registry built by exploring module trees.""" + +from types import ModuleType +from typing import Final + +from jupiter.framework.concept import Concept +from jupiter.framework.concepts.registry import ConceptNotFoundError, ConceptRegistry +from jupiter.framework.entity import Entity +from jupiter.framework.record import Record +from jupiter.framework.utils.utils import find_all_modules +from jupiter.framework.value import Value + + +class ModuleExplorerConceptRegistry(ConceptRegistry): + """A registry for concepts constructed by exploring a module tree.""" + + _concepts: Final[dict[str, type[Concept]]] + _entities: Final[dict[str, type[Entity]]] + _records: Final[dict[str, type[Record]]] + _values: Final[dict[str, type[Value]]] + + def __init__(self) -> None: + """Initialize the registry.""" + self._concepts = {} + self._entities = {} + self._records = {} + self._values = {} + + @staticmethod + def build_from_module_root( + *module_roots: ModuleType, + ) -> "ModuleExplorerConceptRegistry": + """Build a registry from a module root by exploring all modules.""" + registry = ModuleExplorerConceptRegistry() + + for m in find_all_modules(*module_roots): + for _name, obj in m.__dict__.items(): + if not isinstance(obj, type): + continue + + if not issubclass(obj, Concept): + continue + + if obj.__module__ != m.__name__: + continue + + class_name = obj.__name__ + + if class_name in registry._concepts: + raise Exception( + f"Duplicate concept name '{class_name}': " + f"defined in both {registry._concepts[class_name].__module__} " + f"and {obj.__module__}" + ) + registry._concepts[class_name] = obj + + if issubclass(obj, Entity): + if class_name in registry._entities: + raise Exception( + f"Duplicate entity name '{class_name}': " + f"defined in both {registry._entities[class_name].__module__} " + f"and {obj.__module__}" + ) + registry._entities[class_name] = obj + elif issubclass(obj, Record): + if class_name in registry._records: + raise Exception( + f"Duplicate record name '{class_name}': " + f"defined in both {registry._records[class_name].__module__} " + f"and {obj.__module__}" + ) + registry._records[class_name] = obj + elif issubclass(obj, Value): + if class_name in registry._values: + raise Exception( + f"Duplicate value name '{class_name}': " + f"defined in both {registry._values[class_name].__module__} " + f"and {obj.__module__}" + ) + registry._values[class_name] = obj + + return registry + + def get_entity_by_name(self, name: str) -> type[Entity]: + """Get an entity class by its name, or raise ConceptNotFoundError.""" + if name not in self._entities: + raise ConceptNotFoundError( + f"No entity with name '{name}' found in the registry" + ) + return self._entities[name] diff --git a/src/alib/py/framework/jupiter/framework/entity.py b/src/alib/py/framework/jupiter/framework/entity.py index 61e699236..47cd93b17 100644 --- a/src/alib/py/framework/jupiter/framework/entity.py +++ b/src/alib/py/framework/jupiter/framework/entity.py @@ -10,6 +10,7 @@ Sequence, TypeVar, cast, + get_origin, ) from jupiter.framework.base.entity_id import BAD_REF_ID, EntityId @@ -129,10 +130,13 @@ def parent_ref_id(self) -> EntityId: found = None for field in all_fields: - if field.type is not ParentLink: + if ( + field.type is not ParentLink + and get_origin(field.type) is not ParentLink + ): continue found_cnt += 1 - found = cast(ParentLink, getattr(self, field.name)).ref_id + found = cast(ParentLink[Entity], getattr(self, field.name)).ref_id if found_cnt == 0: raise Exception( @@ -190,7 +194,7 @@ class IsOneOfRefId: @dataclass(frozen=True) -class ParentLink: +class ParentLink(Generic[_EntityT]): """A link to a parent entity.""" ref_id: EntityId @@ -505,7 +509,7 @@ def _check_entity_has_parent_field(cls: type[_EntityT]) -> None: found_cnt = 0 for field in all_fields: - if field.type is ParentLink: + if field.type is ParentLink or get_origin(field.type) is ParentLink: found_cnt += 1 if issubclass(cls, RootEntity): @@ -529,7 +533,9 @@ def _check_entity_can_be_filterd_by( if field.name == filter_name: found_field = field break - elif field.type is ParentLink and filter_name == field.name + "_ref_id": + elif ( + field.type is ParentLink or get_origin(field.type) is ParentLink + ) and filter_name == field.name + "_ref_id": found_field = field break else: @@ -542,11 +548,23 @@ def _check_entity_can_be_filterd_by( found_field_type, found_field_optional = normalize_optional(found_field.type) + is_parent_link = ( + found_field_type is ParentLink or get_origin(found_field_type) is ParentLink + ) + if found_field_optional: if filter_rule is None: continue - if issubclass(found_field_type, AtomicValue): + if is_parent_link: + if not ( + isinstance(filter_rule, IsRefId) + or isinstance(filter_rule, IsParentLink) + ): + raise Exception( + f"Filter rule for '{filter_name}' is {filter_rule.__class__} which is not correct" + ) + elif issubclass(found_field_type, AtomicValue): if isinstance(filter_rule, AtomicValue): if found_field_type != filter_rule.__class__: raise Exception( @@ -580,14 +598,6 @@ def _check_entity_can_be_filterd_by( raise Exception( f"Filter rule for '{filter_name}' is {filter_rule.__class__} which is not correct" ) - elif issubclass(found_field_type, ParentLink): - if not ( - isinstance(filter_rule, IsRefId) - or isinstance(filter_rule, IsParentLink) - ): - raise Exception( - f"Filter rule for '{filter_name}' is {filter_rule.__class__} which is not correct" - ) else: raise Exception( f"Filter rule for '{filter_name}' is {filter_rule.__class__} which is not supported" diff --git a/src/alib/py/framework/jupiter/framework/mutation_inovcation/entity_event.py b/src/alib/py/framework/jupiter/framework/mutation_inovcation/entity_event.py new file mode 100644 index 000000000..7902b76ae --- /dev/null +++ b/src/alib/py/framework/jupiter/framework/mutation_inovcation/entity_event.py @@ -0,0 +1,27 @@ +"""Framework level elements for entity events.""" + +from dataclasses import dataclass + +from jupiter.framework.base.entity_id import EntityId +from jupiter.framework.base.mutation_id import MutationId +from jupiter.framework.base.timestamp import Timestamp +from jupiter.framework.base.trace_id import TraceId +from jupiter.framework.event import EventKind + + +@dataclass(frozen=True) +class MutationEntityEvent: + """The record of the modification of an entity.""" + + entity_type: str + entity_ref_id: EntityId + entity_version: int + kind: EventKind + name: str + trace_id: TraceId + mutation_id: MutationId + timestamp: Timestamp + session_index: int + source: str + context_str: str + data: str diff --git a/src/alib/py/framework/jupiter/framework/mutation_inovcation/record.py b/src/alib/py/framework/jupiter/framework/mutation_inovcation/invocation_record.py similarity index 92% rename from src/alib/py/framework/jupiter/framework/mutation_inovcation/record.py rename to src/alib/py/framework/jupiter/framework/mutation_inovcation/invocation_record.py index 9e9f273be..75daf04fb 100644 --- a/src/alib/py/framework/jupiter/framework/mutation_inovcation/record.py +++ b/src/alib/py/framework/jupiter/framework/mutation_inovcation/invocation_record.py @@ -26,6 +26,7 @@ class MutationInvocationRecord: mutation_id: MutationId timestamp: Timestamp context_str: str + source: str name: str args: Mapping[str, RealmThing] result: MutationInvocationResult @@ -37,6 +38,7 @@ def build_success( mutation_id: MutationId, timestamp: Timestamp, context_str: str, + source: str, name: str, args: Mapping[str, RealmThing], ) -> "MutationInvocationRecord": @@ -46,6 +48,7 @@ def build_success( mutation_id=mutation_id, timestamp=timestamp, context_str=context_str, + source=source, name=name, args=args, result=MutationInvocationResult.SUCCESS, @@ -58,16 +61,18 @@ def build_failure( mutation_id: MutationId, timestamp: Timestamp, context_str: str, + source: str, name: str, args: Mapping[str, RealmThing], error: Exception, ) -> "MutationInvocationRecord": - """Build a success case for an invocation.""" + """Build a failure case for an invocation.""" return MutationInvocationRecord( trace_id=trace_id, mutation_id=mutation_id, context_str=context_str, timestamp=timestamp, + source=source, name=name, args=args, result=MutationInvocationResult.FAILURE, diff --git a/src/alib/py/framework/jupiter/framework/mutation_inovcation/recorder.py b/src/alib/py/framework/jupiter/framework/mutation_inovcation/recorder.py index 96c53eca0..7ee63ad6e 100644 --- a/src/alib/py/framework/jupiter/framework/mutation_inovcation/recorder.py +++ b/src/alib/py/framework/jupiter/framework/mutation_inovcation/recorder.py @@ -2,7 +2,13 @@ import abc -from jupiter.framework.mutation_inovcation.record import MutationInvocationRecord +from jupiter.framework.base.entity_id import EntityId +from jupiter.framework.base.mutation_id import MutationId +from jupiter.framework.base.timestamp import Timestamp +from jupiter.framework.mutation_inovcation.entity_event import MutationEntityEvent +from jupiter.framework.mutation_inovcation.invocation_record import ( + MutationInvocationRecord, +) class MutationInvocationRecorder(abc.ABC): @@ -15,6 +21,40 @@ async def record( ) -> None: """Record the invocation of the mutation.""" + @abc.abstractmethod + async def find_all_invocation_records( + self, mutation_ids: list[MutationId] + ) -> list[MutationInvocationRecord]: + """Retrieve all mutation records.""" + + @abc.abstractmethod + async def find_all_entity_events_by_timestamp_desc( + self, entity_type: str, entity_ref_id: EntityId, offset: int, limit: int + ) -> tuple[list[MutationEntityEvent], int]: + """Retrieve all events on an entity in a given range.""" + + @abc.abstractmethod + async def find_all_entity_events_between( + self, + entity_type: str, + entity_ref_id: EntityId, + start: Timestamp, + end: Timestamp, + ) -> list[MutationEntityEvent]: + """Retrieve all events on an entity between two timestamps.""" + + @abc.abstractmethod + async def find_all_entity_events_for_mutation( + self, mutation_id: MutationId + ) -> list[MutationEntityEvent]: + """Retrieve all entity events for a given mutation id.""" + + @abc.abstractmethod + async def find_all_invocation_records_by_context_str( + self, context_str: str, offset: int, limit: int + ) -> tuple[list[MutationInvocationRecord], int]: + """Retrieve all invocation records for a given context with pagination.""" + @abc.abstractmethod async def clear_all(self, context_str: str) -> None: """Clear all invocation records for a given context.""" diff --git a/src/alib/py/framework/jupiter/framework/mutation_inovcation/recorders/impl/sqlite.py b/src/alib/py/framework/jupiter/framework/mutation_inovcation/recorders/impl/sqlite.py index fe10778f5..8fdca8659 100644 --- a/src/alib/py/framework/jupiter/framework/mutation_inovcation/recorders/impl/sqlite.py +++ b/src/alib/py/framework/jupiter/framework/mutation_inovcation/recorders/impl/sqlite.py @@ -5,8 +5,14 @@ from types import TracebackType from typing import Final -from jupiter.framework.mutation_inovcation.record import ( +from jupiter.framework.base.entity_id import EntityId +from jupiter.framework.base.mutation_id import MutationId +from jupiter.framework.base.timestamp import Timestamp +from jupiter.framework.base.trace_id import TraceId +from jupiter.framework.mutation_inovcation.entity_event import MutationEntityEvent +from jupiter.framework.mutation_inovcation.invocation_record import ( MutationInvocationRecord, + MutationInvocationResult, ) from jupiter.framework.mutation_inovcation.recorders.persistent import ( MutationInvocationRecordRepository, @@ -15,6 +21,12 @@ ) from jupiter.framework.realm.realm import RealmCodecRegistry from jupiter.framework.storage.sqlite.connection import SqliteConnection +from jupiter.framework.storage.sqlite.events import ( + build_event_table, + find_entity_events_between, + find_entity_events_by_mutation_id, + find_entity_events_by_timestamp_desc, +) from jupiter.framework.storage.sqlite.repository import SqliteRepository from sqlalchemy import ( JSON, @@ -24,7 +36,9 @@ String, Table, delete, + func, insert, + select, ) from sqlalchemy.ext.asyncio import AsyncConnection, AsyncEngine @@ -36,6 +50,7 @@ class SqliteMutationInvocationRecordRepository( """A SQlite repository for mutation use cases invocation records.""" _mutation_invocation_record_table: Final[Table] + _mutation_entity_event_table: Final[Table] def __init__( self, @@ -53,11 +68,15 @@ def __init__( Column("timestamp", DateTime, primary_key=True), Column("context_str", String, primary_key=True), Column("name", String, primary_key=True), + Column("source", String, nullable=False), Column("args", JSON, nullable=False), Column("result", String, nullable=False), Column("error_str", String, nullable=True), keep_existing=True, ) + self._mutation_entity_event_table = build_event_table( + self._mutation_invocation_record_table, metadata + ) async def create( self, @@ -76,6 +95,7 @@ async def create( invocation_record.timestamp ), context_str=invocation_record.context_str, + source=invocation_record.source, name=invocation_record.name, args=invocation_record.args, result=str(invocation_record.result.value), @@ -83,6 +103,146 @@ async def create( ), ) + async def find_all( + self, + mutation_ids: list[MutationId], + ) -> list[MutationInvocationRecord]: + """Find all invocation records matching the given mutation ids.""" + query_stmt = select(self._mutation_invocation_record_table).where( + self._mutation_invocation_record_table.c.mutation_id.in_( + [self._realm_codec_registry.db_encode(mid) for mid in mutation_ids] + ) + ) + results = await self._connection.execute(query_stmt) + return [ + MutationInvocationRecord( + trace_id=self._realm_codec_registry.db_decode(TraceId, row.trace_id), + mutation_id=self._realm_codec_registry.db_decode( + MutationId, row.mutation_id + ), + timestamp=self._realm_codec_registry.db_decode( + Timestamp, row.timestamp + ), + context_str=row.context_str, + source=row.source, + name=row.name, + args=row.args, + result=MutationInvocationResult(row.result), + error_str=row.error_str, + ) + for row in results + ] + + _MAX_FIND_ALL_LIMIT: int = 200 + + async def find_all_entity_events_by_timestamp_desc( + self, + entity_type: str, + entity_ref_id: EntityId, + offset: int, + limit: int, + ) -> tuple[list[MutationEntityEvent], int]: + """Find all entity events in descending timestamp order with pagination.""" + if offset < 0: + raise ValueError(f"Offset must be non-negative but was {offset}") + if limit <= 0 or limit > self._MAX_FIND_ALL_LIMIT: + raise ValueError( + f"Limit must be between 1 and {self._MAX_FIND_ALL_LIMIT} but was {limit}" + ) + return await find_entity_events_by_timestamp_desc( + self._realm_codec_registry, + self._connection, + self._mutation_entity_event_table, + entity_type, + entity_ref_id, + offset, + limit, + ) + + async def find_all_entity_events_between( + self, + entity_type: str, + entity_ref_id: EntityId, + start: Timestamp, + end: Timestamp, + ) -> list[MutationEntityEvent]: + """Find all entity events between two timestamps.""" + return await find_entity_events_between( + self._realm_codec_registry, + self._connection, + self._mutation_entity_event_table, + entity_type, + entity_ref_id, + start, + end, + ) + + async def find_all_entity_events_for_mutation( + self, + mutation_id: MutationId, + ) -> list[MutationEntityEvent]: + """Find all entity events for a given mutation id.""" + return await find_entity_events_by_mutation_id( + self._realm_codec_registry, + self._connection, + self._mutation_entity_event_table, + mutation_id, + ) + + _MAX_FIND_ALL_BY_CONTEXT_LIMIT: int = 200 + + async def find_all_invocation_records_by_context_str( + self, + context_str: str, + offset: int, + limit: int, + ) -> tuple[list[MutationInvocationRecord], int]: + """Find all invocation records for a given context with pagination.""" + if offset < 0: + raise ValueError(f"Offset must be non-negative but was {offset}") + if limit <= 0 or limit > self._MAX_FIND_ALL_BY_CONTEXT_LIMIT: + raise ValueError( + f"Limit must be between 1 and {self._MAX_FIND_ALL_BY_CONTEXT_LIMIT} but was {limit}" + ) + + tbl = self._mutation_invocation_record_table + + count_stmt = ( + select(func.count()) + .select_from(tbl) + .where(tbl.c.context_str == context_str) + ) + total_cnt_result = await self._connection.execute(count_stmt) + total_cnt = total_cnt_result.scalar_one() + + query_stmt = ( + select(tbl) + .where(tbl.c.context_str == context_str) + .order_by(tbl.c.timestamp.desc()) + .offset(offset) + .limit(limit) + ) + results = await self._connection.execute(query_stmt) + records = [ + MutationInvocationRecord( + trace_id=self._realm_codec_registry.db_decode(TraceId, row.trace_id), + mutation_id=self._realm_codec_registry.db_decode( + MutationId, row.mutation_id + ), + timestamp=self._realm_codec_registry.db_decode( + Timestamp, row.timestamp + ), + context_str=row.context_str, + source=row.source, + name=row.name, + args=row.args, + result=MutationInvocationResult(row.result), + error_str=row.error_str, + ) + for row in results + ] + return records, total_cnt + async def clear_all(self, context_str: str) -> None: """Clear all entries in the invocation record.""" await self._connection.execute( diff --git a/src/alib/py/framework/jupiter/framework/mutation_inovcation/recorders/logging.py b/src/alib/py/framework/jupiter/framework/mutation_inovcation/recorders/logging.py index ab81f3edb..0923a0d06 100644 --- a/src/alib/py/framework/jupiter/framework/mutation_inovcation/recorders/logging.py +++ b/src/alib/py/framework/jupiter/framework/mutation_inovcation/recorders/logging.py @@ -2,7 +2,11 @@ import logging -from jupiter.framework.mutation_inovcation.record import ( +from jupiter.framework.base.entity_id import EntityId +from jupiter.framework.base.mutation_id import MutationId +from jupiter.framework.base.timestamp import Timestamp +from jupiter.framework.mutation_inovcation.entity_event import MutationEntityEvent +from jupiter.framework.mutation_inovcation.invocation_record import ( MutationInvocationRecord, ) from jupiter.framework.mutation_inovcation.recorder import ( @@ -27,5 +31,39 @@ async def record(self, invocation_record: MutationInvocationRecord) -> None: invocation_record.error_str, ) + async def find_all_invocation_records( + self, mutation_ids: list[MutationId] + ) -> list[MutationInvocationRecord]: + """Retrieve all mutation records.""" + return [] + + async def find_all_entity_events_by_timestamp_desc( + self, entity_type: str, entity_ref_id: EntityId, offset: int, limit: int + ) -> tuple[list[MutationEntityEvent], int]: + """Retrieve all events on an entity in a given range.""" + return [], 0 + + async def find_all_entity_events_between( + self, + entity_type: str, + entity_ref_id: EntityId, + start: Timestamp, + end: Timestamp, + ) -> list[MutationEntityEvent]: + """Retrieve all events on an entity between two timestamps.""" + return [] + + async def find_all_entity_events_for_mutation( + self, mutation_id: MutationId + ) -> list[MutationEntityEvent]: + """Retrieve all entity events for a given mutation id.""" + return [] + + async def find_all_invocation_records_by_context_str( + self, context_str: str, offset: int, limit: int + ) -> tuple[list[MutationInvocationRecord], int]: + """Retrieve all invocation records for a given context with pagination.""" + return [], 0 + async def clear_all(self, context_str: str) -> None: """Clear all invocation records for a given context.""" diff --git a/src/alib/py/framework/jupiter/framework/mutation_inovcation/recorders/noop.py b/src/alib/py/framework/jupiter/framework/mutation_inovcation/recorders/noop.py index 0abbe8350..0a31c5d02 100644 --- a/src/alib/py/framework/jupiter/framework/mutation_inovcation/recorders/noop.py +++ b/src/alib/py/framework/jupiter/framework/mutation_inovcation/recorders/noop.py @@ -1,6 +1,10 @@ """A noop recorder for mutations.""" -from jupiter.framework.mutation_inovcation.record import ( +from jupiter.framework.base.entity_id import EntityId +from jupiter.framework.base.mutation_id import MutationId +from jupiter.framework.base.timestamp import Timestamp +from jupiter.framework.mutation_inovcation.entity_event import MutationEntityEvent +from jupiter.framework.mutation_inovcation.invocation_record import ( MutationInvocationRecord, ) from jupiter.framework.mutation_inovcation.recorder import ( @@ -14,5 +18,39 @@ class NoopMutationInvocationRecorder(MutationInvocationRecorder): async def record(self, invocation_record: MutationInvocationRecord) -> None: """Record the invocation of the mutation.""" + async def find_all_invocation_records( + self, mutation_ids: list[MutationId] + ) -> list[MutationInvocationRecord]: + """Retrieve all mutation records.""" + return [] + + async def find_all_entity_events_by_timestamp_desc( + self, entity_type: str, entity_ref_id: EntityId, offset: int, limit: int + ) -> tuple[list[MutationEntityEvent], int]: + """Retrieve all events on an entity in a given range.""" + return [], 0 + + async def find_all_entity_events_between( + self, + entity_type: str, + entity_ref_id: EntityId, + start: Timestamp, + end: Timestamp, + ) -> list[MutationEntityEvent]: + """Retrieve all events on an entity between two timestamps.""" + return [] + + async def find_all_entity_events_for_mutation( + self, mutation_id: MutationId + ) -> list[MutationEntityEvent]: + """Retrieve all entity events for a given mutation id.""" + return [] + + async def find_all_invocation_records_by_context_str( + self, context_str: str, offset: int, limit: int + ) -> tuple[list[MutationInvocationRecord], int]: + """Retrieve all invocation records for a given context with pagination.""" + return [], 0 + async def clear_all(self, context_str: str) -> None: """Clear all invocation records for a given context.""" diff --git a/src/alib/py/framework/jupiter/framework/mutation_inovcation/recorders/persistent.py b/src/alib/py/framework/jupiter/framework/mutation_inovcation/recorders/persistent.py index cde4db0b0..f463d97f5 100644 --- a/src/alib/py/framework/jupiter/framework/mutation_inovcation/recorders/persistent.py +++ b/src/alib/py/framework/jupiter/framework/mutation_inovcation/recorders/persistent.py @@ -4,7 +4,11 @@ from contextlib import AbstractAsyncContextManager from typing import Final -from jupiter.framework.mutation_inovcation.record import ( +from jupiter.framework.base.entity_id import EntityId +from jupiter.framework.base.mutation_id import MutationId +from jupiter.framework.base.timestamp import Timestamp +from jupiter.framework.mutation_inovcation.entity_event import MutationEntityEvent +from jupiter.framework.mutation_inovcation.invocation_record import ( MutationInvocationRecord, ) from jupiter.framework.mutation_inovcation.recorder import ( @@ -23,6 +27,49 @@ async def create( ) -> None: """Create a new invocation record.""" + @abc.abstractmethod + async def find_all( + self, + mutation_ids: list[MutationId], + ) -> list[MutationInvocationRecord]: + """Find all invocation records matching the given mutation ids.""" + + @abc.abstractmethod + async def find_all_entity_events_by_timestamp_desc( + self, + entity_type: str, + entity_ref_id: EntityId, + offset: int, + limit: int, + ) -> tuple[list[MutationEntityEvent], int]: + """Find all entity events in descending timestamp order with pagination.""" + + @abc.abstractmethod + async def find_all_entity_events_between( + self, + entity_type: str, + entity_ref_id: EntityId, + start: Timestamp, + end: Timestamp, + ) -> list[MutationEntityEvent]: + """Find all entity events between two timestamps.""" + + @abc.abstractmethod + async def find_all_entity_events_for_mutation( + self, + mutation_id: MutationId, + ) -> list[MutationEntityEvent]: + """Find all entity events for a given mutation id.""" + + @abc.abstractmethod + async def find_all_invocation_records_by_context_str( + self, + context_str: str, + offset: int, + limit: int, + ) -> tuple[list[MutationInvocationRecord], int]: + """Find all invocation records for a given context with pagination.""" + @abc.abstractmethod async def clear_all(self, context_str: str) -> None: """Clear all invocation record entries.""" @@ -68,6 +115,74 @@ async def record( invocation_record, ) + async def find_all_invocation_records( + self, + mutation_ids: list[MutationId], + ) -> list[MutationInvocationRecord]: + """Retrieve all mutation records.""" + if not mutation_ids: + return [] + async with self._storage_engine.get_unit_of_work() as uow: + return await uow.mutation_invocation_record_repository.find_all( + mutation_ids, + ) + + async def find_all_entity_events_by_timestamp_desc( + self, + entity_type: str, + entity_ref_id: EntityId, + offset: int, + limit: int, + ) -> tuple[list[MutationEntityEvent], int]: + """Retrieve all events on an entity in a given range.""" + async with self._storage_engine.get_unit_of_work() as uow: + return await uow.mutation_invocation_record_repository.find_all_entity_events_by_timestamp_desc( + entity_type, + entity_ref_id, + offset, + limit, + ) + + async def find_all_entity_events_between( + self, + entity_type: str, + entity_ref_id: EntityId, + start: Timestamp, + end: Timestamp, + ) -> list[MutationEntityEvent]: + """Retrieve all events on an entity between two timestamps.""" + async with self._storage_engine.get_unit_of_work() as uow: + return await uow.mutation_invocation_record_repository.find_all_entity_events_between( + entity_type, + entity_ref_id, + start, + end, + ) + + async def find_all_entity_events_for_mutation( + self, + mutation_id: MutationId, + ) -> list[MutationEntityEvent]: + """Retrieve all entity events for a given mutation id.""" + async with self._storage_engine.get_unit_of_work() as uow: + return await uow.mutation_invocation_record_repository.find_all_entity_events_for_mutation( + mutation_id, + ) + + async def find_all_invocation_records_by_context_str( + self, + context_str: str, + offset: int, + limit: int, + ) -> tuple[list[MutationInvocationRecord], int]: + """Retrieve all invocation records for a given context with pagination.""" + async with self._storage_engine.get_unit_of_work() as uow: + return await uow.mutation_invocation_record_repository.find_all_invocation_records_by_context_str( + context_str, + offset, + limit, + ) + async def clear_all(self, context_str: str) -> None: """Clear all invocation records for a given context.""" async with self._storage_engine.get_unit_of_work() as uow: diff --git a/src/alib/py/framework/jupiter/framework/realm/standard.py b/src/alib/py/framework/jupiter/framework/realm/standard.py index 67844459f..ef06ffe3a 100644 --- a/src/alib/py/framework/jupiter/framework/realm/standard.py +++ b/src/alib/py/framework/jupiter/framework/realm/standard.py @@ -926,7 +926,7 @@ def decode(self, value: RealmThing) -> _EntityT: all_fields = dataclasses.fields(self._the_type) - ctor_args: dict[str, DomainThing | ParentLink] = {} + ctor_args: dict[str, DomainThing | ParentLink[Entity]] = {} entity_id_decoder = self._realm_codec_registry.get_decoder( EntityId, self._realm @@ -1007,16 +1007,20 @@ def decode(self, value: RealmThing) -> _EntityT: ctor_args[field.name] = NOT_USED_NAME continue + is_parent_link = ( + field.type is ParentLink or get_origin(field.type) is ParentLink + ) + if field.name in value: field_value = value[field.name] - elif field.type is ParentLink and field.name + "_ref_id" in value: + elif is_parent_link and field.name + "_ref_id" in value: field_value = str(value[field.name + "_ref_id"]) else: raise RealmDecodingError( f"Expected value of type {self._the_type.__name__} to have field {field.name}" ) - if field.type is ParentLink: + if is_parent_link: ctor_args[field.name] = ParentLink( entity_id_decoder.decode(field_value) ) @@ -1101,7 +1105,7 @@ def decode(self, value: RealmThing) -> _RecordT: all_fields = dataclasses.fields(self._the_type) - ctor_args: dict[str, DomainThing | ParentLink] = {} + ctor_args: dict[str, DomainThing | ParentLink[Entity]] = {} entity_id_decoder = self._realm_codec_registry.get_decoder( EntityId, self._realm @@ -1128,16 +1132,20 @@ def decode(self, value: RealmThing) -> _RecordT: ): continue + is_parent_link = ( + field.type is ParentLink or get_origin(field.type) is ParentLink + ) + if field.name in value: field_value = value[field.name] - elif field.type is ParentLink and field.name + "_ref_id" in value: + elif is_parent_link and field.name + "_ref_id" in value: field_value = str(value[field.name + "_ref_id"]) else: raise RealmDecodingError( f"Expected value of type {self._the_type.__name__} to have field {field.name}" ) - if field.type is ParentLink: + if is_parent_link: ctor_args[field.name] = ParentLink( entity_id_decoder.decode(field_value) ) diff --git a/src/alib/py/framework/jupiter/framework/record.py b/src/alib/py/framework/jupiter/framework/record.py index db83c45cd..7ab09680f 100644 --- a/src/alib/py/framework/jupiter/framework/record.py +++ b/src/alib/py/framework/jupiter/framework/record.py @@ -164,7 +164,9 @@ def _get_real_type(cls: type[_RecordT]) -> tuple[type[_RecordT], bool]: if field.name == filter_name: found_field = field break - elif field.type is ParentLink and filter_name == field.name + "_ref_id": + elif ( + field.type is ParentLink or get_origin(field.type) is ParentLink + ) and filter_name == field.name + "_ref_id": found_field = field break else: @@ -177,11 +179,20 @@ def _get_real_type(cls: type[_RecordT]) -> tuple[type[_RecordT], bool]: found_field_type, found_field_optional = _get_real_type(found_field.type) # type: ignore[arg-type] + is_parent_link = ( + get_origin(found_field_type) is ParentLink + ) + if found_field_optional: if filter_rule is None: continue - if issubclass(found_field_type, AtomicValue): + if is_parent_link: + if not isinstance(filter_rule, IsRefId): # type: ignore[unreachable] + raise Exception( + f"Filter rule for '{filter_name}' is {filter_rule.__class__} which is not correct" + ) + elif issubclass(found_field_type, AtomicValue): if isinstance(filter_rule, AtomicValue): # type: ignore[unreachable] if found_field_type != filter_rule.__class__: raise Exception( @@ -194,7 +205,7 @@ def _get_real_type(cls: type[_RecordT]) -> tuple[type[_RecordT], bool]: elif isinstance(filter_rule, IsRefId) or isinstance( filter_rule, IsOneOfRefId ): - if found_field_type != EntityId and found_field_type != ParentLink: + if found_field_type != EntityId: raise Exception( f"Filter rule for '{filter_name}' is {filter_rule.__class__} which is not correct" ) @@ -215,7 +226,7 @@ def _get_real_type(cls: type[_RecordT]) -> tuple[type[_RecordT], bool]: elif isinstance(filter_rule, IsRefId) or isinstance( filter_rule, IsOneOfRefId ): - if found_field_type != EntityId and found_field_type != ParentLink: + if found_field_type != EntityId: raise Exception( f"Filter rule for '{filter_name}' is {filter_rule.__class__} which is not correct" ) @@ -223,7 +234,6 @@ def _get_real_type(cls: type[_RecordT]) -> tuple[type[_RecordT], bool]: raise Exception( f"Filter rule for '{filter_name}' is {filter_rule.__class__} which is not correct" ) - elif issubclass(found_field_type, ParentLink): if not isinstance(filter_rule, IsRefId): # type: ignore[unreachable] raise Exception( f"Filter rule for '{filter_name}' is {filter_rule.__class__} which is not correct" diff --git a/src/alib/py/framework/jupiter/framework/storage/sqlite/events.py b/src/alib/py/framework/jupiter/framework/storage/sqlite/events.py index a01de0509..fc709a11f 100644 --- a/src/alib/py/framework/jupiter/framework/storage/sqlite/events.py +++ b/src/alib/py/framework/jupiter/framework/storage/sqlite/events.py @@ -1,8 +1,14 @@ """Common toolin for SQLite repositories.""" +import json + from jupiter.framework.base.entity_id import EntityId +from jupiter.framework.base.mutation_id import MutationId +from jupiter.framework.base.timestamp import Timestamp +from jupiter.framework.base.trace_id import TraceId from jupiter.framework.entity import Entity -from jupiter.framework.event import Event +from jupiter.framework.event import Event, EventKind +from jupiter.framework.mutation_inovcation.entity_event import MutationEntityEvent from jupiter.framework.realm.realm import ( EncoderNotFoundError, EventStoreRealm, @@ -18,7 +24,9 @@ String, Table, delete, + func, insert, + select, ) from sqlalchemy.ext.asyncio import AsyncConnection @@ -99,6 +107,136 @@ def _serialize_event( return serialized_frame_args +async def find_entity_events_by_timestamp_desc( + realm_codec_registry: RealmCodecRegistry, + connection: AsyncConnection, + event_table: Table, + entity_type: str, + entity_ref_id: EntityId, + offset: int, + limit: int, +) -> tuple[list[MutationEntityEvent], int]: + """Find entity events paginated, ordered by timestamp descending.""" + if offset < 0: + raise ValueError("Offset must be non-negative but was {offset}") + if limit <= 0 or limit > 200: + raise ValueError("Limit must be between 1 and 200 but was {limit}") + base_where = [ + event_table.c.entity_type == entity_type, + event_table.c.entity_ref_id == entity_ref_id.as_int(), + ] + + count_stmt = select(func.count()).select_from(event_table).where(*base_where) + total_cnt = (await connection.execute(count_stmt)).scalar_one() + + query_stmt = ( + select(event_table) + .where(*base_where) + .order_by(event_table.c.timestamp.desc(), event_table.c.session_index.desc()) + .offset(offset) + .limit(limit) + ) + results = await connection.execute(query_stmt) + + events = [ + MutationEntityEvent( + entity_type=row.entity_type, + entity_ref_id=EntityId(str(row.entity_ref_id)), + entity_version=row.entity_version, + kind=EventKind(row.kind), + name=row.name, + trace_id=realm_codec_registry.db_decode(TraceId, row.trace_id), + mutation_id=realm_codec_registry.db_decode(MutationId, row.mutation_id), + timestamp=realm_codec_registry.db_decode(Timestamp, row.timestamp), + session_index=row.session_index, + source=row.source, + context_str=row.context_str, + data=json.dumps(row.data, indent=2) if row.data else "{}", + ) + for row in results + ] + + return events, total_cnt + + +async def find_entity_events_between( + realm_codec_registry: RealmCodecRegistry, + connection: AsyncConnection, + event_table: Table, + entity_type: str, + entity_ref_id: EntityId, + start: Timestamp, + end: Timestamp, +) -> list[MutationEntityEvent]: + """Find all entity events between two timestamps, ordered by timestamp descending.""" + if start > end: + raise ValueError("Start timestamp must be before end timestamp") + query_stmt = ( + select(event_table) + .where( + event_table.c.entity_type == entity_type, + event_table.c.entity_ref_id == entity_ref_id.as_int(), + event_table.c.timestamp >= realm_codec_registry.db_encode(start), + event_table.c.timestamp <= realm_codec_registry.db_encode(end), + ) + .order_by(event_table.c.timestamp.desc(), event_table.c.session_index.desc()) + ) + results = await connection.execute(query_stmt) + + return [ + MutationEntityEvent( + entity_type=row.entity_type, + entity_ref_id=EntityId(str(row.entity_ref_id)), + entity_version=row.entity_version, + kind=EventKind(row.kind), + name=row.name, + trace_id=realm_codec_registry.db_decode(TraceId, row.trace_id), + mutation_id=realm_codec_registry.db_decode(MutationId, row.mutation_id), + timestamp=realm_codec_registry.db_decode(Timestamp, row.timestamp), + session_index=row.session_index, + source=row.source, + context_str=row.context_str, + data=json.dumps(row.data, indent=2) if row.data else "{}", + ) + for row in results + ] + + +async def find_entity_events_by_mutation_id( + realm_codec_registry: RealmCodecRegistry, + connection: AsyncConnection, + event_table: Table, + mutation_id: MutationId, +) -> list[MutationEntityEvent]: + """Find all entity events for a given mutation id, ordered by timestamp descending.""" + query_stmt = ( + select(event_table) + .where( + event_table.c.mutation_id == realm_codec_registry.db_encode(mutation_id), + ) + .order_by(event_table.c.timestamp.desc(), event_table.c.session_index.desc()) + ) + results = await connection.execute(query_stmt) + + return [ + MutationEntityEvent( + entity_type=row.entity_type, + entity_ref_id=EntityId(str(row.entity_ref_id)), + entity_version=row.entity_version, + kind=EventKind(row.kind), + name=row.name, + trace_id=realm_codec_registry.db_decode(TraceId, row.trace_id), + mutation_id=realm_codec_registry.db_decode(MutationId, row.mutation_id), + timestamp=realm_codec_registry.db_decode(Timestamp, row.timestamp), + session_index=row.session_index, + source=row.source, + context_str=row.context_str, + data=json.dumps(row.data, indent=2) if row.data else "{}", + ) + for row in results + ] + + async def remove_events( connection: AsyncConnection, event_table: Table, diff --git a/src/alib/py/framework/jupiter/framework/storage/sqlite/repository.py b/src/alib/py/framework/jupiter/framework/storage/sqlite/repository.py index b5de26489..79fcc37ed 100644 --- a/src/alib/py/framework/jupiter/framework/storage/sqlite/repository.py +++ b/src/alib/py/framework/jupiter/framework/storage/sqlite/repository.py @@ -251,7 +251,7 @@ def _infer_entity_class(self) -> type[_EntityT]: def _get_parent_field_name(self) -> str: all_fields = dataclasses.fields(self._entity_type) for field in all_fields: - if field.type == ParentLink: + if field.type is ParentLink or get_origin(field.type) is ParentLink: return field.name + "_ref_id" raise Exception( @@ -319,7 +319,7 @@ def extract_field_type( field_type, field_optional = extract_field_type(field) - if field_type == ParentLink: + if field_type is ParentLink or get_origin(field_type) is ParentLink: if field_optional: raise Exception("Cannot have optional parent field") table.append_column( diff --git a/src/alib/py/framework/jupiter/framework/use_case.py b/src/alib/py/framework/jupiter/framework/use_case.py index 22c2339d4..9d36e42b9 100644 --- a/src/alib/py/framework/jupiter/framework/use_case.py +++ b/src/alib/py/framework/jupiter/framework/use_case.py @@ -33,7 +33,7 @@ GlobalProperties, UnavailableGloballyError, ) -from jupiter.framework.mutation_inovcation.record import ( +from jupiter.framework.mutation_inovcation.invocation_record import ( MutationInvocationRecord, ) from jupiter.framework.mutation_inovcation.recorder import ( @@ -264,6 +264,7 @@ async def execute( trace_id=context.domain_context.trace_id, mutation_id=context.domain_context.mutation_id, context_str=context.as_str(), + source=context.domain_context.event_source, timestamp=self._time_provider.get_current_time(), name=self.__class__.__name__, args=raw_args, @@ -283,6 +284,7 @@ async def execute( trace_id=context.trace_id, mutation_id=context.mutation_id, context_str=context.as_str(), + source=context.domain_context.event_source, timestamp=self._time_provider.get_current_time(), name=self.__class__.__name__, args=raw_args, @@ -324,16 +326,19 @@ class ReadonlyUseCase( """A command which only does reads.""" _realm_codec_registry: Final[RealmCodecRegistry] + _invocation_recorder: Final[MutationInvocationRecorder] def __init__( self, ports: _PortsT, global_properties: _GlobalPropertiesT, realm_codec_registry: RealmCodecRegistry, + invocation_recorder: MutationInvocationRecorder, ) -> None: """Constructor.""" super().__init__(ports, global_properties) self._realm_codec_registry = realm_codec_registry + self._invocation_recorder = invocation_recorder async def execute( self, @@ -502,10 +507,13 @@ def __init__( global_properties: _GlobalPropertiesT, time_provider: TimeProvider, realm_codec_registry: RealmCodecRegistry, + invocation_recorder: MutationInvocationRecorder, auth_token_stamper: AuthTokenStamper, ) -> None: """Constructor.""" - super().__init__(ports, global_properties, realm_codec_registry) + super().__init__( + ports, global_properties, realm_codec_registry, invocation_recorder + ) self._time_provider = time_provider self._auth_token_stamper = auth_token_stamper @@ -848,10 +856,13 @@ def __init__( global_properties: _GlobalPropertiesT, time_provider: TimeProvider, realm_codec_registry: RealmCodecRegistry, + invocation_recorder: MutationInvocationRecorder, auth_token_stamper: AuthTokenStamper, ) -> None: """Constructor.""" - super().__init__(ports, global_properties, realm_codec_registry) + super().__init__( + ports, global_properties, realm_codec_registry, invocation_recorder + ) self._time_provider = time_provider self._auth_token_stamper = auth_token_stamper diff --git a/src/alib/py/framework/jupiter/framework/utils/generic_support_entity_explorer.py b/src/alib/py/framework/jupiter/framework/utils/generic_support_entity_explorer.py new file mode 100644 index 000000000..ea71e22fc --- /dev/null +++ b/src/alib/py/framework/jupiter/framework/utils/generic_support_entity_explorer.py @@ -0,0 +1,38 @@ +"""A generic explorer for linked support entities.""" + +from jupiter.framework.base.entity_id import EntityId +from jupiter.framework.entity import ( + ContainsLink, + CrownEntity, + LeafSupportEntity, + OwnsLink, +) +from jupiter.framework.storage.repository import DomainUnitOfWork + + +async def generic_support_entity_explorer( + uow: DomainUnitOfWork, + entity: CrownEntity, +) -> list[tuple[str, EntityId]]: + """Return all linked LeafSupportEntity class names and ref ids owned or contained by an entity.""" + result: list[tuple[str, EntityId]] = [] + + for field in entity.__class__.__dict__.values(): + if not (isinstance(field, OwnsLink) or isinstance(field, ContainsLink)): + continue + if not issubclass(field.the_type, CrownEntity): + continue + + linked_entities = await uow.get_for(field.the_type).find_all_generic( + parent_ref_id=None, + allow_archived=True, + **field.get_for_entity(entity), + ) + + for linked_entity in linked_entities: + if isinstance(linked_entity, LeafSupportEntity): + result.append((linked_entity.__class__.__name__, linked_entity.ref_id)) + else: + result.extend(await generic_support_entity_explorer(uow, linked_entity)) + + return result diff --git a/src/cli/jupiter/cli/config.py b/src/cli/jupiter/cli/config.py index 0ada5ed6e..d1a7270b9 100644 --- a/src/cli/jupiter/cli/config.py +++ b/src/cli/jupiter/cli/config.py @@ -32,6 +32,7 @@ ) from jupiter.framework.appform.cli.exception import CliExceptionHandler from jupiter.framework.appform.cli.session_storage import SessionInfo +from jupiter.framework.base.trace_id import TraceId from jupiter.framework.service_properties import ServiceProperties from jupiter.framework.use_case_io import UseCaseResultBase @@ -125,6 +126,7 @@ def _build_session( # type: ignore distribution=AppDistribution.MAC_WEB, version=self._global_properties.version, ), + TraceId.new(), session_info.auth_token_ext if session_info else None, ) @@ -153,6 +155,7 @@ def _build_session( # type: ignore distribution=AppDistribution.MAC_WEB, version=self._global_properties.version, ), + TraceId.new(), session_info.auth_token_ext if session_info else None, ) @@ -181,6 +184,7 @@ def _build_session( # type: ignore distribution=AppDistribution.MAC_WEB, version=self._global_properties.version, ), + TraceId.new(), session_info.auth_token_ext, ) @@ -209,6 +213,7 @@ def _build_session( # type: ignore distribution=AppDistribution.MAC_WEB, version=self._global_properties.version, ), + TraceId.new(), session_info.auth_token_ext, ) diff --git a/src/core/jupiter/core/api_key/root.py b/src/core/jupiter/core/api_key/root.py index c2d8981ea..83f8c4aed 100644 --- a/src/core/jupiter/core/api_key/root.py +++ b/src/core/jupiter/core/api_key/root.py @@ -1,5 +1,7 @@ """An API key.""" +from typing import TYPE_CHECKING + from jupiter.core.api_key.api_key_summary import APIKeySummary from jupiter.core.api_key.key_secret_hash import KeySecretHash from jupiter.core.api_key.key_secret_plain import KeySecretPlain @@ -17,6 +19,9 @@ from jupiter.framework.secure import secure_class from jupiter.framework.update_action import UpdateAction +if TYPE_CHECKING: + from jupiter.core.users.root import User + class InvalidAPIKeyError(Exception): """Error raised when the API key is invalid.""" @@ -28,7 +33,7 @@ class InvalidAPIKeyError(Exception): class APIKey(LeafEntity): """An API key.""" - user: ParentLink + user: ParentLink["User"] name: APIKeyName key_hash: KeySecretHash key_size: int diff --git a/src/core/jupiter/core/auth/root.py b/src/core/jupiter/core/auth/root.py index 3eda2096c..e79a7cb85 100644 --- a/src/core/jupiter/core/auth/root.py +++ b/src/core/jupiter/core/auth/root.py @@ -1,5 +1,7 @@ """Authentication information associated with a user.""" +from typing import TYPE_CHECKING + from jupiter.core.auth.password_hash import PasswordHash from jupiter.core.auth.password_new_plain import PasswordNewPlain from jupiter.core.auth.password_plain import PasswordPlain @@ -18,6 +20,9 @@ from jupiter.framework.realm.realm import DatabaseRealm, only_in_realm from jupiter.framework.secure import secure_class +if TYPE_CHECKING: + from jupiter.core.users.root import User + class IncorrectPasswordError(Exception): """Exception raised when an invalid password is provided.""" @@ -33,7 +38,7 @@ class IncorrectRecoveryTokenError(Exception): class Auth(StubEntity): """Authentication information associated with a user.""" - user: ParentLink + user: ParentLink["User"] password_hash: PasswordHash recovery_token_hash: RecoveryTokenHash diff --git a/src/core/jupiter/core/big_plans/collection.py b/src/core/jupiter/core/big_plans/collection.py index 8c0d63622..26d7c9a33 100644 --- a/src/core/jupiter/core/big_plans/collection.py +++ b/src/core/jupiter/core/big_plans/collection.py @@ -1,5 +1,7 @@ """A big plan collection.""" +from typing import TYPE_CHECKING + from jupiter.core.big_plans.root import BigPlan from jupiter.framework.base.entity_id import EntityId from jupiter.framework.context import DomainContext @@ -12,12 +14,15 @@ entity, ) +if TYPE_CHECKING: + from jupiter.core.workspaces.root import Workspace + @entity class BigPlanCollection(TrunkEntity): """A big plan collection.""" - workspace: ParentLink + workspace: ParentLink["Workspace"] big_plans = ContainsMany(BigPlan, big_plan_collection_ref_id=IsRefId()) diff --git a/src/core/jupiter/core/big_plans/root.py b/src/core/jupiter/core/big_plans/root.py index c5b80f81a..ca7c5ec8c 100644 --- a/src/core/jupiter/core/big_plans/root.py +++ b/src/core/jupiter/core/big_plans/root.py @@ -1,7 +1,7 @@ """A big plan.""" import abc -from typing import Iterable +from typing import TYPE_CHECKING, Iterable from jupiter.core.archival_reason import JupiterArchivalReason from jupiter.core.big_plans.name import BigPlanName @@ -36,12 +36,15 @@ from jupiter.framework.storage.repository import LeafEntityRepository from jupiter.framework.update_action import UpdateAction +if TYPE_CHECKING: + from jupiter.core.big_plans.collection import BigPlanCollection + @entity class BigPlan(LeafEntity): """A big plan.""" - big_plan_collection: ParentLink + big_plan_collection: ParentLink["BigPlanCollection"] aspect_ref_id: EntityId chapter_ref_id: EntityId | None goal_ref_id: EntityId | None diff --git a/src/core/jupiter/core/big_plans/stats.py b/src/core/jupiter/core/big_plans/stats.py index 2cf4659f3..64c8a8aee 100644 --- a/src/core/jupiter/core/big_plans/stats.py +++ b/src/core/jupiter/core/big_plans/stats.py @@ -1,6 +1,7 @@ """Stats about a big plan.""" import abc +from typing import TYPE_CHECKING from jupiter.framework.base.entity_id import EntityId from jupiter.framework.context import DomainContext @@ -9,12 +10,15 @@ from jupiter.framework.record import Record, create_record_action, record from jupiter.framework.storage.repository import RecordRepository +if TYPE_CHECKING: + from jupiter.core.big_plans.root import BigPlan + @record class BigPlanStats(Record): """Stats about a big plan.""" - big_plan: ParentLink + big_plan: ParentLink["BigPlan"] all_inbox_tasks_cnt: int completed_inbox_tasks_cnt: int diff --git a/src/core/jupiter/core/big_plans/sub/milestones/root.py b/src/core/jupiter/core/big_plans/sub/milestones/root.py index cfc1190ba..92bb338af 100644 --- a/src/core/jupiter/core/big_plans/sub/milestones/root.py +++ b/src/core/jupiter/core/big_plans/sub/milestones/root.py @@ -1,6 +1,7 @@ """A milestone for a big plan.""" import abc +from typing import TYPE_CHECKING from jupiter.framework.base.adate import ADate from jupiter.framework.base.entity_id import EntityId @@ -19,6 +20,9 @@ ) from jupiter.framework.update_action import UpdateAction +if TYPE_CHECKING: + from jupiter.core.big_plans.root import BigPlan + class BigPlanMilestoneAlreadyExistsForDateError(EntityAlreadyExistsError): """A big plan milestone already exists for the given date.""" @@ -28,7 +32,7 @@ class BigPlanMilestoneAlreadyExistsForDateError(EntityAlreadyExistsError): class BigPlanMilestone(LeafEntity): """A milestone for tracking progress of a big plan.""" - big_plan: ParentLink + big_plan: ParentLink["BigPlan"] date: ADate name: EntityName diff --git a/src/core/jupiter/core/chores/collection.py b/src/core/jupiter/core/chores/collection.py index d8b739585..a7d260d86 100644 --- a/src/core/jupiter/core/chores/collection.py +++ b/src/core/jupiter/core/chores/collection.py @@ -1,5 +1,7 @@ """A chore collection.""" +from typing import TYPE_CHECKING + from jupiter.core.chores.root import Chore from jupiter.framework.base.entity_id import EntityId from jupiter.framework.context import DomainContext @@ -12,12 +14,15 @@ entity, ) +if TYPE_CHECKING: + from jupiter.core.workspaces.root import Workspace + @entity class ChoreCollection(TrunkEntity): """A chore collection.""" - workspace: ParentLink + workspace: ParentLink["Workspace"] chores = ContainsMany(Chore, chore_collection_ref_id=IsRefId()) diff --git a/src/core/jupiter/core/chores/root.py b/src/core/jupiter/core/chores/root.py index e8f28a3a2..402212576 100644 --- a/src/core/jupiter/core/chores/root.py +++ b/src/core/jupiter/core/chores/root.py @@ -1,5 +1,7 @@ """A chore.""" +from typing import TYPE_CHECKING + from jupiter.core.chores.name import ChoreName from jupiter.core.common.recurring_task_gen_params import RecurringTaskGenParams from jupiter.core.common.sub.inbox_tasks.root import InboxTask @@ -24,12 +26,15 @@ from jupiter.framework.errors import InputValidationError from jupiter.framework.update_action import UpdateAction +if TYPE_CHECKING: + from jupiter.core.chores.collection import ChoreCollection + @entity class Chore(LeafEntity): """A chore.""" - chore_collection: ParentLink + chore_collection: ParentLink["ChoreCollection"] aspect_ref_id: EntityId chapter_ref_id: EntityId | None goal_ref_id: EntityId | None diff --git a/src/core/jupiter/core/common/sub/contacts/root.py b/src/core/jupiter/core/common/sub/contacts/root.py index 4769e6b94..60831a411 100644 --- a/src/core/jupiter/core/common/sub/contacts/root.py +++ b/src/core/jupiter/core/common/sub/contacts/root.py @@ -1,5 +1,7 @@ """Contacts domain trunk entity.""" +from typing import TYPE_CHECKING + from jupiter.core.common.sub.contacts.sub.contact.root import Contact from jupiter.core.common.sub.contacts.sub.link.root import ContactLink from jupiter.framework.base.entity_id import EntityId @@ -12,12 +14,15 @@ entity, ) +if TYPE_CHECKING: + from jupiter.core.workspaces.root import Workspace + @entity class ContactDomain(TrunkEntity): """Contacts trunk entity.""" - workspace: ParentLink + workspace: ParentLink["Workspace"] contacts = ContainsMany(Contact, contact_domain_ref_id=IsRefId()) links = ContainsMany(ContactLink, contact_domain_ref_id=IsRefId()) diff --git a/src/core/jupiter/core/common/sub/contacts/sub/contact/root.py b/src/core/jupiter/core/common/sub/contacts/sub/contact/root.py index 41eed2924..fd0dc0976 100644 --- a/src/core/jupiter/core/common/sub/contacts/sub/contact/root.py +++ b/src/core/jupiter/core/common/sub/contacts/sub/contact/root.py @@ -1,6 +1,7 @@ """A contact.""" import abc +from typing import TYPE_CHECKING from jupiter.core.common.sub.contacts.sub.contact.name import ContactName from jupiter.framework.base.entity_id import EntityId @@ -18,6 +19,9 @@ ) from jupiter.framework.update_action import UpdateAction +if TYPE_CHECKING: + from jupiter.core.common.sub.contacts.root import ContactDomain + class ContactAlreadyExistsError(EntityAlreadyExistsError): """Error raised when a contact already exists.""" @@ -31,7 +35,7 @@ class ContactInSignificantUseError(Exception): class Contact(LeafSupportEntity): """A contact.""" - contact_domain: ParentLink + contact_domain: ParentLink["ContactDomain"] name: ContactName @staticmethod diff --git a/src/core/jupiter/core/common/sub/contacts/sub/link/root.py b/src/core/jupiter/core/common/sub/contacts/sub/link/root.py index 8301e3eed..bd6c84c00 100644 --- a/src/core/jupiter/core/common/sub/contacts/sub/link/root.py +++ b/src/core/jupiter/core/common/sub/contacts/sub/link/root.py @@ -1,6 +1,7 @@ """A link between an entity and its contacts.""" import abc +from typing import TYPE_CHECKING from jupiter.core.common.sub.contacts.namespace import ContactNamespace from jupiter.core.common.sub.contacts.sub.contact.root import Contact @@ -19,12 +20,15 @@ from jupiter.framework.storage.repository import LeafEntityRepository from jupiter.framework.update_action import UpdateAction +if TYPE_CHECKING: + from jupiter.core.common.sub.contacts.root import ContactDomain + @entity class ContactLink(LeafSupportEntity): """A link between an entity and its contacts.""" - contact_domain: ParentLink + contact_domain: ParentLink["ContactDomain"] namespace: ContactNamespace source_entity_ref_id: EntityId diff --git a/src/core/jupiter/core/common/sub/inbox_tasks/collection.py b/src/core/jupiter/core/common/sub/inbox_tasks/collection.py index 937c7c5aa..1ac181b4d 100644 --- a/src/core/jupiter/core/common/sub/inbox_tasks/collection.py +++ b/src/core/jupiter/core/common/sub/inbox_tasks/collection.py @@ -1,5 +1,7 @@ """A inbox task collection.""" +from typing import TYPE_CHECKING + from jupiter.core.common.sub.inbox_tasks.root import InboxTask from jupiter.framework.base.entity_id import EntityId from jupiter.framework.context import DomainContext @@ -12,12 +14,15 @@ entity, ) +if TYPE_CHECKING: + from jupiter.core.workspaces.root import Workspace + @entity class InboxTaskCollection(TrunkEntity): """A inbox task collection.""" - workspace: ParentLink + workspace: ParentLink["Workspace"] inbox_tasks = ContainsMany(InboxTask, inbox_task_collection_ref_id=IsRefId()) diff --git a/src/core/jupiter/core/common/sub/inbox_tasks/root.py b/src/core/jupiter/core/common/sub/inbox_tasks/root.py index b5cbd9f6e..cf15c16df 100644 --- a/src/core/jupiter/core/common/sub/inbox_tasks/root.py +++ b/src/core/jupiter/core/common/sub/inbox_tasks/root.py @@ -3,7 +3,7 @@ import abc import textwrap from collections.abc import Iterable -from typing import ClassVar +from typing import TYPE_CHECKING, ClassVar from jupiter.core.archival_reason import JupiterArchivalReason from jupiter.core.common.difficulty import Difficulty @@ -42,6 +42,9 @@ from jupiter.framework.storage.repository import LeafEntityRepository from jupiter.framework.update_action import UpdateAction +if TYPE_CHECKING: + from jupiter.core.common.sub.inbox_tasks.collection import InboxTaskCollection + class CannotModifyGeneratedTaskError(Exception): """Exception raised when you're trying to modify a generated task.""" @@ -58,7 +61,7 @@ def __init__(self, field: str) -> None: class InboxTask(LeafEntity): """An inbox task.""" - inbox_task_collection: ParentLink + inbox_task_collection: ParentLink["InboxTaskCollection"] source: InboxTaskSource name: InboxTaskName status: InboxTaskStatus diff --git a/src/core/jupiter/core/common/sub/notes/collection.py b/src/core/jupiter/core/common/sub/notes/collection.py index 92b510532..26851e690 100644 --- a/src/core/jupiter/core/common/sub/notes/collection.py +++ b/src/core/jupiter/core/common/sub/notes/collection.py @@ -1,5 +1,7 @@ """The note collection.""" +from typing import TYPE_CHECKING + from jupiter.core.common.sub.notes.root import Note from jupiter.framework.base.entity_id import EntityId from jupiter.framework.context import DomainContext @@ -11,12 +13,15 @@ entity, ) +if TYPE_CHECKING: + from jupiter.core.workspaces.root import Workspace + @entity class NoteCollection(TrunkEntity): """A note collection.""" - workspace: ParentLink + workspace: ParentLink["Workspace"] notes = ContainsMany(Note, note_collection_ref_id=IsRefId()) diff --git a/src/core/jupiter/core/common/sub/notes/root.py b/src/core/jupiter/core/common/sub/notes/root.py index 294750f9d..658bc6656 100644 --- a/src/core/jupiter/core/common/sub/notes/root.py +++ b/src/core/jupiter/core/common/sub/notes/root.py @@ -1,6 +1,7 @@ """A note in the notebook.""" import abc +from typing import TYPE_CHECKING from jupiter.core.common.sub.notes.content_block import OneOfNoteContentBlock from jupiter.core.common.sub.notes.namespace import NoteNamespace @@ -17,12 +18,15 @@ from jupiter.framework.storage.repository import LeafEntityRepository from jupiter.framework.update_action import UpdateAction +if TYPE_CHECKING: + from jupiter.core.common.sub.notes.collection import NoteCollection + @entity class Note(LeafSupportEntity): """A note in the notebook.""" - note_collection: ParentLink + note_collection: ParentLink["NoteCollection"] namespace: NoteNamespace source_entity_ref_id: EntityId content: list[OneOfNoteContentBlock] diff --git a/src/core/jupiter/core/common/sub/tags/root.py b/src/core/jupiter/core/common/sub/tags/root.py index 1c18e35a7..2d08f2e10 100644 --- a/src/core/jupiter/core/common/sub/tags/root.py +++ b/src/core/jupiter/core/common/sub/tags/root.py @@ -1,5 +1,7 @@ """Tags domain trunk entity.""" +from typing import TYPE_CHECKING + from jupiter.core.common.sub.tags.sub.link.root import TagLink from jupiter.core.common.sub.tags.sub.tag.root import Tag from jupiter.framework.base.entity_id import EntityId @@ -12,12 +14,15 @@ entity, ) +if TYPE_CHECKING: + from jupiter.core.workspaces.root import Workspace + @entity class TagDomain(TrunkEntity): """Tags trunk entity.""" - workspace: ParentLink + workspace: ParentLink["Workspace"] tags = ContainsMany(Tag, tag_domain_ref_id=IsRefId()) links = ContainsMany(TagLink, tag_domain_ref_id=IsRefId()) diff --git a/src/core/jupiter/core/common/sub/tags/sub/link/root.py b/src/core/jupiter/core/common/sub/tags/sub/link/root.py index 992339342..48643d8a6 100644 --- a/src/core/jupiter/core/common/sub/tags/sub/link/root.py +++ b/src/core/jupiter/core/common/sub/tags/sub/link/root.py @@ -1,6 +1,7 @@ """A link between an entity and its tags.""" import abc +from typing import TYPE_CHECKING from jupiter.core.common.sub.tags.namespace import TagNamespace from jupiter.core.common.sub.tags.sub.tag.root import Tag @@ -19,12 +20,15 @@ from jupiter.framework.storage.repository import LeafEntityRepository from jupiter.framework.update_action import UpdateAction +if TYPE_CHECKING: + from jupiter.core.common.sub.tags.root import TagDomain + @entity class TagLink(LeafSupportEntity): """A link between an entity and its tags.""" - tag_domain: ParentLink + tag_domain: ParentLink["TagDomain"] namespace: TagNamespace source_entity_ref_id: EntityId diff --git a/src/core/jupiter/core/common/sub/tags/sub/tag/root.py b/src/core/jupiter/core/common/sub/tags/sub/tag/root.py index 70b295ea3..167dfe974 100644 --- a/src/core/jupiter/core/common/sub/tags/sub/tag/root.py +++ b/src/core/jupiter/core/common/sub/tags/sub/tag/root.py @@ -1,6 +1,7 @@ """A tag.""" import abc +from typing import TYPE_CHECKING from jupiter.core.common.sub.tags.namespace import TagNamespace from jupiter.core.common.sub.tags.sub.tag.name import TagName @@ -19,6 +20,9 @@ ) from jupiter.framework.update_action import UpdateAction +if TYPE_CHECKING: + from jupiter.core.common.sub.tags.root import TagDomain + class TagAlreadyExistsError(EntityAlreadyExistsError): """Error raised when a tag already exists.""" @@ -28,7 +32,7 @@ class TagAlreadyExistsError(EntityAlreadyExistsError): class Tag(LeafSupportEntity): """A tag.""" - tag_domain: ParentLink + tag_domain: ParentLink["TagDomain"] namespace: TagNamespace name: TagName diff --git a/src/core/jupiter/core/common/sub/time_events/domain.py b/src/core/jupiter/core/common/sub/time_events/domain.py index c9aeae42a..ba2344305 100644 --- a/src/core/jupiter/core/common/sub/time_events/domain.py +++ b/src/core/jupiter/core/common/sub/time_events/domain.py @@ -1,5 +1,7 @@ """Time event domain trunk entity.""" +from typing import TYPE_CHECKING + from jupiter.core.common.sub.time_events.sub.full_days_block.root import ( TimeEventFullDaysBlock, ) @@ -16,12 +18,15 @@ entity, ) +if TYPE_CHECKING: + from jupiter.core.workspaces.root import Workspace + @entity class TimeEventDomain(TrunkEntity): """Time event trunk entity.""" - workspace: ParentLink + workspace: ParentLink["Workspace"] in_day_blocks = ContainsMany( TimeEventInDayBlock, time_event_domain_ref_id=IsRefId() diff --git a/src/core/jupiter/core/common/sub/time_events/sub/full_days_block/root.py b/src/core/jupiter/core/common/sub/time_events/sub/full_days_block/root.py index cce6802e1..601376a96 100644 --- a/src/core/jupiter/core/common/sub/time_events/sub/full_days_block/root.py +++ b/src/core/jupiter/core/common/sub/time_events/sub/full_days_block/root.py @@ -1,6 +1,7 @@ """A full day block of time.""" import abc +from typing import TYPE_CHECKING from jupiter.core.common.sub.time_events.namespace import ( TimeEventNamespace, @@ -21,12 +22,15 @@ from jupiter.framework.update_action import UpdateAction from jupiter.framework.value import CompositeValue, value +if TYPE_CHECKING: + from jupiter.core.common.sub.time_events.domain import TimeEventDomain + @entity class TimeEventFullDaysBlock(LeafSupportEntity): """A full day block of time.""" - time_event_domain: ParentLink + time_event_domain: ParentLink["TimeEventDomain"] namespace: TimeEventNamespace source_entity_ref_id: EntityId diff --git a/src/core/jupiter/core/common/sub/time_events/sub/in_day_block/root.py b/src/core/jupiter/core/common/sub/time_events/sub/in_day_block/root.py index b7b0a61ea..f6129ae22 100644 --- a/src/core/jupiter/core/common/sub/time_events/sub/in_day_block/root.py +++ b/src/core/jupiter/core/common/sub/time_events/sub/in_day_block/root.py @@ -1,6 +1,7 @@ """Time event.""" import abc +from typing import TYPE_CHECKING from jupiter.core.common.sub.time_events.namespace import ( TimeEventNamespace, @@ -26,12 +27,15 @@ MIN_DURATION_MINS = 1 MAX_DURATION_MINS = 2 * 24 * 60 # 48 hours +if TYPE_CHECKING: + from jupiter.core.common.sub.time_events.domain import TimeEventDomain + @entity class TimeEventInDayBlock(LeafSupportEntity): """Time event.""" - time_event_domain: ParentLink + time_event_domain: ParentLink["TimeEventDomain"] namespace: TimeEventNamespace source_entity_ref_id: EntityId diff --git a/src/core/jupiter/core/config.py b/src/core/jupiter/core/config.py index 16f64b251..8f2eb56fb 100644 --- a/src/core/jupiter/core/config.py +++ b/src/core/jupiter/core/config.py @@ -28,6 +28,7 @@ from jupiter.core.users.root import User from jupiter.core.workspaces.root import Workspace from jupiter.framework.auth.auth_token import AuthToken +from jupiter.framework.base.entity_id import EntityId, EntityIdDatabaseDecoder from jupiter.framework.component_properties import ComponentProperties from jupiter.framework.context import DomainContext from jupiter.framework.global_properties import GlobalProperties @@ -55,6 +56,8 @@ _UseCaseArgsT = TypeVar("_UseCaseArgsT", bound=UseCaseArgsBase) _UseCaseResultT = TypeVar("_UseCaseResultT", bound=Union[None, UseCaseResultBase]) +_ENTITY_ID_DECODER = EntityIdDatabaseDecoder() + @dataclass(frozen=True) class JupiterPorts(DomainPorts): @@ -303,6 +306,19 @@ def as_str(self) -> str: """The string representation of the context.""" return f"user:{self.user.ref_id}+workspace:{self.workspace.ref_id}" + @staticmethod + def unwrap_str(context_str: str) -> tuple[EntityId, EntityId]: + """Unwrap the context string into a tuple of user and workspace IDs.""" + try: + part_user, part_workspace = context_str.split("+") + _, user_id = part_user.split(":") + _, workspace_id = part_workspace.split(":") + return _ENTITY_ID_DECODER.decode(user_id), _ENTITY_ID_DECODER.decode( + workspace_id + ) + except ValueError as e: + raise Exception("Could not unwrap context str") from e + def allows( self, only_for: list[EnumValue | list[EnumValue]] | None ) -> EnumValue | None: @@ -342,6 +358,19 @@ def as_str(self) -> str: """The string representation of the context.""" return f"user:{self.user.ref_id}+workspace:{self.workspace.ref_id}" + @staticmethod + def unwrap_str(context_str: str) -> tuple[EntityId, EntityId]: + """Unwrap the context string into a tuple of user and workspace IDs.""" + try: + part_user, part_workspace = context_str.split("+") + _, user_id = part_user.split(":") + _, workspace_id = part_workspace.split(":") + return _ENTITY_ID_DECODER.decode(user_id), _ENTITY_ID_DECODER.decode( + workspace_id + ) + except ValueError as e: + raise Exception("Could not unwrap context str") from e + def allows( self, only_for: list[EnumValue | list[EnumValue]] | None ) -> EnumValue | None: diff --git a/src/core/jupiter/core/docs/collection.py b/src/core/jupiter/core/docs/collection.py index d3968c31e..45b89d1bd 100644 --- a/src/core/jupiter/core/docs/collection.py +++ b/src/core/jupiter/core/docs/collection.py @@ -1,6 +1,7 @@ """The doc collection.""" import abc +from typing import TYPE_CHECKING from jupiter.core.docs.root import Doc from jupiter.framework.base.entity_id import EntityId @@ -15,12 +16,15 @@ ) from jupiter.framework.storage.repository import TrunkEntityRepository +if TYPE_CHECKING: + from jupiter.core.workspaces.root import Workspace + @entity class DocCollection(TrunkEntity): """A doc collection.""" - workspace: ParentLink + workspace: ParentLink["Workspace"] docs = ContainsMany(Doc, doc_collection_ref_id=IsRefId()) diff --git a/src/core/jupiter/core/docs/root.py b/src/core/jupiter/core/docs/root.py index 075faf09a..305cd93ed 100644 --- a/src/core/jupiter/core/docs/root.py +++ b/src/core/jupiter/core/docs/root.py @@ -1,6 +1,7 @@ """A doc in the docbook.""" import abc +from typing import TYPE_CHECKING from jupiter.core.common.sub.notes.namespace import NoteNamespace from jupiter.core.common.sub.notes.root import Note @@ -23,12 +24,15 @@ from jupiter.framework.storage.repository import LeafEntityRepository from jupiter.framework.update_action import UpdateAction +if TYPE_CHECKING: + from jupiter.core.docs.collection import DocCollection + @entity class Doc(LeafEntity): """A doc in the docbook.""" - doc_collection: ParentLink + doc_collection: ParentLink["DocCollection"] parent_doc_ref_id: EntityId | None idempotency_key: DocIdempotencyKey name: DocName diff --git a/src/core/jupiter/core/gamification/score_log.py b/src/core/jupiter/core/gamification/score_log.py index a21cbf7f4..958014b3f 100644 --- a/src/core/jupiter/core/gamification/score_log.py +++ b/src/core/jupiter/core/gamification/score_log.py @@ -1,6 +1,7 @@ """A container for all the scores a user has.""" import abc +from typing import TYPE_CHECKING from jupiter.core.gamification.score_log_entry import ScoreLogEntry from jupiter.core.gamification.score_period_best import ( @@ -19,12 +20,15 @@ from jupiter.framework.record import ContainsManyRecords from jupiter.framework.storage.repository import TrunkEntityRepository +if TYPE_CHECKING: + from jupiter.core.users.root import User + @entity class ScoreLog(TrunkEntity): """a log of the scores a user receives.""" - user: ParentLink + user: ParentLink["User"] entries = ContainsMany(ScoreLogEntry, score_log_ref_id=IsRefId()) period_bests = ContainsManyRecords(ScorePeriodBest, score_log_ref_id=IsRefId()) diff --git a/src/core/jupiter/core/gamification/score_log_entry.py b/src/core/jupiter/core/gamification/score_log_entry.py index d5e9bdcd0..ccd8a344a 100644 --- a/src/core/jupiter/core/gamification/score_log_entry.py +++ b/src/core/jupiter/core/gamification/score_log_entry.py @@ -2,6 +2,7 @@ import abc import random +from typing import TYPE_CHECKING from jupiter.core.big_plans.root import BigPlan from jupiter.core.big_plans.status import BigPlanStatus @@ -20,12 +21,15 @@ ) from jupiter.framework.storage.repository import LeafEntityRepository +if TYPE_CHECKING: + from jupiter.core.gamification.score_log import ScoreLog + @entity class ScoreLogEntry(LeafEntity): """A record of a win or loss in accomplishing a task.""" - score_log: ParentLink + score_log: ParentLink["ScoreLog"] source: ScoreSource task_ref_id: EntityId difficulty: Difficulty | None diff --git a/src/core/jupiter/core/gamification/score_period_best.py b/src/core/jupiter/core/gamification/score_period_best.py index 3161d8d85..c571e71a4 100644 --- a/src/core/jupiter/core/gamification/score_period_best.py +++ b/src/core/jupiter/core/gamification/score_period_best.py @@ -1,6 +1,7 @@ """The best score for a period of time and a particular subdivision of it.""" import abc +from typing import TYPE_CHECKING from jupiter.core.common.recurring_task_period import RecurringTaskPeriod from jupiter.core.gamification.score_stats import ScoreStats @@ -17,12 +18,15 @@ ) from jupiter.framework.storage.repository import RecordRepository +if TYPE_CHECKING: + from jupiter.core.gamification.score_log import ScoreLog + @record class ScorePeriodBest(Record): """The best score for a period of time and a particular subdivision of it.""" - score_log: ParentLink + score_log: ParentLink["ScoreLog"] period: RecurringTaskPeriod | None timeline: str sub_period: RecurringTaskPeriod diff --git a/src/core/jupiter/core/gamification/score_stats.py b/src/core/jupiter/core/gamification/score_stats.py index 81f7f44d9..ff18426a3 100644 --- a/src/core/jupiter/core/gamification/score_stats.py +++ b/src/core/jupiter/core/gamification/score_stats.py @@ -1,6 +1,7 @@ """Statistics about scores for a particular time interval.""" import abc +from typing import TYPE_CHECKING from jupiter.core.common.recurring_task_period import RecurringTaskPeriod from jupiter.core.gamification.score_log_entry import ScoreLogEntry @@ -16,12 +17,15 @@ from jupiter.framework.record import Record, create_record_action, record from jupiter.framework.storage.repository import RecordRepository +if TYPE_CHECKING: + from jupiter.core.gamification.score_log import ScoreLog + @record class ScoreStats(Record): """Statistics about scores for a particular time interval.""" - score_log: ParentLink + score_log: ParentLink["ScoreLog"] period: RecurringTaskPeriod | None timeline: str total_score: int diff --git a/src/core/jupiter/core/gc/log.py b/src/core/jupiter/core/gc/log.py index 7ac174904..b6cdf0b86 100644 --- a/src/core/jupiter/core/gc/log.py +++ b/src/core/jupiter/core/gc/log.py @@ -1,5 +1,7 @@ """A GC log attched to a workspace.""" +from typing import TYPE_CHECKING + from jupiter.core.gc.log_entry import GCLogEntry from jupiter.framework.base.entity_id import EntityId from jupiter.framework.context import DomainContext @@ -12,12 +14,15 @@ entity, ) +if TYPE_CHECKING: + from jupiter.core.workspaces.root import Workspace + @entity class GCLog(TrunkEntity): """A log of GC actions a user has performed.""" - workspace: ParentLink + workspace: ParentLink["Workspace"] entries = ContainsMany(GCLogEntry, gc_log_ref_id=IsRefId()) diff --git a/src/core/jupiter/core/gc/log_entry.py b/src/core/jupiter/core/gc/log_entry.py index 953539de3..f59504fa6 100644 --- a/src/core/jupiter/core/gc/log_entry.py +++ b/src/core/jupiter/core/gc/log_entry.py @@ -1,6 +1,7 @@ """A particular entry in the GC log.""" import abc +from typing import TYPE_CHECKING from jupiter.core.common.entity_summary import EntitySummary from jupiter.core.sync_target import SyncTarget @@ -18,12 +19,15 @@ ) from jupiter.framework.storage.repository import LeafEntityRepository +if TYPE_CHECKING: + from jupiter.core.gc.log import GCLog + @entity class GCLogEntry(LeafEntity): """A particular entry in the GC log.""" - gc_log: ParentLink + gc_log: ParentLink["GCLog"] source: str gc_targets: list[SyncTarget] opened: bool diff --git a/src/core/jupiter/core/gen/log.py b/src/core/jupiter/core/gen/log.py index 624587806..c2ae4856c 100644 --- a/src/core/jupiter/core/gen/log.py +++ b/src/core/jupiter/core/gen/log.py @@ -1,5 +1,7 @@ """A task generation log attched to a workspace.""" +from typing import TYPE_CHECKING + from jupiter.core.gen.log_entry import GenLogEntry from jupiter.framework.base.entity_id import EntityId from jupiter.framework.context import DomainContext @@ -12,12 +14,15 @@ entity, ) +if TYPE_CHECKING: + from jupiter.core.workspaces.root import Workspace + @entity class GenLog(TrunkEntity): """A log of task generation actions a user has performed.""" - workspace: ParentLink + workspace: ParentLink["Workspace"] entries = ContainsMany(GenLogEntry, gen_log_ref_id=IsRefId()) diff --git a/src/core/jupiter/core/gen/log_entry.py b/src/core/jupiter/core/gen/log_entry.py index 2b4836e1c..79cdfedc6 100644 --- a/src/core/jupiter/core/gen/log_entry.py +++ b/src/core/jupiter/core/gen/log_entry.py @@ -1,6 +1,7 @@ """A particular entry in the task generation log.""" import abc +from typing import TYPE_CHECKING from jupiter.core.common.entity_summary import EntitySummary from jupiter.core.common.recurring_task_period import RecurringTaskPeriod @@ -20,12 +21,15 @@ ) from jupiter.framework.storage.repository import LeafEntityRepository +if TYPE_CHECKING: + from jupiter.core.gen.log import GenLog + @entity class GenLogEntry(LeafSupportEntity): """A particular entry in the task generation log.""" - gen_log: ParentLink + gen_log: ParentLink["GenLog"] source: str gen_even_if_not_modified: bool today: ADate diff --git a/src/core/jupiter/core/habits/collection.py b/src/core/jupiter/core/habits/collection.py index c98124f89..969bb180f 100644 --- a/src/core/jupiter/core/habits/collection.py +++ b/src/core/jupiter/core/habits/collection.py @@ -1,5 +1,7 @@ """A habit collection.""" +from typing import TYPE_CHECKING + from jupiter.core.habits.root import Habit from jupiter.framework.base.entity_id import EntityId from jupiter.framework.context import DomainContext @@ -12,12 +14,15 @@ entity, ) +if TYPE_CHECKING: + from jupiter.core.workspaces.root import Workspace + @entity class HabitCollection(TrunkEntity): """A habit collection.""" - workspace: ParentLink + workspace: ParentLink["Workspace"] habits = ContainsMany(Habit, habit_collection_ref_id=IsRefId()) diff --git a/src/core/jupiter/core/habits/root.py b/src/core/jupiter/core/habits/root.py index a1baff910..48d2249c2 100644 --- a/src/core/jupiter/core/habits/root.py +++ b/src/core/jupiter/core/habits/root.py @@ -1,5 +1,7 @@ """A habit.""" +from typing import TYPE_CHECKING + from jupiter.core.common.recurring_task_gen_params import RecurringTaskGenParams from jupiter.core.common.recurring_task_period import RecurringTaskPeriod from jupiter.core.common.sub.inbox_tasks.root import InboxTask @@ -29,12 +31,15 @@ from jupiter.framework.record import ContainsManyRecords from jupiter.framework.update_action import UpdateAction +if TYPE_CHECKING: + from jupiter.core.habits.collection import HabitCollection + @entity class Habit(LeafEntity): """A habit.""" - habit_collection: ParentLink + habit_collection: ParentLink["HabitCollection"] aspect_ref_id: EntityId chapter_ref_id: EntityId | None goal_ref_id: EntityId | None diff --git a/src/core/jupiter/core/habits/streak_mark.py b/src/core/jupiter/core/habits/streak_mark.py index 793819637..a68185ec8 100644 --- a/src/core/jupiter/core/habits/streak_mark.py +++ b/src/core/jupiter/core/habits/streak_mark.py @@ -1,6 +1,7 @@ """The record of a streak of a habit.""" import abc +from typing import TYPE_CHECKING from jupiter.core.common.sub.inbox_tasks.status import InboxTaskStatus from jupiter.framework.base.adate import ADate @@ -15,12 +16,15 @@ ) from jupiter.framework.storage.repository import RecordRepository +if TYPE_CHECKING: + from jupiter.core.habits.root import Habit + @record class HabitStreakMark(Record): """The record of a streak of a habit.""" - habit: ParentLink + habit: ParentLink["Habit"] date: ADate statuses: dict[EntityId, InboxTaskStatus] diff --git a/src/core/jupiter/core/home/config.py b/src/core/jupiter/core/home/config.py index 81da8c5e2..7abf72ec4 100644 --- a/src/core/jupiter/core/home/config.py +++ b/src/core/jupiter/core/home/config.py @@ -1,5 +1,7 @@ """The home config domain application.""" +from typing import TYPE_CHECKING + from jupiter.core.home.sub.tab.root import HomeTab from jupiter.core.home.sub.tab.target import HomeTabTarget from jupiter.framework.base.entity_id import EntityId @@ -14,12 +16,15 @@ update_entity_action, ) +if TYPE_CHECKING: + from jupiter.core.workspaces.root import Workspace + @entity class HomeConfig(TrunkEntity): """The home config entity.""" - workspace: ParentLink + workspace: ParentLink["Workspace"] order_of_tabs: dict[HomeTabTarget, list[EntityId]] diff --git a/src/core/jupiter/core/home/sub/tab/root.py b/src/core/jupiter/core/home/sub/tab/root.py index e866901d6..32025a24a 100644 --- a/src/core/jupiter/core/home/sub/tab/root.py +++ b/src/core/jupiter/core/home/sub/tab/root.py @@ -1,5 +1,7 @@ """A tab on the home page.""" +from typing import TYPE_CHECKING + from jupiter.core.common.entity_icon import EntityIcon from jupiter.core.home.sub.tab.target import HomeTabTarget from jupiter.core.home.sub.tab.widget_placement import ( @@ -22,12 +24,15 @@ ) from jupiter.framework.update_action import UpdateAction +if TYPE_CHECKING: + from jupiter.core.home.config import HomeConfig + @entity class HomeTab(BranchEntity): """A tab on the home page.""" - home_config: ParentLink + home_config: ParentLink["HomeConfig"] target: HomeTabTarget name: EntityName icon: EntityIcon | None diff --git a/src/core/jupiter/core/home/sub/widget/root.py b/src/core/jupiter/core/home/sub/widget/root.py index 73a40d91c..da0e8ec44 100644 --- a/src/core/jupiter/core/home/sub/widget/root.py +++ b/src/core/jupiter/core/home/sub/widget/root.py @@ -1,5 +1,7 @@ """A widget on the home page.""" +from typing import TYPE_CHECKING + from jupiter.core.home.sub.tab.target import HomeTabTarget from jupiter.core.home.widget import ( WIDGET_CONSTRAINTS, @@ -18,12 +20,15 @@ update_entity_action, ) +if TYPE_CHECKING: + from jupiter.core.home.sub.tab.root import HomeTab + @entity class HomeWidget(LeafEntity): """A widget on the home page.""" - home_tab: ParentLink + home_tab: ParentLink["HomeTab"] the_type: WidgetType geometry: WidgetGeometry diff --git a/src/core/jupiter/core/infra/__init__.py b/src/core/jupiter/core/infra/__init__.py index 11ba43467..4f56f0f45 100644 --- a/src/core/jupiter/core/infra/__init__.py +++ b/src/core/jupiter/core/infra/__init__.py @@ -1 +1,3 @@ """Common infrastructure for the Jupiter core.""" + +SLICE_TAG = "Infra" diff --git a/src/core/jupiter/core/infra/component/layout/branch-panel.tsx b/src/core/jupiter/core/infra/component/layout/branch-panel.tsx index b44ba3f0f..fe42d1ede 100644 --- a/src/core/jupiter/core/infra/component/layout/branch-panel.tsx +++ b/src/core/jupiter/core/infra/component/layout/branch-panel.tsx @@ -5,6 +5,7 @@ import { Close as CloseIcon, Delete as DeleteIcon, DeleteForever as DeleteForeverIcon, + History as HistoryIcon, } from "@mui/icons-material"; import { Box, @@ -27,6 +28,7 @@ import { useRef, useState, } from "react"; +import type { EntityId, NamedEntityTag } from "@jupiter/webapi-client"; import { extractBranchFromPath } from "#/core/infra/routes"; import { @@ -36,6 +38,7 @@ import { import { useBigScreen } from "#/core/infra/component/use-big-screen"; import { useHydrated } from "#/core/infra/component/use-hidrated"; import { useTrunkNeedsToShowLeaf } from "#/core/infra/component/use-nested-entities"; +import { EntityMutationHistoryPanel } from "#/core/infra/component/layout/entity-mutation-history-panel"; const SMALL_SCREEN_ANIMATION_START = "100vw"; const SMALL_SCREEN_ANIMATION_END = "100vw"; @@ -43,6 +46,8 @@ const SMALL_SCREEN_ANIMATION_END = "100vw"; interface BranchPanelProps { createLocation?: string; showArchiveAndRemoveButton?: boolean; + entityType?: NamedEntityTag; + entityRefId?: EntityId; inputsEnabled?: boolean; entityArchived?: boolean; actions?: JSX.Element; @@ -57,6 +62,10 @@ export function BranchPanel(props: PropsWithChildren) { const isHydrated = useHydrated(); const shouldShowALeaf = useTrunkNeedsToShowLeaf(); const [showArchiveDialog, setShowArchiveDialog] = useState(false); + const [showHistory, setShowHistory] = useState(false); + + const hasHistory = + props.entityType !== undefined && props.entityRefId !== undefined; // This little function is a hack to get around the fact that Framer Motion // generates a translateX(Xpx) CSS applied to the StyledMotionDrawer element. @@ -189,11 +198,20 @@ export function BranchPanel(props: PropsWithChildren) { {props.actions} + {hasHistory && ( + setShowHistory((h) => !h)} + > + + + )} + {props.showArchiveAndRemoveButton && ( <> setShowArchiveDialog(true)} @@ -238,9 +256,10 @@ export function BranchPanel(props: PropsWithChildren) { @@ -252,14 +271,28 @@ export function BranchPanel(props: PropsWithChildren) { )} - - {props.children} - + {showHistory && hasHistory ? ( + + + + ) : ( + + {props.children} + + )} ); } diff --git a/src/core/jupiter/core/infra/component/layout/entity-event-list.tsx b/src/core/jupiter/core/infra/component/layout/entity-event-list.tsx new file mode 100644 index 000000000..50bc1a5ab --- /dev/null +++ b/src/core/jupiter/core/infra/component/layout/entity-event-list.tsx @@ -0,0 +1,152 @@ +import { ExpandMore as ExpandMoreIcon } from "@mui/icons-material"; +import { Box, Collapse, IconButton, Stack, Typography } from "@mui/material"; +import { Link } from "@remix-run/react"; +import { DateTime } from "luxon"; +import { useState } from "react"; + +export interface EntityEventEntryData { + mutation_id?: string; + entity_name: string; + mutation_name?: string; + event_kind: string; + event_name: string; + timestamp: string; + source: string; + user_ref_id: string; + entity_version: number; + data: string; +} + +export interface EntityEventUser { + ref_id: string; + name: string; +} + +function eventKindVerb(kind: string): string { + switch (kind) { + case "Created": + return "created"; + case "Updated": + return "updated"; + case "Archived": + return "archived"; + default: + return kind.toLowerCase(); + } +} + +function stripUseCaseSuffix(name: string): string { + return name.replace(/UseCase$/, ""); +} + +interface EntityEventRowProps { + entry: EntityEventEntryData; + user: EntityEventUser | undefined; +} + +export function EntityEventRow({ entry, user }: EntityEventRowProps) { + const [showData, setShowData] = useState(false); + const formattedTimestamp = DateTime.fromISO(entry.timestamp).toLocaleString( + DateTime.DATETIME_MED, + ); + const userName = user?.name ?? "Unknown"; + + return ( + + + {userName} {eventKindVerb(entry.event_kind)}{" "} + {entry.entity_name ?? entry.event_name} + {entry.mutation_name && ( + <> + {" "} + in mutation{" "} + {entry.mutation_id ? ( + + + {stripUseCaseSuffix(entry.mutation_name)} + + + ) : ( + {stripUseCaseSuffix(entry.mutation_name)} + )} + + )} + {"::"} + {entry.event_name} + + + + {formattedTimestamp} · v{entry.entity_version} ·{" "} + {entry.source} + + setShowData((s) => !s)}> + + + + + + {entry.data} + + + + ); +} + +interface EntityEventListProps { + entries: EntityEventEntryData[]; + usersById: Record; + emptyMessage?: string; +} + +export function EntityEventList({ + entries, + usersById, + emptyMessage = "No events found.", +}: EntityEventListProps) { + return ( + + {entries.length === 0 && ( + + {emptyMessage} + + )} + + {entries.map((entry, idx) => ( + + ))} + + ); +} diff --git a/src/core/jupiter/core/infra/component/layout/entity-mutation-history-panel.tsx b/src/core/jupiter/core/infra/component/layout/entity-mutation-history-panel.tsx new file mode 100644 index 000000000..4d6983daa --- /dev/null +++ b/src/core/jupiter/core/infra/component/layout/entity-mutation-history-panel.tsx @@ -0,0 +1,166 @@ +import { + Box, + CircularProgress, + Stack, + ToggleButton, + ToggleButtonGroup, +} from "@mui/material"; +import type { + HistoryEntry, + User, + EntityId, + NamedEntityTag, +} from "@jupiter/webapi-client"; +import { useFetcher } from "@remix-run/react"; +import { useEffect, useState } from "react"; + +import { EntityEventList } from "#/core/infra/component/layout/entity-event-list"; + +interface HistoryFetcherData { + entries: HistoryEntry[]; + users: User[]; + totalCnt: number; + pageSize: number; +} + +interface EntityMutationHistoryPanelProps { + entityType: NamedEntityTag; + entityRefId: EntityId; +} + +export function EntityMutationHistoryPanel( + props: EntityMutationHistoryPanelProps, +) { + const fetcher = useFetcher(); + const [currentPage, setCurrentPage] = useState(0); + + useEffect(() => { + const params = new URLSearchParams({ + entityType: props.entityType, + entityRefId: props.entityRefId, + }); + if (currentPage > 0) { + params.set( + "retrieveOffset", + (currentPage * (fetcher.data?.pageSize ?? 50)).toString(), + ); + } + fetcher.load( + `/app/workspace/infra/entity-mutation-history?${params.toString()}`, + ); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [props.entityType, props.entityRefId, currentPage]); + + if (fetcher.state === "loading" && !fetcher.data) { + return ( + + + + ); + } + + if (!fetcher.data) { + return null; + } + + const { entries, users, totalCnt, pageSize } = fetcher.data; + + const usersById = Object.fromEntries(users.map((u) => [u.ref_id, u])); + + return ( + + + + ({ + mutation_id: e.mutation_id, + entity_name: e.entity_name ?? e.event_name, + mutation_name: e.mutation_name, + event_kind: e.event_kind, + event_name: e.event_name, + timestamp: e.timestamp, + source: e.source, + user_ref_id: e.user_ref_id, + entity_version: e.entity_version, + data: e.data ?? "", + }))} + usersById={usersById} + emptyMessage="No history entries found." + /> + + {entries.length > 0 && ( + + )} + + + + ); +} + +interface HistoryPagesProps { + currentPage: number; + totalCnt: number; + pageSize: number; + onPageChange: (page: number) => void; +} + +function HistoryPages(props: HistoryPagesProps) { + const pageCount = Math.ceil(props.totalCnt / props.pageSize); + + if (pageCount <= 1) { + return null; + } + + const shouldShowPage = Array(pageCount).fill(false); + shouldShowPage[0] = true; + shouldShowPage[pageCount - 1] = true; + + for (let delta = -3; delta <= 3; delta++) { + const idx = props.currentPage + delta; + if (idx >= 0 && idx < pageCount) { + shouldShowPage[idx] = true; + } + } + + const buttons = []; + for (let i = 0; i < pageCount; i++) { + if (shouldShowPage[i]) { + buttons.push( + props.onPageChange(i)} + > + {i + 1} + , + ); + } else if (i > 0 && shouldShowPage[i - 1]) { + buttons.push( + + ... + , + ); + } + } + + return ( + + {buttons} + + ); +} diff --git a/src/core/jupiter/core/infra/component/layout/leaf-panel.tsx b/src/core/jupiter/core/infra/component/layout/leaf-panel.tsx index be1bdfbaa..dded75a30 100644 --- a/src/core/jupiter/core/infra/component/layout/leaf-panel.tsx +++ b/src/core/jupiter/core/infra/component/layout/leaf-panel.tsx @@ -4,6 +4,7 @@ import { ArrowUpward as ArrowUpwardIcon, Delete as DeleteIcon, DeleteForever as DeleteForeverIcon, + History as HistoryIcon, KeyboardDoubleArrowRight as KeyboardDoubleArrowRightIcon, PictureInPictureAlt as PictureInPictureAltIcon, SwitchLeft as SwitchLeftIcon, @@ -24,6 +25,7 @@ import { Form, useNavigate } from "@remix-run/react"; import { motion, useIsPresent } from "framer-motion"; import type { PropsWithChildren } from "react"; import { useCallback, useEffect, useRef, useState } from "react"; +import { EntityId, NamedEntityTag } from "@jupiter/webapi-client"; import { LeafPanelExpansionState, @@ -36,6 +38,7 @@ import { saveScrollPosition, } from "#/core/infra/scroll-restoration"; import { useBigScreen } from "#/core/infra/component/use-big-screen"; +import { EntityMutationHistoryPanel } from "#/core/infra/component/layout/entity-mutation-history-panel"; const BIG_SCREEN_ANIMATION_START = "480px"; const BIG_SCREEN_ANIMATION_END = "480px"; @@ -54,6 +57,8 @@ interface LeafPanelProps { isLeaflet?: boolean; showArchiveButton?: boolean; showArchiveAndRemoveButton?: boolean; + entityType?: NamedEntityTag; + entityRefId?: EntityId; fakeKey: string; inputsEnabled: boolean; entityNotEditable?: boolean; @@ -88,7 +93,10 @@ export function LeafPanel(props: PropsWithChildren) { BIG_SCREEN_WIDTH_FULL_INT, ); const [showArchiveDialog, setShowArchiveDialog] = useState(false); + const [showHistory, setShowHistory] = useState(false); + const hasHistory = + props.entityType !== undefined && props.entityRefId !== undefined; const showArchiveButNotRemove = props.showArchiveButton && !props.showArchiveAndRemoveButton; @@ -356,11 +364,20 @@ export function LeafPanel(props: PropsWithChildren) { + {hasHistory && ( + setShowHistory((h) => !h)} + > + + + )} + {(props.showArchiveButton || props.showArchiveAndRemoveButton) && ( <> ) { - {(isBigScreen || !props.shouldShowALeaflet) && ( + {showHistory && hasHistory ? ( - {props.children} - + - )} + ) : ( + <> + {(isBigScreen || !props.shouldShowALeaflet) && ( + + {props.children} + + + )} - {!isBigScreen && props.shouldShowALeaflet && <>{props.children}} + {!isBigScreen && props.shouldShowALeaflet && <>{props.children}} + + )} ); diff --git a/src/core/jupiter/core/infra/component/sidebar.tsx b/src/core/jupiter/core/infra/component/sidebar.tsx index 1ad67bd87..d693c99c0 100644 --- a/src/core/jupiter/core/infra/component/sidebar.tsx +++ b/src/core/jupiter/core/infra/component/sidebar.tsx @@ -424,6 +424,17 @@ export default function Sidebar(props: SidebarProps) { + + + 📜 + + + + diff --git a/src/core/jupiter/core/infra/use_case/__init__.py b/src/core/jupiter/core/infra/use_case/__init__.py new file mode 100644 index 000000000..3241d40ba --- /dev/null +++ b/src/core/jupiter/core/infra/use_case/__init__.py @@ -0,0 +1 @@ +"""Use cases for application infrastructure concerns.""" diff --git a/src/core/jupiter/core/infra/use_case/get_entity_mutation_history.py b/src/core/jupiter/core/infra/use_case/get_entity_mutation_history.py new file mode 100644 index 000000000..56ce53e3e --- /dev/null +++ b/src/core/jupiter/core/infra/use_case/get_entity_mutation_history.py @@ -0,0 +1,178 @@ +"""Retrieve the history of mutations for a particular entity.""" + +from typing import ClassVar + +from jupiter.core.config import ( + JupiterLoggedInReadonlyContext, + JupiterLoggedInReadonlyUseCase, +) +from jupiter.core.named_entity_tag import NamedEntityTag +from jupiter.core.named_entity_tag_to_cls import NAMED_ENTITY_TAG_TO_CLS +from jupiter.core.users.root import User +from jupiter.framework.base.entity_id import EntityId +from jupiter.framework.base.mutation_id import MutationId +from jupiter.framework.base.timestamp import Timestamp +from jupiter.framework.errors import InputValidationError +from jupiter.framework.mutation_inovcation.entity_event import MutationEntityEvent +from jupiter.framework.mutation_inovcation.invocation_record import ( + MutationInvocationRecord, +) +from jupiter.framework.use_case import readonly_use_case +from jupiter.framework.use_case_io import ( + UseCaseArgsBase, + UseCaseResultBase, + use_case_args, + use_case_result, + use_case_result_part, +) +from jupiter.framework.utils.generic_support_entity_explorer import ( + generic_support_entity_explorer, +) + + +@use_case_args +class GetEntityMutationHistoryArgs(UseCaseArgsBase): + """Arguments for the entity mutation history.""" + + entity_type: NamedEntityTag + entity_ref_id: EntityId + retrieve_offset: int | None + retrieve_limit: int | None + + +@use_case_result_part +class HistoryEntry(UseCaseResultBase): + """An instance of the history.""" + + # Which mutation + mutation_id: MutationId + # Which entity + entity_name: str + # What + mutation_name: str + event_kind: str + event_name: str + # When + timestamp: Timestamp + # Who + source: str + user_ref_id: EntityId + # Data + entity_version: int + data: str + + +@use_case_result +class GetEntityMutationHistoryResult(UseCaseResultBase): + """Results for the entity mutation history.""" + + entries: list[HistoryEntry] + users: list[User] + total_cnt: int + page_size: int + + +@readonly_use_case() +class GetEntityMutationHistoryUseCase( + JupiterLoggedInReadonlyUseCase[ + GetEntityMutationHistoryArgs, GetEntityMutationHistoryResult + ] +): + """Use case for loading the history of mutations for an entity.""" + + _DEFAULT_OFFSET: ClassVar[int] = 0 + _DEFAULT_LIMIT: ClassVar[int] = 4 + _MAX_LIMIT: ClassVar[int] = 100 + + async def _execute( + self, + context: JupiterLoggedInReadonlyContext, + args: GetEntityMutationHistoryArgs, + ) -> GetEntityMutationHistoryResult: + """Execute the command's action.""" + retrieve_offset = args.retrieve_offset or self._DEFAULT_OFFSET + retrieve_limit = args.retrieve_limit or self._DEFAULT_LIMIT + if retrieve_offset < 0: + raise InputValidationError( + f"Retrieve limit needs to be positive but was {retrieve_offset}" + ) + if retrieve_limit <= 0 or retrieve_limit > self._MAX_LIMIT: + raise InputValidationError( + f"Retrieve limit needs to be between 0 and {self._MAX_LIMIT} but was {retrieve_limit}" + ) + + main_events, total_cnt = ( + await self._invocation_recorder.find_all_entity_events_by_timestamp_desc( + args.entity_type.value, + args.entity_ref_id, + retrieve_offset, + retrieve_limit, + ) + ) + + linked_events: list[MutationEntityEvent] = [] + if main_events: + earliest = min(e.timestamp for e in main_events).subtract_minutes(10) + latest = max(e.timestamp for e in main_events).add_minutes(30) + + entity_cls = NAMED_ENTITY_TAG_TO_CLS.get(args.entity_type) + if entity_cls is not None: + async with self._ports.domain_storage_engine.get_unit_of_work() as uow: + entity = await uow.get_for(entity_cls).load_by_id( + args.entity_ref_id, + allow_archived=True, + ) + linked_refs = await generic_support_entity_explorer(uow, entity) + + for linked_type_name, linked_ref_id in linked_refs: + events = ( + await self._invocation_recorder.find_all_entity_events_between( + linked_type_name, + linked_ref_id, + earliest, + latest, + ) + ) + linked_events.extend(events) + + all_events = main_events + linked_events + all_events.sort(key=lambda e: e.timestamp, reverse=True) + + all_mutations: list[MutationInvocationRecord] = ( + await self._invocation_recorder.find_all_invocation_records( + list({m.mutation_id for m in all_events}) + ) + ) + all_mutations_by_ref_id = {m.mutation_id: m for m in all_mutations} + + async with self._ports.domain_storage_engine.get_unit_of_work() as uow: + all_users = await uow.get_for(User).find_all( + allow_archived=True, + filter_ref_ids=[ + JupiterLoggedInReadonlyContext.unwrap_str(e.context_str)[0] + for e in all_events + ], + ) + + return GetEntityMutationHistoryResult( + entries=[ + HistoryEntry( + mutation_id=e.mutation_id, + entity_name=e.entity_type, + mutation_name=all_mutations_by_ref_id[e.mutation_id].name, + event_kind=e.kind.value, + event_name=e.name, + timestamp=e.timestamp, + source=e.source, + user_ref_id=JupiterLoggedInReadonlyContext.unwrap_str( + e.context_str + )[0], + entity_version=e.entity_version, + data=e.data, + ) + for e in all_events + ], + users=all_users, + total_cnt=total_cnt, + page_size=self._DEFAULT_LIMIT, + ) diff --git a/src/core/jupiter/core/infra/use_case/get_mutation_entity_events.py b/src/core/jupiter/core/infra/use_case/get_mutation_entity_events.py new file mode 100644 index 000000000..02833839f --- /dev/null +++ b/src/core/jupiter/core/infra/use_case/get_mutation_entity_events.py @@ -0,0 +1,105 @@ +"""Retrieve all entity events generated by a particular mutation.""" + +from jupiter.core.config import ( + JupiterLoggedInReadonlyContext, + JupiterLoggedInReadonlyUseCase, +) +from jupiter.core.users.root import User +from jupiter.framework.base.entity_id import EntityId +from jupiter.framework.base.mutation_id import MutationId +from jupiter.framework.base.timestamp import Timestamp +from jupiter.framework.use_case import readonly_use_case +from jupiter.framework.use_case_io import ( + UseCaseArgsBase, + UseCaseResultBase, + use_case_args, + use_case_result, + use_case_result_part, +) + + +@use_case_args +class GetMutationEntityEventsArgs(UseCaseArgsBase): + """Arguments for getting entity events from a mutation.""" + + mutation_id: MutationId + + +@use_case_result_part +class EventEntry(UseCaseResultBase): + """A single entity event produced by a mutation.""" + + entity_name: str + event_kind: str + event_name: str + timestamp: Timestamp + source: str + user_ref_id: EntityId + entity_version: int + data: str + + +@use_case_result +class GetMutationEntityEventsResult(UseCaseResultBase): + """Results for the mutation entity events.""" + + mutation_name: str + entries: list[EventEntry] + users: list[User] + + +@readonly_use_case() +class GetMutationEntityEventsUseCase( + JupiterLoggedInReadonlyUseCase[ + GetMutationEntityEventsArgs, GetMutationEntityEventsResult + ] +): + """Use case for loading all entity events produced by a mutation.""" + + async def _execute( + self, + context: JupiterLoggedInReadonlyContext, + args: GetMutationEntityEventsArgs, + ) -> GetMutationEntityEventsResult: + """Execute the command's action.""" + events = await self._invocation_recorder.find_all_entity_events_for_mutation( + args.mutation_id, + ) + + invocation_records = ( + await self._invocation_recorder.find_all_invocation_records( + [args.mutation_id], + ) + ) + mutation_name = invocation_records[0].name if invocation_records else "Unknown" + + events.sort(key=lambda e: e.timestamp, reverse=True) + + async with self._ports.domain_storage_engine.get_unit_of_work() as uow: + all_users = await uow.get_for(User).find_all( + allow_archived=True, + filter_ref_ids=[ + JupiterLoggedInReadonlyContext.unwrap_str(e.context_str)[0] + for e in events + ], + ) + + return GetMutationEntityEventsResult( + mutation_name=mutation_name, + entries=[ + EventEntry( + entity_name=e.entity_type, + event_kind=e.kind.value, + event_name=e.name, + timestamp=e.timestamp, + source=e.source, + user_ref_id=JupiterLoggedInReadonlyContext.unwrap_str( + e.context_str + )[0], + entity_version=e.entity_version, + data=e.data, + ) + for e in events + ], + users=all_users, + ) diff --git a/src/core/jupiter/core/infra/use_case/get_mutation_invocation_history.py b/src/core/jupiter/core/infra/use_case/get_mutation_invocation_history.py new file mode 100644 index 000000000..c13e4058b --- /dev/null +++ b/src/core/jupiter/core/infra/use_case/get_mutation_invocation_history.py @@ -0,0 +1,124 @@ +"""Retrieve the history of mutation invocations for a user and workspace.""" + +import json +from typing import ClassVar + +from jupiter.core.config import ( + JupiterLoggedInReadonlyContext, + JupiterLoggedInReadonlyUseCase, +) +from jupiter.core.users.root import User +from jupiter.framework.base.entity_id import EntityId +from jupiter.framework.base.mutation_id import MutationId +from jupiter.framework.base.timestamp import Timestamp +from jupiter.framework.errors import InputValidationError +from jupiter.framework.use_case import readonly_use_case +from jupiter.framework.use_case_io import ( + UseCaseArgsBase, + UseCaseResultBase, + use_case_args, + use_case_result, + use_case_result_part, +) + + +@use_case_args +class GetMutationInvocationHistoryArgs(UseCaseArgsBase): + """Arguments for the mutation invocation history.""" + + retrieve_offset: int | None + retrieve_limit: int | None + + +@use_case_result_part +class InvocationHistoryEntry(UseCaseResultBase): + """A single mutation invocation history entry.""" + + mutation_id: MutationId + mutation_name: str + timestamp: Timestamp + source: str + user_ref_id: EntityId + result: str + args_str: str + error_str: str | None + + +@use_case_result +class GetMutationInvocationHistoryResult(UseCaseResultBase): + """Results for the mutation invocation history.""" + + entries: list[InvocationHistoryEntry] + users: list[User] + total_cnt: int + page_size: int + + +@readonly_use_case() +class GetMutationInvocationHistoryUseCase( + JupiterLoggedInReadonlyUseCase[ + GetMutationInvocationHistoryArgs, GetMutationInvocationHistoryResult + ] +): + """Use case for loading the history of mutation invocations for a user and workspace.""" + + _DEFAULT_OFFSET: ClassVar[int] = 0 + _DEFAULT_LIMIT: ClassVar[int] = 20 + _MAX_LIMIT: ClassVar[int] = 100 + + async def _execute( + self, + context: JupiterLoggedInReadonlyContext, + args: GetMutationInvocationHistoryArgs, + ) -> GetMutationInvocationHistoryResult: + """Execute the command's action.""" + retrieve_offset = args.retrieve_offset or self._DEFAULT_OFFSET + retrieve_limit = args.retrieve_limit or self._DEFAULT_LIMIT + if retrieve_offset < 0: + raise InputValidationError( + f"Retrieve offset needs to be positive but was {retrieve_offset}" + ) + if retrieve_limit <= 0 or retrieve_limit > self._MAX_LIMIT: + raise InputValidationError( + f"Retrieve limit needs to be between 0 and {self._MAX_LIMIT} but was {retrieve_limit}" + ) + + context_str = context.as_str() + + records, total_cnt = ( + await self._invocation_recorder.find_all_invocation_records_by_context_str( + context_str, + retrieve_offset, + retrieve_limit, + ) + ) + + async with self._ports.domain_storage_engine.get_unit_of_work() as uow: + all_users = await uow.get_for(User).find_all( + allow_archived=True, + filter_ref_ids=[ + JupiterLoggedInReadonlyContext.unwrap_str(r.context_str)[0] + for r in records + ], + ) + + return GetMutationInvocationHistoryResult( + entries=[ + InvocationHistoryEntry( + mutation_id=r.mutation_id, + mutation_name=r.name, + timestamp=r.timestamp, + source=r.source, + user_ref_id=JupiterLoggedInReadonlyContext.unwrap_str( + r.context_str + )[0], + result=str(r.result.value), + args_str=json.dumps(r.args, indent=2, default=str), + error_str=r.error_str, + ) + for r in records + ], + users=all_users, + total_cnt=total_cnt, + page_size=self._DEFAULT_LIMIT, + ) diff --git a/src/core/jupiter/core/journals/collection.py b/src/core/jupiter/core/journals/collection.py index 74488d988..ac3edeb07 100644 --- a/src/core/jupiter/core/journals/collection.py +++ b/src/core/jupiter/core/journals/collection.py @@ -1,5 +1,7 @@ """A journal attached to a workspace.""" +from typing import TYPE_CHECKING + from jupiter.core.common.difficulty import Difficulty from jupiter.core.common.eisen import Eisen from jupiter.core.common.recurring_task_gen_params import RecurringTaskGenParams @@ -24,12 +26,15 @@ from jupiter.framework.errors import InputValidationError from jupiter.framework.update_action import UpdateAction +if TYPE_CHECKING: + from jupiter.core.workspaces.root import Workspace + @entity class JournalCollection(TrunkEntity): """A journal.""" - workspace: ParentLink + workspace: ParentLink["Workspace"] periods: set[RecurringTaskPeriod] generation_approach: JournalGenerationApproach diff --git a/src/core/jupiter/core/journals/root.py b/src/core/jupiter/core/journals/root.py index 496ad345e..1e6a2cc45 100644 --- a/src/core/jupiter/core/journals/root.py +++ b/src/core/jupiter/core/journals/root.py @@ -1,6 +1,7 @@ """A journal for a particular time range.""" import abc +from typing import TYPE_CHECKING from jupiter.core.archival_reason import JupiterArchivalReason from jupiter.core.common.recurring_task_period import RecurringTaskPeriod @@ -34,6 +35,9 @@ ) from jupiter.framework.update_action import UpdateAction +if TYPE_CHECKING: + from jupiter.core.journals.collection import JournalCollection + class CannotModifyGeneratedJournalError(Exception): """Exception raised when you're trying to modify a generated journal.""" @@ -47,7 +51,7 @@ class JournalExistsForDatePeriodCombinationError(EntityAlreadyExistsError): class Journal(LeafEntity): """A journal for a particular range.""" - journal_collection: ParentLink + journal_collection: ParentLink["JournalCollection"] source: JournalSource right_now: ADate diff --git a/src/core/jupiter/core/journals/stats.py b/src/core/jupiter/core/journals/stats.py index fed9c26af..bb061ecd3 100644 --- a/src/core/jupiter/core/journals/stats.py +++ b/src/core/jupiter/core/journals/stats.py @@ -1,6 +1,7 @@ """Stats about a journal.""" import abc +from typing import TYPE_CHECKING from jupiter.core.common.recurring_task_period import RecurringTaskPeriod from jupiter.core.common.sub.inbox_tasks.source import InboxTaskSource @@ -14,12 +15,15 @@ from jupiter.framework.record import Record, create_record_action, record from jupiter.framework.storage.repository import RecordRepository +if TYPE_CHECKING: + from jupiter.core.journals.root import Journal + @record class JournalStats(Record): """Stats about a journal.""" - journal: ParentLink + journal: ParentLink["Journal"] report: ReportPeriodResult @staticmethod diff --git a/src/core/jupiter/core/life_plan/root.py b/src/core/jupiter/core/life_plan/root.py index cdc51e1a3..4164373ce 100644 --- a/src/core/jupiter/core/life_plan/root.py +++ b/src/core/jupiter/core/life_plan/root.py @@ -1,6 +1,6 @@ """A life plan.""" -from typing import ClassVar +from typing import TYPE_CHECKING, ClassVar from jupiter.core.common.birth_year import BirthYear from jupiter.core.common.birthday import Birthday @@ -34,6 +34,9 @@ TIME_PLAN_MAX_LIFE_PLAN_LINKS = 3 +if TYPE_CHECKING: + from jupiter.core.workspaces.root import Workspace + @entity class LifePlan(TrunkEntity): @@ -45,7 +48,7 @@ class LifePlan(TrunkEntity): RecurringTaskPeriod.YEARLY, } - workspace: ParentLink + workspace: ParentLink["Workspace"] birthday: Birthday birth_year: BirthYear diff --git a/src/core/jupiter/core/life_plan/sub/aspects/root.py b/src/core/jupiter/core/life_plan/sub/aspects/root.py index f579e8a5d..9abecbad8 100644 --- a/src/core/jupiter/core/life_plan/sub/aspects/root.py +++ b/src/core/jupiter/core/life_plan/sub/aspects/root.py @@ -1,6 +1,7 @@ """The aspect.""" import abc +from typing import TYPE_CHECKING from jupiter.core.big_plans.root import BigPlan from jupiter.core.chores.root import Chore @@ -30,12 +31,15 @@ MAX_ASPECT_DEPTH_FROM_ROOT = 5 +if TYPE_CHECKING: + from jupiter.core.life_plan.root import LifePlan + @entity class Aspect(LeafEntity): """The aspect.""" - life_plan: ParentLink + life_plan: ParentLink["LifePlan"] parent_aspect_ref_id: EntityId | None name: AspectName order_of_child_aspects: list[EntityId] diff --git a/src/core/jupiter/core/life_plan/sub/chapters/root.py b/src/core/jupiter/core/life_plan/sub/chapters/root.py index b20ee8868..ca2c47623 100644 --- a/src/core/jupiter/core/life_plan/sub/chapters/root.py +++ b/src/core/jupiter/core/life_plan/sub/chapters/root.py @@ -1,5 +1,7 @@ """A chapter in a life plan.""" +from typing import TYPE_CHECKING + from jupiter.core.common.sub.notes.namespace import NoteNamespace from jupiter.core.common.sub.notes.root import Note from jupiter.core.common.sub.tags.namespace import TagNamespace @@ -21,12 +23,15 @@ from jupiter.framework.errors import InputValidationError from jupiter.framework.update_action import UpdateAction +if TYPE_CHECKING: + from jupiter.core.life_plan.root import LifePlan + @entity class Chapter(LeafEntity): """A chapter in a life plan.""" - life_plan: ParentLink + life_plan: ParentLink["LifePlan"] name: ChapterName aspect_ref_id: EntityId start_date: PartialDate diff --git a/src/core/jupiter/core/life_plan/sub/goals/root.py b/src/core/jupiter/core/life_plan/sub/goals/root.py index b93463a39..a217c2768 100644 --- a/src/core/jupiter/core/life_plan/sub/goals/root.py +++ b/src/core/jupiter/core/life_plan/sub/goals/root.py @@ -1,5 +1,7 @@ """A goal in a life plan.""" +from typing import TYPE_CHECKING + from jupiter.core.common.sub.notes.namespace import NoteNamespace from jupiter.core.common.sub.notes.root import Note from jupiter.core.common.sub.tags.namespace import TagNamespace @@ -20,12 +22,15 @@ MAX_GOAL_DEPTH_FROM_ROOT = 5 +if TYPE_CHECKING: + from jupiter.core.life_plan.root import LifePlan + @entity class Goal(LeafEntity): """A goal in a life plan.""" - life_plan: ParentLink + life_plan: ParentLink["LifePlan"] name: GoalName aspect_ref_id: EntityId parent_goal_ref_id: EntityId | None diff --git a/src/core/jupiter/core/life_plan/sub/milestones/root.py b/src/core/jupiter/core/life_plan/sub/milestones/root.py index 5d39f0a60..b7c798cc9 100644 --- a/src/core/jupiter/core/life_plan/sub/milestones/root.py +++ b/src/core/jupiter/core/life_plan/sub/milestones/root.py @@ -1,5 +1,7 @@ """A milestone in a life plan.""" +from typing import TYPE_CHECKING + from jupiter.core.common.sub.notes.namespace import NoteNamespace from jupiter.core.common.sub.notes.root import Note from jupiter.core.common.sub.tags.namespace import TagNamespace @@ -19,12 +21,15 @@ ) from jupiter.framework.update_action import UpdateAction +if TYPE_CHECKING: + from jupiter.core.life_plan.root import LifePlan + @entity class Milestone(LeafEntity): """A milestone in a life plan.""" - life_plan: ParentLink + life_plan: ParentLink["LifePlan"] name: MilestoneName aspect_ref_id: EntityId date: ADate diff --git a/src/core/jupiter/core/life_plan/sub/visions/root.py b/src/core/jupiter/core/life_plan/sub/visions/root.py index 621ed7630..22041da06 100644 --- a/src/core/jupiter/core/life_plan/sub/visions/root.py +++ b/src/core/jupiter/core/life_plan/sub/visions/root.py @@ -1,5 +1,7 @@ """A vision in a life plan.""" +from typing import TYPE_CHECKING + from jupiter.core.common.sub.notes.namespace import NoteNamespace from jupiter.core.common.sub.notes.root import Note from jupiter.core.life_plan.sub.visions.status import VisionStatus @@ -16,12 +18,15 @@ update_entity_action, ) +if TYPE_CHECKING: + from jupiter.core.life_plan.root import LifePlan + @entity class Vision(LeafEntity): """A vision in a life plan.""" - life_plan: ParentLink + life_plan: ParentLink["LifePlan"] status: VisionStatus note = OwnsOne( diff --git a/src/core/jupiter/core/mcp_key/root.py b/src/core/jupiter/core/mcp_key/root.py index 6284a7909..c97f51987 100644 --- a/src/core/jupiter/core/mcp_key/root.py +++ b/src/core/jupiter/core/mcp_key/root.py @@ -1,5 +1,7 @@ """An MCP key.""" +from typing import TYPE_CHECKING + from jupiter.core.api_key.key_secret_hash import KeySecretHash from jupiter.core.api_key.key_secret_plain import KeySecretPlain from jupiter.core.mcp_key.mcp_key_summary import MCPKeySummary @@ -17,6 +19,9 @@ from jupiter.framework.secure import secure_class from jupiter.framework.update_action import UpdateAction +if TYPE_CHECKING: + from jupiter.core.users.root import User + class InvalidMCPKeyError(Exception): """Error raised when the MCP key is invalid.""" @@ -28,7 +33,7 @@ class InvalidMCPKeyError(Exception): class MCPKey(LeafEntity): """An MCP key.""" - user: ParentLink + user: ParentLink["User"] name: MCPKeyName key_hash: KeySecretHash key_size: int diff --git a/src/core/jupiter/core/metrics/collection.py b/src/core/jupiter/core/metrics/collection.py index 2acc1a838..ef9329ec4 100644 --- a/src/core/jupiter/core/metrics/collection.py +++ b/src/core/jupiter/core/metrics/collection.py @@ -1,5 +1,7 @@ """A metric collection.""" +from typing import TYPE_CHECKING + from jupiter.core.metrics.root import Metric from jupiter.framework.base.entity_id import EntityId from jupiter.framework.context import DomainContext @@ -12,12 +14,15 @@ entity, ) +if TYPE_CHECKING: + from jupiter.core.workspaces.root import Workspace + @entity class MetricCollection(TrunkEntity): """A metric collection.""" - workspace: ParentLink + workspace: ParentLink["Workspace"] metrics = ContainsMany(Metric, metric_collection_ref_id=IsRefId()) diff --git a/src/core/jupiter/core/metrics/root.py b/src/core/jupiter/core/metrics/root.py index 8740d8d66..d66f77ccf 100644 --- a/src/core/jupiter/core/metrics/root.py +++ b/src/core/jupiter/core/metrics/root.py @@ -1,5 +1,7 @@ """A metric.""" +from typing import TYPE_CHECKING + from jupiter.core.common.entity_icon import EntityIcon from jupiter.core.common.recurring_task_gen_params import RecurringTaskGenParams from jupiter.core.common.sub.inbox_tasks.root import InboxTask @@ -27,12 +29,15 @@ ) from jupiter.framework.update_action import UpdateAction +if TYPE_CHECKING: + from jupiter.core.metrics.collection import MetricCollection + @entity class Metric(BranchEntity): """A metric.""" - metric_collection: ParentLink + metric_collection: ParentLink["MetricCollection"] name: MetricName is_key: bool icon: EntityIcon | None diff --git a/src/core/jupiter/core/metrics/sub/entry/root.py b/src/core/jupiter/core/metrics/sub/entry/root.py index 49e89f8e8..39abb9906 100644 --- a/src/core/jupiter/core/metrics/sub/entry/root.py +++ b/src/core/jupiter/core/metrics/sub/entry/root.py @@ -1,5 +1,7 @@ """A metric entry.""" +from typing import TYPE_CHECKING + from jupiter.core.common.sub.notes.namespace import NoteNamespace from jupiter.core.common.sub.notes.root import Note from jupiter.core.common.sub.tags.namespace import TagNamespace @@ -19,12 +21,15 @@ ) from jupiter.framework.update_action import UpdateAction +if TYPE_CHECKING: + from jupiter.core.metrics.root import Metric + @entity class MetricEntry(LeafEntity): """A metric entry.""" - metric: ParentLink + metric: ParentLink["Metric"] collection_time: ADate value: float diff --git a/src/core/jupiter/core/named_entity_tag_to_cls.py b/src/core/jupiter/core/named_entity_tag_to_cls.py new file mode 100644 index 000000000..a35a9ee6d --- /dev/null +++ b/src/core/jupiter/core/named_entity_tag_to_cls.py @@ -0,0 +1,71 @@ +"""Mapping from NamedEntityTag to the corresponding entity class.""" + +from jupiter.core.big_plans.root import BigPlan +from jupiter.core.big_plans.sub.milestones.root import BigPlanMilestone +from jupiter.core.chores.root import Chore +from jupiter.core.docs.root import Doc +from jupiter.core.gamification.score_log_entry import ScoreLogEntry +from jupiter.core.habits.root import Habit +from jupiter.core.home.sub.tab.root import HomeTab +from jupiter.core.home.sub.widget.root import HomeWidget +from jupiter.core.journals.root import Journal +from jupiter.core.life_plan.sub.aspects.root import Aspect +from jupiter.core.life_plan.sub.chapters.root import Chapter +from jupiter.core.life_plan.sub.goals.root import Goal +from jupiter.core.life_plan.sub.milestones.root import Milestone +from jupiter.core.life_plan.sub.visions.root import Vision +from jupiter.core.metrics.root import Metric +from jupiter.core.metrics.sub.entry.root import MetricEntry +from jupiter.core.named_entity_tag import NamedEntityTag +from jupiter.core.prm.sub.circle.root import Circle +from jupiter.core.prm.sub.person.root import Person +from jupiter.core.prm.sub.person.sub.occasion.root import Occasion +from jupiter.core.push_integrations.sub.email.task import EmailTask +from jupiter.core.push_integrations.sub.slack.task import SlackTask +from jupiter.core.schedule.sub.event_full_days.root import ScheduleEventFullDays +from jupiter.core.schedule.sub.event_in_day.root import ScheduleEventInDay +from jupiter.core.schedule.sub.export.root import ScheduleExport +from jupiter.core.schedule.sub.external_sync_log.root import ScheduleExternalSyncLog +from jupiter.core.schedule.sub.stream.root import ScheduleStream +from jupiter.core.smart_lists.root import SmartList +from jupiter.core.smart_lists.sub.item.root import SmartListItem +from jupiter.core.time_plans.root import TimePlan +from jupiter.core.time_plans.sub.activity.root import TimePlanActivity +from jupiter.core.todo.root import TodoTask +from jupiter.core.vacations.root import Vacation +from jupiter.framework.entity import CrownEntity + +NAMED_ENTITY_TAG_TO_CLS: dict[NamedEntityTag, type[CrownEntity]] = { + NamedEntityTag.SCORE_LOG_ENTRY: ScoreLogEntry, + NamedEntityTag.HOME_TAB: HomeTab, + NamedEntityTag.HOME_WIDGET: HomeWidget, + NamedEntityTag.TODO_TASK: TodoTask, + NamedEntityTag.TIME_PLAN: TimePlan, + NamedEntityTag.TIME_PLAN_ACTIVITY: TimePlanActivity, + NamedEntityTag.SCHEDULE_STREAM: ScheduleStream, + NamedEntityTag.SCHEDULE_EXPORT: ScheduleExport, + NamedEntityTag.SCHEDULE_EVENT_IN_DAY: ScheduleEventInDay, + NamedEntityTag.SCHEDULE_EVENT_FULL_DAYS_BLOCK: ScheduleEventFullDays, + NamedEntityTag.SCHEDULE_EXTERNAL_SYNC_LOG: ScheduleExternalSyncLog, + NamedEntityTag.HABIT: Habit, + NamedEntityTag.CHORE: Chore, + NamedEntityTag.BIG_PLAN: BigPlan, + NamedEntityTag.BIG_PLAN_MILESTONE: BigPlanMilestone, + NamedEntityTag.DOC: Doc, + NamedEntityTag.JOURNAL: Journal, + NamedEntityTag.CHAPTER: Chapter, + NamedEntityTag.GOAL: Goal, + NamedEntityTag.MILESTONE: Milestone, + NamedEntityTag.VISION: Vision, + NamedEntityTag.VACATION: Vacation, + NamedEntityTag.ASPECT: Aspect, + NamedEntityTag.SMART_LIST: SmartList, + NamedEntityTag.SMART_LIST_ITEM: SmartListItem, + NamedEntityTag.METRIC: Metric, + NamedEntityTag.METRIC_ENTRY: MetricEntry, + NamedEntityTag.PERSON: Person, + NamedEntityTag.OCCASION: Occasion, + NamedEntityTag.CIRCLE: Circle, + NamedEntityTag.SLACK_TASK: SlackTask, + NamedEntityTag.EMAIL_TASK: EmailTask, +} diff --git a/src/core/jupiter/core/prm/root.py b/src/core/jupiter/core/prm/root.py index 691bae985..2b32a45d1 100644 --- a/src/core/jupiter/core/prm/root.py +++ b/src/core/jupiter/core/prm/root.py @@ -1,5 +1,7 @@ """The person collection.""" +from typing import TYPE_CHECKING + from jupiter.core.prm.sub.circle.root import Circle from jupiter.core.prm.sub.person.root import Person from jupiter.framework.base.entity_id import EntityId @@ -15,12 +17,15 @@ MAX_CIRCLES_PER_PERSON = 3 +if TYPE_CHECKING: + from jupiter.core.workspaces.root import Workspace + @entity class PRM(TrunkEntity): """The personal relationship database.""" - workspace: ParentLink + workspace: ParentLink["Workspace"] max_circles_per_person: int persons = ContainsMany(Person, prm_ref_id=IsRefId()) diff --git a/src/core/jupiter/core/prm/sub/circle/root.py b/src/core/jupiter/core/prm/sub/circle/root.py index 9df823172..83d3efb53 100644 --- a/src/core/jupiter/core/prm/sub/circle/root.py +++ b/src/core/jupiter/core/prm/sub/circle/root.py @@ -1,5 +1,7 @@ """A circle of people.""" +from typing import TYPE_CHECKING + from jupiter.core.prm.sub.circle.name import CircleName from jupiter.framework.base.entity_id import EntityId from jupiter.framework.context import DomainContext @@ -12,12 +14,15 @@ ) from jupiter.framework.update_action import UpdateAction +if TYPE_CHECKING: + from jupiter.core.prm.root import PRM + @entity class Circle(LeafEntity): """A circle of people, user-defined.""" - prm: ParentLink + prm: ParentLink["PRM"] name: CircleName @staticmethod diff --git a/src/core/jupiter/core/prm/sub/person/root.py b/src/core/jupiter/core/prm/sub/person/root.py index 1fca3e517..0e1d152f3 100644 --- a/src/core/jupiter/core/prm/sub/person/root.py +++ b/src/core/jupiter/core/prm/sub/person/root.py @@ -1,5 +1,7 @@ """A person.""" +from typing import TYPE_CHECKING + from jupiter.core.common.recurring_task_gen_params import RecurringTaskGenParams from jupiter.core.common.sub.contacts.namespace import ContactNamespace from jupiter.core.common.sub.contacts.sub.link.root import ContactLink @@ -29,12 +31,15 @@ from jupiter.framework.record import ContainsManyRecords from jupiter.framework.update_action import UpdateAction +if TYPE_CHECKING: + from jupiter.core.prm.root import PRM + @entity class Person(LeafEntity): """A person.""" - prm: ParentLink + prm: ParentLink["PRM"] catch_up_params: RecurringTaskGenParams | None occasions = ContainsMany(Occasion, person_ref_id=IsRefId()) diff --git a/src/core/jupiter/core/prm/sub/person/sub/occasion/root.py b/src/core/jupiter/core/prm/sub/person/sub/occasion/root.py index da210c437..00b35b1ff 100644 --- a/src/core/jupiter/core/prm/sub/person/sub/occasion/root.py +++ b/src/core/jupiter/core/prm/sub/person/sub/occasion/root.py @@ -1,6 +1,7 @@ """An occasion.""" import abc +from typing import TYPE_CHECKING from jupiter.core.common.birthday import Birthday from jupiter.core.common.sub.inbox_tasks.root import InboxTask @@ -31,12 +32,15 @@ from jupiter.framework.storage.repository import LeafEntityRepository from jupiter.framework.update_action import UpdateAction +if TYPE_CHECKING: + from jupiter.core.prm.sub.person.root import Person + @entity class Occasion(LeafEntity): """An occasion.""" - person: ParentLink + person: ParentLink["Person"] kind: OccasionKind name: OccasionName date: Birthday diff --git a/src/core/jupiter/core/prm/sub/person_circle_links/root.py b/src/core/jupiter/core/prm/sub/person_circle_links/root.py index 9a7e9ed8f..dc0b19dde 100644 --- a/src/core/jupiter/core/prm/sub/person_circle_links/root.py +++ b/src/core/jupiter/core/prm/sub/person_circle_links/root.py @@ -1,6 +1,7 @@ """Links between persons and circles.""" import abc +from typing import TYPE_CHECKING from jupiter.framework.base.entity_id import EntityId from jupiter.framework.context import DomainContext @@ -8,12 +9,15 @@ from jupiter.framework.record import Record, create_record_action, record from jupiter.framework.storage.repository import RecordRepository +if TYPE_CHECKING: + from jupiter.core.prm.root import PRM + @record class PersonCircleLink(Record): """A link between a person and a circle.""" - prm: ParentLink + prm: ParentLink["PRM"] person_ref_id: EntityId circle_ref_id: EntityId diff --git a/src/core/jupiter/core/push_integrations/group.py b/src/core/jupiter/core/push_integrations/group.py index d95b5111b..9058802e3 100644 --- a/src/core/jupiter/core/push_integrations/group.py +++ b/src/core/jupiter/core/push_integrations/group.py @@ -1,5 +1,7 @@ """A container for all the group of various push integrations we have.""" +from typing import TYPE_CHECKING + from jupiter.core.push_integrations.sub.email.task_collection import ( EmailTaskCollection, ) @@ -17,12 +19,15 @@ entity, ) +if TYPE_CHECKING: + from jupiter.core.workspaces.root import Workspace + @entity class PushIntegrationGroup(TrunkEntity): """A container for all the group of various push integrations we have.""" - workspace: ParentLink + workspace: ParentLink["Workspace"] slack_task_collection = ContainsOne( SlackTaskCollection, push_integration_group_ref_id=IsRefId() diff --git a/src/core/jupiter/core/push_integrations/sub/email/task.py b/src/core/jupiter/core/push_integrations/sub/email/task.py index ed1db8e2c..b0f69d33e 100644 --- a/src/core/jupiter/core/push_integrations/sub/email/task.py +++ b/src/core/jupiter/core/push_integrations/sub/email/task.py @@ -1,5 +1,7 @@ """An email task which needs to be converted into an inbox task.""" +from typing import TYPE_CHECKING + from jupiter.core.common.email_address import EmailAddress from jupiter.core.common.sub.inbox_tasks.root import InboxTask from jupiter.core.common.sub.inbox_tasks.source import InboxTaskSource @@ -24,12 +26,17 @@ from jupiter.framework.errors import InputValidationError from jupiter.framework.update_action import UpdateAction +if TYPE_CHECKING: + from jupiter.core.push_integrations.sub.email.task_collection import ( + EmailTaskCollection, + ) + @entity class EmailTask(LeafEntity): """An email task which needs to be converted into an inbox task.""" - email_task_collection: ParentLink + email_task_collection: ParentLink["EmailTaskCollection"] from_address: EmailAddress from_name: EmailUserName to_address: EmailAddress diff --git a/src/core/jupiter/core/push_integrations/sub/email/task_collection.py b/src/core/jupiter/core/push_integrations/sub/email/task_collection.py index e30f15a48..b5a835cd3 100644 --- a/src/core/jupiter/core/push_integrations/sub/email/task_collection.py +++ b/src/core/jupiter/core/push_integrations/sub/email/task_collection.py @@ -1,5 +1,7 @@ """A collection of email tasks.""" +from typing import TYPE_CHECKING + from jupiter.core.push_integrations.sub.email.task import EmailTask from jupiter.framework.base.entity_id import EntityId from jupiter.framework.context import DomainContext @@ -12,12 +14,15 @@ entity, ) +if TYPE_CHECKING: + from jupiter.core.push_integrations.group import PushIntegrationGroup + @entity class EmailTaskCollection(TrunkEntity): """A collection of email tasks.""" - push_integration_group: ParentLink + push_integration_group: ParentLink["PushIntegrationGroup"] email_tasks = ContainsMany(EmailTask, email_task_collection_ref_id=IsRefId()) diff --git a/src/core/jupiter/core/push_integrations/sub/slack/task.py b/src/core/jupiter/core/push_integrations/sub/slack/task.py index 59227363e..b41b3e2ef 100644 --- a/src/core/jupiter/core/push_integrations/sub/slack/task.py +++ b/src/core/jupiter/core/push_integrations/sub/slack/task.py @@ -1,5 +1,7 @@ """A Slack task which needs to be converted into an inbox task.""" +from typing import TYPE_CHECKING + from jupiter.core.common.sub.inbox_tasks.root import InboxTask from jupiter.core.common.sub.inbox_tasks.source import InboxTaskSource from jupiter.core.push_integrations.extra_info import ( @@ -26,12 +28,17 @@ from jupiter.framework.errors import InputValidationError from jupiter.framework.update_action import UpdateAction +if TYPE_CHECKING: + from jupiter.core.push_integrations.sub.slack.task_collection import ( + SlackTaskCollection, + ) + @entity class SlackTask(LeafEntity): """A Slack task which needs to be converted into an inbox task.""" - slack_task_collection: ParentLink + slack_task_collection: ParentLink["SlackTaskCollection"] user: SlackUserName message: str generation_extra_info: PushGenerationExtraInfo diff --git a/src/core/jupiter/core/push_integrations/sub/slack/task_collection.py b/src/core/jupiter/core/push_integrations/sub/slack/task_collection.py index 13d2d325f..dedaea3bd 100644 --- a/src/core/jupiter/core/push_integrations/sub/slack/task_collection.py +++ b/src/core/jupiter/core/push_integrations/sub/slack/task_collection.py @@ -1,5 +1,7 @@ """A collection of slack tasks.""" +from typing import TYPE_CHECKING + from jupiter.core.push_integrations.sub.slack.task import SlackTask from jupiter.framework.base.entity_id import EntityId from jupiter.framework.context import DomainContext @@ -12,12 +14,15 @@ entity, ) +if TYPE_CHECKING: + from jupiter.core.push_integrations.group import PushIntegrationGroup + @entity class SlackTaskCollection(TrunkEntity): """A collection of slack tasks.""" - push_integration_group: ParentLink + push_integration_group: ParentLink["PushIntegrationGroup"] slack_tasks = ContainsMany(SlackTask, slack_task_collection_ref_id=IsRefId()) diff --git a/src/core/jupiter/core/schedule/domain.py b/src/core/jupiter/core/schedule/domain.py index 8e75eb2c7..e190d318b 100644 --- a/src/core/jupiter/core/schedule/domain.py +++ b/src/core/jupiter/core/schedule/domain.py @@ -1,5 +1,7 @@ """The schedule domain.""" +from typing import TYPE_CHECKING + from jupiter.core.schedule.sub.event_full_days.root import ( ScheduleEventFullDays, ) @@ -23,12 +25,15 @@ entity, ) +if TYPE_CHECKING: + from jupiter.core.workspaces.root import Workspace + @entity class ScheduleDomain(TrunkEntity): """The schedule domain.""" - workspace: ParentLink + workspace: ParentLink["Workspace"] external_sync_log = ContainsOne( ScheduleExternalSyncLog, schedule_domain_ref_id=IsRefId() diff --git a/src/core/jupiter/core/schedule/sub/event_full_days/root.py b/src/core/jupiter/core/schedule/sub/event_full_days/root.py index a1ecda548..962338e31 100644 --- a/src/core/jupiter/core/schedule/sub/event_full_days/root.py +++ b/src/core/jupiter/core/schedule/sub/event_full_days/root.py @@ -1,5 +1,7 @@ """A full day block in a schedule.""" +from typing import TYPE_CHECKING + from jupiter.core.common.sub.notes.namespace import NoteNamespace from jupiter.core.common.sub.notes.root import Note from jupiter.core.common.sub.tags.namespace import TagNamespace @@ -29,12 +31,15 @@ ) from jupiter.framework.update_action import UpdateAction +if TYPE_CHECKING: + from jupiter.core.schedule.domain import ScheduleDomain + @entity class ScheduleEventFullDays(LeafEntity): """A full day block in a schedule.""" - schedule_domain: ParentLink + schedule_domain: ParentLink["ScheduleDomain"] schedule_stream_ref_id: EntityId source: ScheduleStreamSource diff --git a/src/core/jupiter/core/schedule/sub/event_in_day/root.py b/src/core/jupiter/core/schedule/sub/event_in_day/root.py index 7951237b8..b69c9bca6 100644 --- a/src/core/jupiter/core/schedule/sub/event_in_day/root.py +++ b/src/core/jupiter/core/schedule/sub/event_in_day/root.py @@ -1,5 +1,7 @@ """An event in a schedule.""" +from typing import TYPE_CHECKING + from jupiter.core.common.sub.notes.namespace import NoteNamespace from jupiter.core.common.sub.notes.root import Note from jupiter.core.common.sub.tags.namespace import TagNamespace @@ -29,12 +31,15 @@ ) from jupiter.framework.update_action import UpdateAction +if TYPE_CHECKING: + from jupiter.core.schedule.domain import ScheduleDomain + @entity class ScheduleEventInDay(LeafEntity): """An event in a schedule.""" - schedule_domain: ParentLink + schedule_domain: ParentLink["ScheduleDomain"] schedule_stream_ref_id: EntityId source: ScheduleStreamSource diff --git a/src/core/jupiter/core/schedule/sub/export/root.py b/src/core/jupiter/core/schedule/sub/export/root.py index 92f9b3eb8..188608f10 100644 --- a/src/core/jupiter/core/schedule/sub/export/root.py +++ b/src/core/jupiter/core/schedule/sub/export/root.py @@ -2,6 +2,7 @@ import abc import uuid +from typing import TYPE_CHECKING from jupiter.core.common.sub.notes.namespace import NoteNamespace from jupiter.core.common.sub.notes.root import Note @@ -22,12 +23,15 @@ from jupiter.framework.storage.repository import LeafEntityRepository from jupiter.framework.update_action import UpdateAction +if TYPE_CHECKING: + from jupiter.core.schedule.domain import ScheduleDomain + @entity class ScheduleExport(LeafEntity): """A calendar export configuration that bundles multiple schedule streams.""" - schedule_domain: ParentLink + schedule_domain: ParentLink["ScheduleDomain"] external_id: str name: ScheduleExportName diff --git a/src/core/jupiter/core/schedule/sub/external_sync_log/entry.py b/src/core/jupiter/core/schedule/sub/external_sync_log/entry.py index 4ce5c26d9..315452327 100644 --- a/src/core/jupiter/core/schedule/sub/external_sync_log/entry.py +++ b/src/core/jupiter/core/schedule/sub/external_sync_log/entry.py @@ -1,6 +1,7 @@ """An entry in a sync log.""" import abc +from typing import TYPE_CHECKING from jupiter.core.common.entity_summary import EntitySummary from jupiter.framework.base.adate import ADate @@ -20,6 +21,9 @@ from jupiter.framework.storage.repository import LeafEntityRepository from jupiter.framework.value import CompositeValue, value +if TYPE_CHECKING: + from jupiter.core.schedule.sub.external_sync_log.root import ScheduleExternalSyncLog + @value class ScheduleExternalSyncLogPerStreamResult(CompositeValue): @@ -34,7 +38,7 @@ class ScheduleExternalSyncLogPerStreamResult(CompositeValue): class ScheduleExternalSyncLogEntry(LeafEntity): """An entry in a sync log.""" - schedule_external_sync_log: ParentLink + schedule_external_sync_log: ParentLink["ScheduleExternalSyncLog"] source: str today: ADate start_of_window: ADate diff --git a/src/core/jupiter/core/schedule/sub/external_sync_log/root.py b/src/core/jupiter/core/schedule/sub/external_sync_log/root.py index a1f090eda..06170d2be 100644 --- a/src/core/jupiter/core/schedule/sub/external_sync_log/root.py +++ b/src/core/jupiter/core/schedule/sub/external_sync_log/root.py @@ -1,5 +1,7 @@ """A sync log attached to a schedule domain.""" +from typing import TYPE_CHECKING + from jupiter.core.schedule.sub.external_sync_log.entry import ( ScheduleExternalSyncLogEntry, ) @@ -15,12 +17,15 @@ entity, ) +if TYPE_CHECKING: + from jupiter.core.schedule.domain import ScheduleDomain + @entity class ScheduleExternalSyncLog(BranchEntity): """A sync log attached to a schedule domain.""" - schedule_domain: ParentLink + schedule_domain: ParentLink["ScheduleDomain"] entries = ContainsMany( ScheduleExternalSyncLogEntry, schedule_external_sync_log_ref_id=IsRefId() diff --git a/src/core/jupiter/core/schedule/sub/stream/root.py b/src/core/jupiter/core/schedule/sub/stream/root.py index 49850a353..e27949551 100644 --- a/src/core/jupiter/core/schedule/sub/stream/root.py +++ b/src/core/jupiter/core/schedule/sub/stream/root.py @@ -1,5 +1,7 @@ """A specific schedule group or stream of events.""" +from typing import TYPE_CHECKING + from jupiter.core.common.sub.notes.namespace import NoteNamespace from jupiter.core.common.sub.notes.root import Note from jupiter.core.common.sub.tags.namespace import TagNamespace @@ -32,6 +34,9 @@ ) from jupiter.framework.update_action import UpdateAction +if TYPE_CHECKING: + from jupiter.core.schedule.domain import ScheduleDomain + class CannotModifyScheduleStreamError(Exception): """Cannot modify the schedule stream.""" @@ -41,7 +46,7 @@ class CannotModifyScheduleStreamError(Exception): class ScheduleStream(LeafEntity): """A schedule group or stream of events.""" - schedule_domain: ParentLink + schedule_domain: ParentLink["ScheduleDomain"] source: ScheduleStreamSource name: ScheduleStreamName diff --git a/src/core/jupiter/core/smart_lists/collection.py b/src/core/jupiter/core/smart_lists/collection.py index 5a47606c2..cc1771f45 100644 --- a/src/core/jupiter/core/smart_lists/collection.py +++ b/src/core/jupiter/core/smart_lists/collection.py @@ -1,5 +1,7 @@ """A smart list collection.""" +from typing import TYPE_CHECKING + from jupiter.core.smart_lists.root import SmartList from jupiter.framework.base.entity_id import EntityId from jupiter.framework.context import DomainContext @@ -12,12 +14,15 @@ entity, ) +if TYPE_CHECKING: + from jupiter.core.workspaces.root import Workspace + @entity class SmartListCollection(TrunkEntity): """A smart list collection.""" - workspace: ParentLink + workspace: ParentLink["Workspace"] smart_lists = ContainsMany(SmartList, smart_list_collection_ref_id=IsRefId()) diff --git a/src/core/jupiter/core/smart_lists/root.py b/src/core/jupiter/core/smart_lists/root.py index 8bc5baefa..2a15c8999 100644 --- a/src/core/jupiter/core/smart_lists/root.py +++ b/src/core/jupiter/core/smart_lists/root.py @@ -1,5 +1,7 @@ """A smart list.""" +from typing import TYPE_CHECKING + from jupiter.core.common.entity_icon import EntityIcon from jupiter.core.common.sub.notes.namespace import NoteNamespace from jupiter.core.common.sub.notes.root import Note @@ -21,12 +23,15 @@ ) from jupiter.framework.update_action import UpdateAction +if TYPE_CHECKING: + from jupiter.core.smart_lists.collection import SmartListCollection + @entity class SmartList(BranchEntity): """A smart list.""" - smart_list_collection: ParentLink + smart_list_collection: ParentLink["SmartListCollection"] name: SmartListName icon: EntityIcon | None diff --git a/src/core/jupiter/core/smart_lists/sub/item/root.py b/src/core/jupiter/core/smart_lists/sub/item/root.py index 0921b79e2..46b987ed0 100644 --- a/src/core/jupiter/core/smart_lists/sub/item/root.py +++ b/src/core/jupiter/core/smart_lists/sub/item/root.py @@ -1,5 +1,7 @@ """A smart list item.""" +from typing import TYPE_CHECKING + from jupiter.core.common.sub.notes.namespace import NoteNamespace from jupiter.core.common.sub.notes.root import Note from jupiter.core.common.sub.tags.namespace import TagNamespace @@ -21,12 +23,15 @@ ) from jupiter.framework.update_action import UpdateAction +if TYPE_CHECKING: + from jupiter.core.smart_lists.root import SmartList + @entity class SmartListItem(LeafEntity): """A smart list item.""" - smart_list: ParentLink + smart_list: ParentLink["SmartList"] name: SmartListItemName is_done: bool url: URL | None diff --git a/src/core/jupiter/core/stats/log.py b/src/core/jupiter/core/stats/log.py index efbad69ad..da241eb85 100644 --- a/src/core/jupiter/core/stats/log.py +++ b/src/core/jupiter/core/stats/log.py @@ -1,5 +1,7 @@ """A log of stats computation actions a user has performed.""" +from typing import TYPE_CHECKING + from jupiter.core.stats.log_entry import StatsLogEntry from jupiter.framework.base.entity_id import EntityId from jupiter.framework.context import DomainContext @@ -12,12 +14,15 @@ entity, ) +if TYPE_CHECKING: + from jupiter.core.workspaces.root import Workspace + @entity class StatsLog(TrunkEntity): """A log of stats computation actions a user has performed.""" - workspace: ParentLink + workspace: ParentLink["Workspace"] entries = ContainsMany(StatsLogEntry, stats_log_ref_id=IsRefId()) diff --git a/src/core/jupiter/core/stats/log_entry.py b/src/core/jupiter/core/stats/log_entry.py index cc934ec62..5189d23e0 100644 --- a/src/core/jupiter/core/stats/log_entry.py +++ b/src/core/jupiter/core/stats/log_entry.py @@ -1,6 +1,7 @@ """A particular entry in the stats log.""" import abc +from typing import TYPE_CHECKING from jupiter.core.common.entity_summary import EntitySummary from jupiter.core.sync_target import SyncTarget @@ -18,12 +19,15 @@ ) from jupiter.framework.storage.repository import LeafEntityRepository +if TYPE_CHECKING: + from jupiter.core.stats.log import StatsLog + @entity class StatsLogEntry(LeafSupportEntity): """A particular entry in the stats log.""" - stats_log: ParentLink + stats_log: ParentLink["StatsLog"] source: str stats_targets: list[SyncTarget] today: ADate diff --git a/src/core/jupiter/core/time_plans/domain.py b/src/core/jupiter/core/time_plans/domain.py index c1229e881..69eb65250 100644 --- a/src/core/jupiter/core/time_plans/domain.py +++ b/src/core/jupiter/core/time_plans/domain.py @@ -1,5 +1,7 @@ """The time plan trunk domain object.""" +from typing import TYPE_CHECKING + from jupiter.core.common.difficulty import Difficulty from jupiter.core.common.eisen import Eisen from jupiter.core.common.recurring_task_gen_params import RecurringTaskGenParams @@ -25,12 +27,15 @@ from jupiter.framework.errors import InputValidationError from jupiter.framework.update_action import UpdateAction +if TYPE_CHECKING: + from jupiter.core.workspaces.root import Workspace + @entity class TimePlanDomain(TrunkEntity): """A time plan trunk domain object.""" - workspace: ParentLink + workspace: ParentLink["Workspace"] periods: set[RecurringTaskPeriod] generation_approach: TimePlanGenerationApproach diff --git a/src/core/jupiter/core/time_plans/life_plan_links.py b/src/core/jupiter/core/time_plans/life_plan_links.py index 5a323ec92..31097b1bb 100644 --- a/src/core/jupiter/core/time_plans/life_plan_links.py +++ b/src/core/jupiter/core/time_plans/life_plan_links.py @@ -1,6 +1,7 @@ """Links between time plans and life plan entities (chapters, aspects/aspects, goals).""" import abc +from typing import TYPE_CHECKING from jupiter.framework.base.entity_id import EntityId from jupiter.framework.context import DomainContext @@ -10,12 +11,15 @@ RecordRepository, ) +if TYPE_CHECKING: + from jupiter.core.time_plans.root import TimePlan + @record class TimePlanAspectLink(Record): """A link between a time plan and a aspect (aka aspect).""" - time_plan: ParentLink + time_plan: ParentLink["TimePlan"] aspect_ref_id: EntityId @staticmethod @@ -54,7 +58,7 @@ async def remove_all_for_aspect(self, aspect_ref_id: EntityId) -> None: class TimePlanChapterLink(Record): """A link between a time plan and a chapter.""" - time_plan: ParentLink + time_plan: ParentLink["TimePlan"] chapter_ref_id: EntityId @staticmethod @@ -93,7 +97,7 @@ async def remove_all_for_chapter(self, chapter_ref_id: EntityId) -> None: class TimePlanGoalLink(Record): """A link between a time plan and a goal.""" - time_plan: ParentLink + time_plan: ParentLink["TimePlan"] goal_ref_id: EntityId @staticmethod diff --git a/src/core/jupiter/core/time_plans/root.py b/src/core/jupiter/core/time_plans/root.py index 8f3bdfeef..d72873c0c 100644 --- a/src/core/jupiter/core/time_plans/root.py +++ b/src/core/jupiter/core/time_plans/root.py @@ -1,6 +1,7 @@ """A plan for a particular period of time.""" import abc +from typing import TYPE_CHECKING from jupiter.core.common import schedules from jupiter.core.common.recurring_task_period import RecurringTaskPeriod @@ -11,6 +12,11 @@ from jupiter.core.common.sub.tags.namespace import TagNamespace from jupiter.core.common.sub.tags.sub.link.root import TagLink from jupiter.core.common.timeline import infer_timeline +from jupiter.core.time_plans.life_plan_links import ( + TimePlanAspectLink, + TimePlanChapterLink, + TimePlanGoalLink, +) from jupiter.core.time_plans.source import TimePlanSource from jupiter.core.time_plans.sub.activity.root import TimePlanActivity from jupiter.framework.base.adate import ADate @@ -28,12 +34,16 @@ entity, update_entity_action, ) +from jupiter.framework.record import ContainsManyRecords from jupiter.framework.storage.repository import ( EntityAlreadyExistsError, LeafEntityRepository, ) from jupiter.framework.update_action import UpdateAction +if TYPE_CHECKING: + from jupiter.core.time_plans.domain import TimePlanDomain + class CannotModifyGeneratedTimePlanError(Exception): """Exception raised when you're trying to modify a generated time plan.""" @@ -47,7 +57,7 @@ class TimePlanExistsForDatePeriodCombinationError(EntityAlreadyExistsError): class TimePlan(LeafEntity): """A plan for a particular period of time.""" - time_plan_domain: ParentLink + time_plan_domain: ParentLink["TimePlanDomain"] source: TimePlanSource right_now: ADate @@ -57,6 +67,15 @@ class TimePlan(LeafEntity): end_date: ADate activities = ContainsMany(TimePlanActivity, time_plan_ref_id=IsRefId()) + time_plan_aspect_links = ContainsManyRecords( + TimePlanAspectLink, time_plan_ref_id=IsRefId() + ) + time_plan_chapter_links = ContainsManyRecords( + TimePlanChapterLink, time_plan_ref_id=IsRefId() + ) + time_plan_goal_links = ContainsManyRecords( + TimePlanGoalLink, time_plan_ref_id=IsRefId() + ) note = OwnsOne( Note, namespace=NoteNamespace.TIME_PLAN, source_entity_ref_id=IsRefId() ) diff --git a/src/core/jupiter/core/time_plans/sub/activity/root.py b/src/core/jupiter/core/time_plans/sub/activity/root.py index d5e18371b..d9ef39c81 100644 --- a/src/core/jupiter/core/time_plans/sub/activity/root.py +++ b/src/core/jupiter/core/time_plans/sub/activity/root.py @@ -1,6 +1,7 @@ """A certain activity that happens in a plan.""" import abc +from typing import TYPE_CHECKING from jupiter.core.archival_reason import JupiterArchivalReason from jupiter.core.big_plans.root import BigPlan @@ -37,12 +38,15 @@ ) from jupiter.framework.update_action import UpdateAction +if TYPE_CHECKING: + from jupiter.core.time_plans.root import TimePlan + @entity class TimePlanActivity(LeafEntity): """A certain activity that happens in a plan.""" - time_plan: ParentLink + time_plan: ParentLink["TimePlan"] target: TimePlanActivityTarget target_ref_id: EntityId diff --git a/src/core/jupiter/core/todo/components/properties-editor.tsx b/src/core/jupiter/core/todo/components/properties-editor.tsx index db883d5e9..b70fe897c 100644 --- a/src/core/jupiter/core/todo/components/properties-editor.tsx +++ b/src/core/jupiter/core/todo/components/properties-editor.tsx @@ -55,6 +55,7 @@ import { InboxTaskStatusBigTag } from "#/core/common/sub/inbox_tasks/component/s import { lifePlanBirthdayDate } from "#/core/life_plan/root"; import { LifePlanAssociations } from "#/core/life_plan/components/life-plan-associations"; import { isWorkspaceFeatureAvailable } from "#/core/workspaces/root"; +import { useBigScreen } from "#/core/infra/component/use-big-screen"; interface TodoTaskPropertiesEditorProps { title: string; @@ -84,6 +85,7 @@ export function TodoTaskPropertiesEditor(props: TodoTaskPropertiesEditorProps) { const [selectedAspectRefId, setSelectedAspectRefId] = useState( props.todoTask.aspect_ref_id, ); + const isBigScreen = useBigScreen(); return ( - + None: + op.create_index( + "ix_mutation_entity_event_mutation_id", + "mutation_entity_event", + ["mutation_id"], + ) + + +def downgrade() -> None: + op.drop_index( + "ix_mutation_entity_event_mutation_id", + table_name="mutation_entity_event", + ) diff --git a/src/core/migrations/versions/2026_04_05_21_59_add_source_field_to_mutation_invocation_.py b/src/core/migrations/versions/2026_04_05_21_59_add_source_field_to_mutation_invocation_.py new file mode 100644 index 000000000..ae204824d --- /dev/null +++ b/src/core/migrations/versions/2026_04_05_21_59_add_source_field_to_mutation_invocation_.py @@ -0,0 +1,34 @@ +"""'Add source field to mutation_invocation_record' + +Revision ID: 338cd79a9bb0 +Revises: 6d29645b5809 +Create Date: 2026-04-05 21:59:52.394330 + +""" + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = "338cd79a9bb0" +down_revision = "6d29645b5809" +branch_labels = None +depends_on = None + + +def upgrade(): + with op.batch_alter_table("mutation_invocation_record") as batch_op: + batch_op.add_column(sa.Column("source", sa.String, nullable=True)) + + op.execute( + "UPDATE mutation_invocation_record SET source = 'unknown' WHERE source IS NULL" + ) + + with op.batch_alter_table("mutation_invocation_record") as batch_op: + batch_op.alter_column("source", nullable=False) + + +def downgrade(): + with op.batch_alter_table("mutation_invocation_record") as batch_op: + batch_op.drop_column("source") diff --git a/src/desktop/package.json b/src/desktop/package.json index f970f7e81..fc3b80bc3 100644 --- a/src/desktop/package.json +++ b/src/desktop/package.json @@ -24,7 +24,7 @@ "@electron/osx-sign": "1.3.3", "electron": "^35.0.3", "plist": "^3.1.0", - "vite": "^6.2.2", + "vite": "^6.4.2", "vite-plugin-handlebars": "^1.5.0" }, "dependencies": { diff --git a/src/mobile/package.json b/src/mobile/package.json index c028da9b4..456eda5bb 100644 --- a/src/mobile/package.json +++ b/src/mobile/package.json @@ -27,7 +27,7 @@ "@capacitor/assets": "^3.0.5", "@capacitor/cli": "^7.0.0", "@trapezedev/configure": "^7.1.3", - "vite": "^6.2.2", + "vite": "^6.4.2", "vite-plugin-handlebars": "^1.5.0" } } diff --git a/src/webapi/jupiter/webapi/config.py b/src/webapi/jupiter/webapi/config.py index 9edecabcd..cffaa925e 100644 --- a/src/webapi/jupiter/webapi/config.py +++ b/src/webapi/jupiter/webapi/config.py @@ -366,6 +366,7 @@ async def simple_login( time_provider=self._request_time_provider, realm_codec_registry=self._realm_codec_registry, auth_token_stamper=self._auth_token_stamper, + invocation_recorder=self._invocation_recorder, ports=self._ports, ) diff --git a/src/webapi/jupiter/webapi/jupiter.py b/src/webapi/jupiter/webapi/jupiter.py index 82af5eca2..ab7c9f018 100644 --- a/src/webapi/jupiter/webapi/jupiter.py +++ b/src/webapi/jupiter/webapi/jupiter.py @@ -16,6 +16,7 @@ SqliteSearchStorageEngine, ) from jupiter.framework.auth.auth_token_stamper import AuthTokenStamper +from jupiter.framework.concepts.standard import ModuleExplorerConceptRegistry from jupiter.framework.mutation_inovcation.recorders.impl.sqlite import ( SqliteMutationInvocationStorageEngine, ) @@ -85,6 +86,10 @@ async def main() -> None: realm_codec_registry, sqlite_connection ) + concept_registry = ModuleExplorerConceptRegistry.build_from_module_root( + jupiter.core + ) + crm: CRM if ( global_properties.env == Env.PRODUCTION diff --git a/src/webui/app/routes/app/workspace/big-plans/$id.tsx b/src/webui/app/routes/app/workspace/big-plans/$id.tsx index beeed7797..906b29875 100644 --- a/src/webui/app/routes/app/workspace/big-plans/$id.tsx +++ b/src/webui/app/routes/app/workspace/big-plans/$id.tsx @@ -10,6 +10,7 @@ import type { Workspace, } from "@jupiter/webapi-client"; import { + NamedEntityTag, ApiError, BigPlanStatus, Difficulty, @@ -581,6 +582,8 @@ export default function BigPlan() { return ( parseInt(s, 10)) + .optional(), +}); + +export async function loader({ request }: LoaderFunctionArgs) { + const apiClient = await getLoggedInApiClient(request); + const query = parseQuery(request, QuerySchema); + + const result = await apiClient.infra.getEntityMutationHistory({ + entity_type: query.entityType, + entity_ref_id: query.entityRefId, + retrieve_offset: query.retrieveOffset, + }); + + return json({ + entries: result.entries, + users: result.users, + totalCnt: result.total_cnt, + pageSize: result.page_size, + }); +} diff --git a/src/webui/app/routes/app/workspace/journals/$id.tsx b/src/webui/app/routes/app/workspace/journals/$id.tsx index a3dc870fe..81ae7d9f6 100644 --- a/src/webui/app/routes/app/workspace/journals/$id.tsx +++ b/src/webui/app/routes/app/workspace/journals/$id.tsx @@ -1,5 +1,6 @@ import { ApiError, + NamedEntityTag, RecurringTaskPeriod, TagNamespace, WorkspaceFeature, @@ -209,6 +210,8 @@ export default function Journal() { return ( (); + const [searchParams, setSearchParams] = useSearchParams(); + const shouldShowALeafToo = useTrunkNeedsToShowLeaf(); + + const usersById = Object.fromEntries( + (users as InvocationUser[]).map((u) => [u.ref_id, u]), + ); + + const currentPage = Math.floor( + parseInt(searchParams.get("offset") ?? "0", 10) / pageSize, + ); + + const handlePageChange = useCallback( + (page: number) => { + const newParams = new URLSearchParams(searchParams); + if (page === 0) { + newParams.delete("offset"); + } else { + newParams.set("offset", (page * pageSize).toString()); + } + setSearchParams(newParams); + }, + [searchParams, setSearchParams, pageSize], + ); + + return ( + + + + + + {(entries as InvocationEntry[]).length === 0 && ( + + No mutation invocations found. + + )} + + {(entries as InvocationEntry[]).map((entry, idx) => ( + + ))} + + {(entries as InvocationEntry[]).length > 0 && ( + + )} + + + + + + + + + + ); +} + +function InvocationRow({ + entry, + user, +}: { + entry: InvocationEntry; + user: InvocationUser | undefined; +}) { + const [showArgs, setShowArgs] = useState(false); + const [showError, setShowError] = useState(false); + const formattedTimestamp = DateTime.fromISO(entry.timestamp).toLocaleString( + DateTime.DATETIME_MED, + ); + const mutationName = stripUseCaseSuffix(entry.mutation_name); + const userName = user?.name ?? "Unknown"; + + return ( + + + + {userName} ran{" "} + + + {mutationName} + + + + + + + + + {formattedTimestamp} · {entry.source} + + setShowArgs((s) => !s)}> + + + + + + + {entry.args_str} + + + + {entry.error_str && ( + <> + + + Error + + setShowError((s) => !s)}> + + + + + + {entry.error_str} + + + + )} + + ); +} + +interface PaginationControlsProps { + currentPage: number; + totalCnt: number; + pageSize: number; + onPageChange: (page: number) => void; +} + +function PaginationControls(props: PaginationControlsProps) { + const pageCount = Math.ceil(props.totalCnt / props.pageSize); + + if (pageCount <= 1) { + return null; + } + + const shouldShowPage = Array(pageCount).fill(false); + shouldShowPage[0] = true; + shouldShowPage[pageCount - 1] = true; + + for (let delta = -3; delta <= 3; delta++) { + const idx = props.currentPage + delta; + if (idx >= 0 && idx < pageCount) { + shouldShowPage[idx] = true; + } + } + + const buttons = []; + for (let i = 0; i < pageCount; i++) { + if (shouldShowPage[i]) { + buttons.push( + props.onPageChange(i)} + > + {i + 1} + , + ); + } else if (i > 0 && shouldShowPage[i - 1]) { + buttons.push( + + ... + , + ); + } + } + + return ( + + {buttons} + + ); +} + +export const ErrorBoundary = makeTrunkErrorBoundary("/app/workspace", { + error: () => + `There was an error loading the mutation history! Please try again!`, +}); diff --git a/src/webui/app/routes/app/workspace/mutation-history/$id.tsx b/src/webui/app/routes/app/workspace/mutation-history/$id.tsx new file mode 100644 index 000000000..9abfc03c1 --- /dev/null +++ b/src/webui/app/routes/app/workspace/mutation-history/$id.tsx @@ -0,0 +1,114 @@ +import { Stack } from "@mui/material"; +import { ApiError } from "@jupiter/webapi-client"; +import type { LoaderFunctionArgs } from "@remix-run/node"; +import { json } from "@remix-run/node"; +import type { ShouldRevalidateFunction } from "@remix-run/react"; +import { ReasonPhrases, StatusCodes } from "http-status-codes"; +import { z } from "zod"; +import { parseParams } from "zodix"; +import { makeLeafErrorBoundary } from "@jupiter/core/infra/component/error-boundary"; +import { LeafPanel } from "@jupiter/core/infra/component/layout/leaf-panel"; +import { EntityEventList } from "@jupiter/core/infra/component/layout/entity-event-list"; +import { DisplayType } from "@jupiter/core/infra/component/use-nested-entities"; + +import { useLoaderDataSafeForAnimation } from "~/rendering/use-loader-data-for-animation"; +import { standardShouldRevalidate } from "~/rendering/standard-should-revalidate"; +import { getLoggedInApiClient } from "~/api-clients.server"; + +const ParamsSchema = z.object({ + id: z.string(), +}); + +export const handle = { + displayType: DisplayType.LEAF, +}; + +export async function loader({ request, params }: LoaderFunctionArgs) { + const apiClient = await getLoggedInApiClient(request); + const { id } = parseParams(params, ParamsSchema); + + try { + const result = await apiClient.infra.getMutationEntityEvents({ + mutation_id: id, + }); + + return json({ + mutationName: result.mutation_name, + entries: result.entries, + users: result.users, + }); + } catch (error) { + if (error instanceof ApiError && error.status === StatusCodes.NOT_FOUND) { + throw new Response(ReasonPhrases.NOT_FOUND, { + status: StatusCodes.NOT_FOUND, + statusText: ReasonPhrases.NOT_FOUND, + }); + } + + throw error; + } +} + +export const shouldRevalidate: ShouldRevalidateFunction = + standardShouldRevalidate; + +export default function MutationDetail() { + const { mutationName, entries, users } = + useLoaderDataSafeForAnimation(); + + const usersById = Object.fromEntries( + (users as Array<{ ref_id: string; name: string }>).map((u) => [ + u.ref_id, + u, + ]), + ); + + const mutationLabel = mutationName.replace(/UseCase$/, ""); + + return ( + + + + ).map((e) => ({ + entity_name: e.entity_name, + event_kind: e.event_kind, + event_name: e.event_name, + timestamp: e.timestamp, + source: e.source, + user_ref_id: e.user_ref_id, + entity_version: e.entity_version, + data: e.data, + }))} + usersById={usersById} + emptyMessage="No entity events found for this mutation." + /> + + + ); +} + +export const ErrorBoundary = makeLeafErrorBoundary( + `/app/workspace/mutation-history`, + ParamsSchema, + { + notFound: (params) => `Could not find mutation #${params.id}!`, + error: (params) => + `There was an error loading mutation #${params.id}! Please try again!`, + }, +); diff --git a/src/webui/app/routes/app/workspace/prm/circles/$id.tsx b/src/webui/app/routes/app/workspace/prm/circles/$id.tsx index 1811ac021..04fdf0f56 100644 --- a/src/webui/app/routes/app/workspace/prm/circles/$id.tsx +++ b/src/webui/app/routes/app/workspace/prm/circles/$id.tsx @@ -1,4 +1,4 @@ -import { ApiError } from "@jupiter/webapi-client"; +import { ApiError, NamedEntityTag } from "@jupiter/webapi-client"; import { FormControl, InputLabel, OutlinedInput } from "@mui/material"; import type { ActionFunctionArgs, LoaderFunctionArgs } from "@remix-run/node"; import { json, redirect } from "@remix-run/node"; @@ -127,6 +127,8 @@ export default function Circle() { return ( - + - + =14"