@datalayer/core 1.1.66 → 1.2.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +43 -20
- package/lib/api/constants.d.ts +1 -1
- package/lib/api/constants.js +1 -1
- package/lib/api/contents/attachments.js +3 -7
- package/lib/api/contents/datasets.d.ts +48 -0
- package/lib/api/contents/datasets.js +44 -7
- package/lib/api/contents/runtimeMounts.js +1 -1
- package/lib/api/contents/sandboxUid.d.ts +13 -45
- package/lib/api/contents/sandboxUid.js +14 -65
- package/lib/api/evals/client.d.ts +402 -0
- package/lib/api/evals/client.js +359 -0
- package/lib/api/evals/index.d.ts +10 -0
- package/lib/api/evals/index.js +18 -0
- package/lib/api/evals/request.d.ts +43 -0
- package/lib/api/evals/request.js +67 -0
- package/lib/api/evals/status.d.ts +27 -0
- package/lib/api/evals/status.js +122 -0
- package/lib/api/evals/types.d.ts +1335 -0
- package/lib/api/evals/types.js +5 -0
- package/lib/api/iam/authentication.js +6 -4
- package/lib/api/iam/connectedAgents.d.ts +29 -0
- package/lib/api/iam/connectedAgents.js +35 -0
- package/lib/api/iam/healthz.js +2 -2
- package/lib/api/iam/identityProviders.d.ts +76 -0
- package/lib/api/iam/identityProviders.js +187 -0
- package/lib/api/iam/index.d.ts +1 -0
- package/lib/api/iam/index.js +1 -0
- package/lib/api/iam/mcpPolicy.d.ts +102 -1
- package/lib/api/iam/mcpPolicy.js +38 -2
- package/lib/api/iam/oauth2.js +18 -12
- package/lib/api/iam/profile.d.ts +17 -1
- package/lib/api/iam/profile.js +27 -0
- package/lib/api/iam/scimTokens.d.ts +79 -0
- package/lib/api/iam/scimTokens.js +141 -0
- package/lib/api/iam/secrets.js +10 -23
- package/lib/api/iam/trials.d.ts +38 -0
- package/lib/api/iam/trials.js +66 -0
- package/lib/api/index.d.ts +4 -0
- package/lib/api/index.js +4 -0
- package/lib/api/mcp/observability.d.ts +34 -11
- package/lib/api/mcp/observability.js +97 -59
- package/lib/api/orchestration/client.d.ts +53 -0
- package/lib/api/orchestration/client.js +105 -0
- package/lib/api/orchestration/events.d.ts +88 -0
- package/lib/api/orchestration/events.js +245 -0
- package/lib/api/orchestration/generated.d.ts +693 -0
- package/lib/api/orchestration/generated.js +687 -0
- package/lib/api/orchestration/index.d.ts +30 -0
- package/lib/api/orchestration/index.js +34 -0
- package/lib/api/orchestration/lifecycle.d.ts +39 -0
- package/lib/api/orchestration/lifecycle.js +57 -0
- package/lib/api/orchestration/measures.d.ts +55 -0
- package/lib/api/orchestration/measures.js +99 -0
- package/lib/api/otel/dashboards.d.ts +56 -0
- package/lib/api/otel/dashboards.js +50 -0
- package/lib/api/otel/index.d.ts +1 -0
- package/lib/api/otel/index.js +1 -0
- package/lib/api/otel/metrics.d.ts +16 -0
- package/lib/api/otel/metrics.js +15 -0
- package/lib/api/scheduler/client.d.ts +32 -0
- package/lib/api/scheduler/client.js +42 -0
- package/lib/api/scheduler/index.d.ts +8 -0
- package/lib/api/scheduler/index.js +16 -0
- package/lib/api/scheduler/request.d.ts +28 -0
- package/lib/api/scheduler/request.js +50 -0
- package/lib/api/scheduler/types.d.ts +88 -0
- package/lib/api/scheduler/types.js +5 -0
- package/lib/api/spacer/comments.d.ts +97 -0
- package/lib/api/spacer/comments.js +45 -0
- package/lib/api/spacer/index.d.ts +9 -0
- package/lib/api/spacer/index.js +17 -0
- package/lib/api/spacer/notebooks.d.ts +33 -0
- package/lib/api/spacer/notebooks.js +39 -0
- package/lib/api/spacer/request.d.ts +25 -0
- package/lib/api/spacer/request.js +46 -0
- package/lib/api/spacer/spaces.d.ts +68 -0
- package/lib/api/spacer/spaces.js +45 -0
- package/lib/api/utils/validation.d.ts +4 -4
- package/lib/api/utils/validation.js +10 -7
- package/lib/client/auth/strategies.js +1 -1
- package/lib/client/constants.d.ts +1 -0
- package/lib/client/constants.js +1 -0
- package/lib/client/mixins/IAMMixin.js +2 -2
- package/lib/components/animation/AnimatedText.d.ts +1 -44
- package/lib/components/animation/AnimatedText.js +6 -122
- package/lib/components/anonymous/AnonymousKeyExpired.d.ts +56 -0
- package/lib/components/anonymous/AnonymousKeyExpired.js +96 -0
- package/lib/components/anonymous/AnonymousKeyTimer.d.ts +63 -0
- package/lib/components/anonymous/AnonymousKeyTimer.js +140 -0
- package/lib/components/anonymous/index.d.ts +7 -0
- package/lib/components/anonymous/index.js +15 -0
- package/lib/components/avatars/UserAvatar.d.ts +7 -1
- package/lib/components/avatars/UserAvatar.js +15 -1
- package/lib/components/billing/BillingEntitySelect.d.ts +6 -0
- package/lib/components/billing/BillingEntitySelect.js +49 -10
- package/lib/components/billing/eligibility.d.ts +22 -0
- package/lib/components/billing/eligibility.js +27 -0
- package/lib/components/billing/index.d.ts +1 -0
- package/lib/components/billing/index.js +1 -0
- package/lib/components/checkout/StripeCheckout.d.ts +1 -1
- package/lib/components/checkout/StripeCheckout.js +1 -1
- package/lib/components/collaboration/LiveEditorCollaborators.d.ts +5 -0
- package/lib/components/collaboration/LiveEditorCollaborators.js +3 -1
- package/lib/components/display/DatalayerBox.d.ts +2 -3
- package/lib/components/display/DatalayerBox.js +1 -1
- package/lib/components/display/NavLink.js +3 -2
- package/lib/components/display/VisuallyHidden.d.ts +1 -2
- package/lib/components/labels/StatusLabels.d.ts +2 -10
- package/lib/components/labels/StatusLabels.js +2 -26
- package/lib/components/principal/PrincipalAppearance.d.ts +21 -42
- package/lib/components/principal/PrincipalAppearance.js +232 -174
- package/lib/components/principal/PrincipalAvatar.d.ts +8 -1
- package/lib/components/principal/PrincipalAvatar.js +2 -2
- package/lib/components/principal/PrincipalDetailsOverlay.d.ts +8 -1
- package/lib/components/principal/PrincipalDetailsOverlay.js +2 -2
- package/lib/components/screencapture/Screencapture.js +46 -3
- package/lib/components/sharing/ShareAccessComponent.d.ts +45 -4
- package/lib/components/sharing/ShareAccessComponent.js +110 -78
- package/lib/components/sharing/SharingEditor.d.ts +5 -1
- package/lib/components/sharing/SharingEditor.js +9 -4
- package/lib/components/sharing/index.d.ts +1 -0
- package/lib/components/sharing/index.js +1 -0
- package/lib/components/sharing/sandboxSharing.d.ts +26 -0
- package/lib/components/sharing/sandboxSharing.js +47 -0
- package/lib/components/spaces/SpaceDetailsOverlay.d.ts +39 -0
- package/lib/components/spaces/SpaceDetailsOverlay.js +80 -0
- package/lib/components/spaces/SpaceDisplay.d.ts +38 -0
- package/lib/components/spaces/SpaceDisplay.js +92 -0
- package/lib/components/spaces/index.d.ts +4 -0
- package/lib/components/spaces/index.js +7 -0
- package/lib/components/spaces/spaceDisplayModel.d.ts +93 -0
- package/lib/components/spaces/spaceDisplayModel.js +170 -0
- package/lib/config/Configuration.d.ts +4 -4
- package/lib/config/Configuration.js +7 -7
- package/lib/config/index.d.ts +1 -0
- package/lib/config/index.js +1 -0
- package/lib/config/planes.d.ts +45 -0
- package/lib/config/planes.js +53 -0
- package/lib/hooks/cacheConverters.d.ts +0 -7
- package/lib/hooks/cacheConverters.js +0 -25
- package/lib/hooks/index.d.ts +1 -0
- package/lib/hooks/index.js +1 -0
- package/lib/hooks/useBackdrop.d.ts +4 -5
- package/lib/hooks/useBillingEntityStore.js +3 -0
- package/lib/hooks/useCache.d.ts +197 -44
- package/lib/hooks/useCache.js +1586 -494
- package/lib/hooks/useContents.d.ts +22 -1
- package/lib/hooks/useContents.js +90 -21
- package/lib/hooks/useMcp.d.ts +87 -1
- package/lib/hooks/useMcp.js +248 -13
- package/lib/hooks/useNavigate.d.ts +41 -3
- package/lib/hooks/useNavigate.js +116 -52
- package/lib/hooks/usePrincipalCacheStore.js +3 -0
- package/lib/hooks/usePrincipalStore.js +3 -0
- package/lib/hooks/useSpaceCacheStore.d.ts +50 -0
- package/lib/hooks/useSpaceCacheStore.js +85 -0
- package/lib/hooks/useWindowSize.js +1 -1
- package/lib/models/CreditsDTO.d.ts +6 -5
- package/lib/models/CreditsDTO.js +7 -5
- package/lib/models/IAMProvidersSpecs.d.ts +10 -5
- package/lib/models/IAMProvidersSpecs.js +0 -23
- package/lib/models/McpBinding.d.ts +6 -0
- package/lib/models/Organization.d.ts +69 -0
- package/lib/models/Organization.js +96 -0
- package/lib/models/Secret.d.ts +1 -2
- package/lib/models/Secret.js +2 -18
- package/lib/models/StartedBy.d.ts +34 -0
- package/lib/models/StartedBy.js +36 -0
- package/lib/models/Team.d.ts +2 -0
- package/lib/models/Team.js +2 -0
- package/lib/models/User.d.ts +4 -0
- package/lib/models/User.js +3 -0
- package/lib/models/UserSettings.d.ts +13 -0
- package/lib/models/UserSettings.js +9 -0
- package/lib/models/contents/__fixtures__/v1-contracts.json +158 -30
- package/lib/navigation/adapters/react-router.d.ts +1 -1
- package/lib/navigation/adapters/react-router.js +3 -0
- package/lib/navigation/components.js +2 -3
- package/lib/routes/publicPaths.js +4 -0
- package/lib/state/index.d.ts +1 -0
- package/lib/state/index.js +1 -0
- package/lib/state/sessionEnd.d.ts +55 -0
- package/lib/state/sessionEnd.js +162 -0
- package/lib/state/substates/CoreState.js +3 -16
- package/lib/state/substates/IAMState.d.ts +3 -3
- package/lib/state/substates/IAMState.js +7 -0
- package/lib/state/substates/LayoutState.js +12 -0
- package/lib/state/substates/NavigationState.d.ts +82 -0
- package/lib/state/substates/NavigationState.js +255 -0
- package/lib/state/substates/ProfileState.d.ts +1 -0
- package/lib/state/substates/ProfileState.js +1 -0
- package/lib/state/substates/index.d.ts +1 -0
- package/lib/state/substates/index.js +1 -0
- package/lib/utils/Lazy.d.ts +20 -0
- package/lib/utils/Lazy.js +32 -6
- package/lib/utils/Screencapture.d.ts +15 -0
- package/lib/utils/Screencapture.js +26 -20
- package/lib/utils/WithSuspense.d.ts +11 -1
- package/lib/utils/WithSuspense.js +11 -1
- package/lib/views/mcp/AdmittedClients.d.ts +44 -0
- package/lib/views/mcp/AdmittedClients.js +59 -0
- package/lib/views/mcp/ApprovalQueue.d.ts +66 -0
- package/lib/views/mcp/ApprovalQueue.js +95 -0
- package/lib/views/mcp/ConnectedAgents.js +38 -3
- package/lib/views/mcp/EnterpriseConsole.d.ts +1 -1
- package/lib/views/mcp/EnterpriseConsole.js +23 -1
- package/lib/views/mcp/IdentityProviders.d.ts +33 -0
- package/lib/views/mcp/IdentityProviders.js +299 -0
- package/lib/views/mcp/McpDashboard.js +90 -7
- package/lib/views/mcp/McpHome.js +1 -1
- package/lib/views/mcp/McpObservability.d.ts +30 -1
- package/lib/views/mcp/McpObservability.js +105 -19
- package/lib/views/mcp/NotebookRuns.d.ts +39 -0
- package/lib/views/mcp/NotebookRuns.js +44 -0
- package/lib/views/mcp/OrganizationPolicy.js +2 -1
- package/lib/views/mcp/PersonalPolicy.js +2 -1
- package/lib/views/mcp/PolicyForm.d.ts +17 -1
- package/lib/views/mcp/PolicyForm.js +25 -3
- package/lib/views/mcp/PolicyHistory.d.ts +13 -0
- package/lib/views/mcp/PolicyHistory.js +14 -1
- package/lib/views/mcp/RunDetail.d.ts +87 -0
- package/lib/views/mcp/RunDetail.js +186 -0
- package/lib/views/mcp/ScimProvisioning.d.ts +54 -0
- package/lib/views/mcp/ScimProvisioning.js +173 -0
- package/lib/views/mcp/TeamPolicies.js +66 -5
- package/lib/views/mcp/ToolAccess.d.ts +60 -0
- package/lib/views/mcp/ToolAccess.js +101 -0
- package/lib/views/mcp/TraceTimeline.d.ts +81 -0
- package/lib/views/mcp/TraceTimeline.js +132 -0
- package/lib/views/mcp/index.d.ts +5 -0
- package/lib/views/mcp/index.js +5 -0
- package/lib/views/otel/simpleAuthStore.js +3 -0
- package/lib/views/secrets/SecretEdit.js +3 -2
- package/lib/views/secrets/SecretNew.js +5 -1
- package/package.json +271 -264
- package/scripts/generate-mcp-types.py +16 -10
- package/scripts/generate-orchestration-types.py +920 -0
|
@@ -0,0 +1,920 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# Copyright (c) 2023-2025 Datalayer, Inc.
|
|
3
|
+
# Distributed under the terms of the Modified BSD License.
|
|
4
|
+
|
|
5
|
+
"""
|
|
6
|
+
Generate deterministic TypeScript types from the canonical orchestration models.
|
|
7
|
+
|
|
8
|
+
The pydantic models in ``datalayer_core/orchestration`` are the source of
|
|
9
|
+
truth (PLAN_ORCHESTRATOR.md, 19.8) and ``src/api/orchestration/generated.ts``
|
|
10
|
+
is what they say, in TypeScript, for the browser and the app. This is the
|
|
11
|
+
``generate-contents-types.py`` and ``generate-mcp-types.py`` pattern with two
|
|
12
|
+
documents rather than one:
|
|
13
|
+
|
|
14
|
+
- the models themselves, walked, for every canonical record and vocabulary
|
|
15
|
+
— including those no route carries, such as a descriptor mapping;
|
|
16
|
+
- the control plane's OpenAPI document (O1-01), checked in by the agents
|
|
17
|
+
service, for the answers that wrap those records and for the operations
|
|
18
|
+
the app may call. Every canonical schema the control plane serves must be
|
|
19
|
+
the one the models give, or this refuses to write anything: a service
|
|
20
|
+
answering a different shape than ``core`` declares is exactly the drift the
|
|
21
|
+
generated file exists to catch. ``DATALAYER_ORCHESTRATION_OPENAPI`` names
|
|
22
|
+
another copy of that document.
|
|
23
|
+
|
|
24
|
+
What is written:
|
|
25
|
+
|
|
26
|
+
- one ``interface`` per model and one ``type`` per shared vocabulary, camel
|
|
27
|
+
case on the wire because the plan writes the model that way in section 5.2
|
|
28
|
+
and because a browser that has to convert is a browser that can convert
|
|
29
|
+
wrongly;
|
|
30
|
+
- ``ORCHESTRATION_LIFECYCLE``: the states, the terminal states and the
|
|
31
|
+
transition table of section 6.1, so the TypeScript state machine (O0-02)
|
|
32
|
+
reads the same data the Python one does rather than a copy of it;
|
|
33
|
+
- ``ORCHESTRATION_COMMANDS``: the twelve commands and which of them mutate,
|
|
34
|
+
so that "every mutating command carries an idempotency key" (6.4) is one
|
|
35
|
+
fact in one place;
|
|
36
|
+
- ``ORCHESTRATION_ACKNOWLEDGEMENT_ORDER``: the five milestones of 6.3, in
|
|
37
|
+
order, because "has it reached acceptance yet" is a comparison;
|
|
38
|
+
- ``ORCHESTRATION_LIMITS``: the default depth, fan-out and retry limits of
|
|
39
|
+
19.8, read from the models' own defaults;
|
|
40
|
+
- ``ORCHESTRATION_FIELDS``: which fields each model has, and which model
|
|
41
|
+
each nested one holds. TypeScript
|
|
42
|
+
types vanish at runtime and ``src/**/__tests__`` is outside the tsconfig,
|
|
43
|
+
so this is what lets the vitest suite hold the shared fixture to the same
|
|
44
|
+
contract the pytest suite holds it to;
|
|
45
|
+
- ``ORCHESTRATION_API``: every operation of the control plane — its command
|
|
46
|
+
name, method and path — so a client builds no orchestration URL of its
|
|
47
|
+
own and a spec can hold the app's calls to the service's routes.
|
|
48
|
+
|
|
49
|
+
``--check`` exits non-zero when the checked-in file is not what the models
|
|
50
|
+
give — the CI check that the file is current.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
from __future__ import annotations
|
|
54
|
+
|
|
55
|
+
import importlib
|
|
56
|
+
import json
|
|
57
|
+
import os
|
|
58
|
+
import pkgutil
|
|
59
|
+
import sys
|
|
60
|
+
from enum import Enum
|
|
61
|
+
from pathlib import Path
|
|
62
|
+
from typing import Any
|
|
63
|
+
|
|
64
|
+
ROOT = Path(__file__).parents[1]
|
|
65
|
+
sys.path.insert(0, str(ROOT))
|
|
66
|
+
DEFAULT_CONTROL_PLANE = (
|
|
67
|
+
ROOT.parents[2]
|
|
68
|
+
/ "k8s/services/ai-agents/datalayer_ai_agents/contracts/v1/orchestration-openapi.json"
|
|
69
|
+
)
|
|
70
|
+
CONTROL_PLANE = Path(
|
|
71
|
+
os.environ.get("DATALAYER_ORCHESTRATION_OPENAPI", DEFAULT_CONTROL_PLANE)
|
|
72
|
+
)
|
|
73
|
+
#: What FastAPI adds to every document for its own request validation. Not
|
|
74
|
+
#: the control plane's vocabulary, and a `ValidationError` exported from
|
|
75
|
+
#: `core` would shadow every other one a consumer imports.
|
|
76
|
+
FRAMEWORK_SCHEMAS = frozenset({"HTTPValidationError", "ValidationError"})
|
|
77
|
+
|
|
78
|
+
from datalayer_core.orchestration.base import CanonicalModel # noqa: E402
|
|
79
|
+
from datalayer_core.orchestration.commands import ( # noqa: E402
|
|
80
|
+
COMMAND_MODELS,
|
|
81
|
+
is_mutating,
|
|
82
|
+
)
|
|
83
|
+
from datalayer_core.orchestration.events import ACKNOWLEDGEMENT_ORDER # noqa: E402
|
|
84
|
+
from datalayer_core.orchestration.execution import ( # noqa: E402
|
|
85
|
+
DelegationLimits,
|
|
86
|
+
RetryPolicy,
|
|
87
|
+
)
|
|
88
|
+
from datalayer_core.orchestration.lifecycle import ( # noqa: E402
|
|
89
|
+
INITIAL_STATE,
|
|
90
|
+
TERMINAL_STATES,
|
|
91
|
+
TRANSITIONS,
|
|
92
|
+
ExecutionState,
|
|
93
|
+
LifecycleEvent,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
PACKAGE = "datalayer_core.orchestration"
|
|
97
|
+
OUTPUT = ROOT / "src/api/orchestration/generated.ts"
|
|
98
|
+
#: The licence header every source file in this repository carries, emitted
|
|
99
|
+
#: here for the same reason the MCP generator emits it: `fix-license-header`
|
|
100
|
+
#: adds one to whatever this writes, and then `--check` compares that against
|
|
101
|
+
#: output that never had one, and the two CI jobs break each other forever.
|
|
102
|
+
LICENCE = (
|
|
103
|
+
"/*\n"
|
|
104
|
+
" * Copyright (c) 2023-2025 Datalayer, Inc.\n"
|
|
105
|
+
" * Distributed under the terms of the Modified BSD License.\n"
|
|
106
|
+
" */"
|
|
107
|
+
)
|
|
108
|
+
HEADER = (
|
|
109
|
+
"/* This file is generated from the datalayer_core.orchestration "
|
|
110
|
+
"pydantic models and the control plane's OpenAPI document. Do not edit. */"
|
|
111
|
+
)
|
|
112
|
+
#: Everything under `src` is prettier-checked, and prettier rewrites double
|
|
113
|
+
#: quotes to single ones and reflows anything that does not fit. Emitting
|
|
114
|
+
#: `json.dumps` output here would mean the formatter and this generator
|
|
115
|
+
#: disagreed permanently about the same file, so what is emitted is what
|
|
116
|
+
#: prettier would have written: single quotes, 80 columns, trailing commas.
|
|
117
|
+
PRINT_WIDTH = 80
|
|
118
|
+
INDENT = " "
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def quote(value: str) -> str:
|
|
122
|
+
"""
|
|
123
|
+
Return a TypeScript string literal, quoted the way prettier quotes.
|
|
124
|
+
|
|
125
|
+
Parameters
|
|
126
|
+
----------
|
|
127
|
+
value : str
|
|
128
|
+
The string to quote.
|
|
129
|
+
|
|
130
|
+
Returns
|
|
131
|
+
-------
|
|
132
|
+
str
|
|
133
|
+
The literal, in single quotes.
|
|
134
|
+
"""
|
|
135
|
+
escaped = value.replace("\\", "\\\\").replace("'", "\\'")
|
|
136
|
+
return f"'{escaped}'"
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def type_name(reference: str) -> str:
|
|
140
|
+
"""
|
|
141
|
+
Return the model name a JSON Schema reference points at.
|
|
142
|
+
|
|
143
|
+
Parameters
|
|
144
|
+
----------
|
|
145
|
+
reference : str
|
|
146
|
+
A `$ref` string.
|
|
147
|
+
|
|
148
|
+
Returns
|
|
149
|
+
-------
|
|
150
|
+
str
|
|
151
|
+
The name at the end of it.
|
|
152
|
+
"""
|
|
153
|
+
return reference.rsplit("/", 1)[-1]
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def ts_type(schema: dict[str, Any]) -> str:
|
|
157
|
+
"""
|
|
158
|
+
Return the TypeScript type for one JSON Schema node.
|
|
159
|
+
|
|
160
|
+
Parameters
|
|
161
|
+
----------
|
|
162
|
+
schema : dict[str, Any]
|
|
163
|
+
The schema node.
|
|
164
|
+
|
|
165
|
+
Returns
|
|
166
|
+
-------
|
|
167
|
+
str
|
|
168
|
+
Its TypeScript spelling.
|
|
169
|
+
"""
|
|
170
|
+
if "$ref" in schema:
|
|
171
|
+
return type_name(schema["$ref"])
|
|
172
|
+
if "const" in schema:
|
|
173
|
+
return quote(schema["const"])
|
|
174
|
+
if "enum" in schema:
|
|
175
|
+
return " | ".join(quote(value) for value in schema["enum"])
|
|
176
|
+
if "oneOf" in schema:
|
|
177
|
+
return " | ".join(ts_type(item) for item in schema["oneOf"])
|
|
178
|
+
if "anyOf" in schema:
|
|
179
|
+
return " | ".join(ts_type(item) for item in schema["anyOf"])
|
|
180
|
+
kind = schema.get("type")
|
|
181
|
+
if isinstance(kind, list):
|
|
182
|
+
return " | ".join(
|
|
183
|
+
"null" if item == "null" else ts_type({"type": item}) for item in kind
|
|
184
|
+
)
|
|
185
|
+
if kind == "array":
|
|
186
|
+
return f"Array<{ts_type(schema.get('items', {}))}>"
|
|
187
|
+
if kind == "object":
|
|
188
|
+
additional = schema.get("additionalProperties")
|
|
189
|
+
return (
|
|
190
|
+
f"Record<string, {ts_type(additional)}>"
|
|
191
|
+
if isinstance(additional, dict)
|
|
192
|
+
else "Record<string, unknown>"
|
|
193
|
+
)
|
|
194
|
+
if kind in {"integer", "number"}:
|
|
195
|
+
return "number"
|
|
196
|
+
if kind == "boolean":
|
|
197
|
+
return "boolean"
|
|
198
|
+
if kind == "null":
|
|
199
|
+
return "null"
|
|
200
|
+
if kind == "string":
|
|
201
|
+
return "string"
|
|
202
|
+
return "unknown"
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def summary(description: str | None) -> str | None:
|
|
206
|
+
"""
|
|
207
|
+
Return the first paragraph of a docstring, as one line.
|
|
208
|
+
|
|
209
|
+
The whole docstring belongs in the Python module, where the reasoning
|
|
210
|
+
around it is; the TypeScript reader gets the sentence that says what the
|
|
211
|
+
type is and a pointer to the rest.
|
|
212
|
+
|
|
213
|
+
Parameters
|
|
214
|
+
----------
|
|
215
|
+
description : str | None
|
|
216
|
+
The description the schema carries, if any.
|
|
217
|
+
|
|
218
|
+
Returns
|
|
219
|
+
-------
|
|
220
|
+
str | None
|
|
221
|
+
One line, or None when there is nothing to say.
|
|
222
|
+
"""
|
|
223
|
+
if not description:
|
|
224
|
+
return None
|
|
225
|
+
paragraph = description.strip().split("\n\n")[0]
|
|
226
|
+
return " ".join(paragraph.split())
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def literal(value: Any, indent: int, used: int) -> list[str]:
|
|
230
|
+
"""
|
|
231
|
+
Render a JSON value as TypeScript, laid out the way prettier lays it out.
|
|
232
|
+
|
|
233
|
+
Prettier keeps a literal on one line when it fits inside the print width
|
|
234
|
+
and expands it when it does not, so that is what is emitted: the file is
|
|
235
|
+
then a fixed point of both this generator and the formatter.
|
|
236
|
+
|
|
237
|
+
Parameters
|
|
238
|
+
----------
|
|
239
|
+
value : Any
|
|
240
|
+
The value to render.
|
|
241
|
+
indent : int
|
|
242
|
+
How many levels the value is nested at.
|
|
243
|
+
used : int
|
|
244
|
+
How many columns are already taken on the opening line.
|
|
245
|
+
|
|
246
|
+
Returns
|
|
247
|
+
-------
|
|
248
|
+
list[str]
|
|
249
|
+
The lines, the first of them without its leading indentation.
|
|
250
|
+
"""
|
|
251
|
+
flat = _flat(value)
|
|
252
|
+
if used + len(flat) <= PRINT_WIDTH:
|
|
253
|
+
return [flat]
|
|
254
|
+
pad = INDENT * (indent + 1)
|
|
255
|
+
lines: list[str] = []
|
|
256
|
+
if isinstance(value, dict):
|
|
257
|
+
lines.append("{")
|
|
258
|
+
for key, item in value.items():
|
|
259
|
+
prefix = f"{_key(key)}: "
|
|
260
|
+
rendered = literal(item, indent + 1, len(pad) + len(prefix) + 1)
|
|
261
|
+
lines.append(f"{pad}{prefix}{rendered[0]}")
|
|
262
|
+
lines.extend(rendered[1:])
|
|
263
|
+
lines[-1] += ","
|
|
264
|
+
lines.append(f"{INDENT * indent}}}")
|
|
265
|
+
return lines
|
|
266
|
+
lines.append("[")
|
|
267
|
+
for item in value:
|
|
268
|
+
rendered = literal(item, indent + 1, len(pad) + 1)
|
|
269
|
+
lines.append(f"{pad}{rendered[0]}")
|
|
270
|
+
lines.extend(rendered[1:])
|
|
271
|
+
lines[-1] += ","
|
|
272
|
+
lines.append(f"{INDENT * indent}]")
|
|
273
|
+
return lines
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def _key(key: str) -> str:
|
|
277
|
+
"""
|
|
278
|
+
Return an object key, quoted only when TypeScript needs it quoted.
|
|
279
|
+
|
|
280
|
+
Parameters
|
|
281
|
+
----------
|
|
282
|
+
key : str
|
|
283
|
+
The key.
|
|
284
|
+
|
|
285
|
+
Returns
|
|
286
|
+
-------
|
|
287
|
+
str
|
|
288
|
+
The key as it is written in a literal.
|
|
289
|
+
"""
|
|
290
|
+
plain = key.replace("_", "a").isalnum() and not key[0].isdigit()
|
|
291
|
+
return key if plain else quote(key)
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def _flat(value: Any) -> str:
|
|
295
|
+
"""
|
|
296
|
+
Return the one-line rendering of a value.
|
|
297
|
+
|
|
298
|
+
Parameters
|
|
299
|
+
----------
|
|
300
|
+
value : Any
|
|
301
|
+
The value to render.
|
|
302
|
+
|
|
303
|
+
Returns
|
|
304
|
+
-------
|
|
305
|
+
str
|
|
306
|
+
The value on one line, without trailing comma.
|
|
307
|
+
"""
|
|
308
|
+
if isinstance(value, dict):
|
|
309
|
+
if not value:
|
|
310
|
+
return "{}"
|
|
311
|
+
inner = ", ".join(f"{_key(k)}: {_flat(v)}" for k, v in value.items())
|
|
312
|
+
return f"{{ {inner} }}"
|
|
313
|
+
if isinstance(value, (list, tuple)):
|
|
314
|
+
return f"[{', '.join(_flat(item) for item in value)}]"
|
|
315
|
+
if isinstance(value, str):
|
|
316
|
+
return quote(value)
|
|
317
|
+
if isinstance(value, bool):
|
|
318
|
+
return "true" if value else "false"
|
|
319
|
+
if value is None:
|
|
320
|
+
return "null"
|
|
321
|
+
return json.dumps(value)
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def _declare(name: str, annotation: str, value: Any) -> list[str]:
|
|
325
|
+
"""
|
|
326
|
+
Render one exported constant with an explicit type.
|
|
327
|
+
|
|
328
|
+
Parameters
|
|
329
|
+
----------
|
|
330
|
+
name : str
|
|
331
|
+
The constant's name.
|
|
332
|
+
annotation : str
|
|
333
|
+
Its TypeScript type.
|
|
334
|
+
value : Any
|
|
335
|
+
Its value.
|
|
336
|
+
|
|
337
|
+
Returns
|
|
338
|
+
-------
|
|
339
|
+
list[str]
|
|
340
|
+
The lines of the declaration.
|
|
341
|
+
"""
|
|
342
|
+
opening = f"export const {name}: {annotation} = "
|
|
343
|
+
flat = _flat(value)
|
|
344
|
+
if len(opening) + len(flat) + 1 <= PRINT_WIDTH:
|
|
345
|
+
return [f"{opening}{flat};"]
|
|
346
|
+
# Prettier breaks after the `=` and puts the value on its own line when it
|
|
347
|
+
# fits there, before it gives up and expands the literal.
|
|
348
|
+
if len(INDENT) + len(flat) + 1 <= PRINT_WIDTH:
|
|
349
|
+
return [opening.rstrip(), f"{INDENT}{flat};"]
|
|
350
|
+
rendered = literal(value, 0, len(opening))
|
|
351
|
+
return [f"{opening}{rendered[0]}", *rendered[1:-1], f"{rendered[-1]};"]
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def _union(name: str, members: list[str]) -> list[str]:
|
|
355
|
+
"""
|
|
356
|
+
Render a union type alias, broken over lines the way prettier breaks one.
|
|
357
|
+
|
|
358
|
+
Parameters
|
|
359
|
+
----------
|
|
360
|
+
name : str
|
|
361
|
+
The alias name.
|
|
362
|
+
members : list[str]
|
|
363
|
+
The union's members, already spelled in TypeScript.
|
|
364
|
+
|
|
365
|
+
Returns
|
|
366
|
+
-------
|
|
367
|
+
list[str]
|
|
368
|
+
The lines of the alias.
|
|
369
|
+
"""
|
|
370
|
+
joined = " | ".join(members)
|
|
371
|
+
single = f"export type {name} = {joined};"
|
|
372
|
+
if len(single) <= PRINT_WIDTH:
|
|
373
|
+
return [single]
|
|
374
|
+
# As with a constant, prettier first tries the whole union on the next
|
|
375
|
+
# line and only then puts each member on one of its own.
|
|
376
|
+
if len(INDENT) + len(joined) + 1 <= PRINT_WIDTH:
|
|
377
|
+
return [f"export type {name} =", f"{INDENT}{joined};"]
|
|
378
|
+
return [
|
|
379
|
+
f"export type {name} =",
|
|
380
|
+
*[f"{INDENT}| {member}" for member in members[:-1]],
|
|
381
|
+
f"{INDENT}| {members[-1]};",
|
|
382
|
+
]
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def _put(schemas: dict[str, Any], name: str, schema: dict[str, Any]) -> None:
|
|
386
|
+
"""
|
|
387
|
+
Add one schema, refusing two definitions of the same name.
|
|
388
|
+
|
|
389
|
+
Parameters
|
|
390
|
+
----------
|
|
391
|
+
schemas : dict[str, Any]
|
|
392
|
+
The schemas collected so far.
|
|
393
|
+
name : str
|
|
394
|
+
The name being defined.
|
|
395
|
+
schema : dict[str, Any]
|
|
396
|
+
Its definition.
|
|
397
|
+
|
|
398
|
+
Raises
|
|
399
|
+
------
|
|
400
|
+
SystemExit
|
|
401
|
+
When one name has two different definitions, which would make the
|
|
402
|
+
generated file depend on the order the modules were walked in.
|
|
403
|
+
"""
|
|
404
|
+
if name in schemas and schemas[name] != schema:
|
|
405
|
+
raise SystemExit(f"Two different definitions of '{name}' in {PACKAGE}.")
|
|
406
|
+
schemas[name] = schema
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def read_models() -> dict[str, Any]:
|
|
410
|
+
"""
|
|
411
|
+
Return every canonical schema, in an OpenAPI-shaped document.
|
|
412
|
+
|
|
413
|
+
The package is walked rather than listed, so a model added to it reaches
|
|
414
|
+
TypeScript without anybody remembering to name it here — and the check
|
|
415
|
+
gate fails until the generated file is regenerated. Enums are collected
|
|
416
|
+
directly as well as through the models that reference them: a vocabulary
|
|
417
|
+
no field happens to use, such as the context kinds, is still part of the
|
|
418
|
+
contract.
|
|
419
|
+
|
|
420
|
+
Returns
|
|
421
|
+
-------
|
|
422
|
+
dict[str, Any]
|
|
423
|
+
A document with `components.schemas`, as the other generators read.
|
|
424
|
+
"""
|
|
425
|
+
package = importlib.import_module(PACKAGE)
|
|
426
|
+
models: dict[str, type[CanonicalModel]] = {}
|
|
427
|
+
enums: dict[str, type[Enum]] = {}
|
|
428
|
+
for found in sorted(pkgutil.iter_modules(package.__path__), key=lambda m: m.name):
|
|
429
|
+
module = importlib.import_module(f"{PACKAGE}.{found.name}")
|
|
430
|
+
for member in vars(module).values():
|
|
431
|
+
if not isinstance(member, type):
|
|
432
|
+
continue
|
|
433
|
+
if not member.__module__.startswith(PACKAGE):
|
|
434
|
+
continue
|
|
435
|
+
if issubclass(member, CanonicalModel) and member is not CanonicalModel:
|
|
436
|
+
models[member.__name__] = member
|
|
437
|
+
elif issubclass(member, Enum):
|
|
438
|
+
enums[member.__name__] = member
|
|
439
|
+
schemas: dict[str, Any] = {}
|
|
440
|
+
for name, model in sorted(models.items()):
|
|
441
|
+
schema = model.model_json_schema(
|
|
442
|
+
ref_template="#/components/schemas/{model}", by_alias=True
|
|
443
|
+
)
|
|
444
|
+
for defined, definition in sorted(schema.pop("$defs", {}).items()):
|
|
445
|
+
_put(schemas, defined, definition)
|
|
446
|
+
_put(schemas, name, schema)
|
|
447
|
+
for name, enum in sorted(enums.items()):
|
|
448
|
+
if name not in schemas:
|
|
449
|
+
_put(schemas, name, _enum_schema(enum))
|
|
450
|
+
return {"components": {"schemas": schemas}}
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
def _without_null_defaults(schema: Any) -> Any:
|
|
454
|
+
"""
|
|
455
|
+
Return a schema without its `default: null` entries.
|
|
456
|
+
|
|
457
|
+
FastAPI leaves those out of an OpenAPI document and pydantic keeps them
|
|
458
|
+
in a model's schema; they say the same thing, and no TypeScript written
|
|
459
|
+
here reads them.
|
|
460
|
+
|
|
461
|
+
Parameters
|
|
462
|
+
----------
|
|
463
|
+
schema : Any
|
|
464
|
+
The schema node.
|
|
465
|
+
|
|
466
|
+
Returns
|
|
467
|
+
-------
|
|
468
|
+
Any
|
|
469
|
+
The node, with every null default removed at every depth.
|
|
470
|
+
"""
|
|
471
|
+
if isinstance(schema, dict):
|
|
472
|
+
return {
|
|
473
|
+
key: _without_null_defaults(value)
|
|
474
|
+
for key, value in schema.items()
|
|
475
|
+
if not (key == "default" and value is None)
|
|
476
|
+
}
|
|
477
|
+
if isinstance(schema, list):
|
|
478
|
+
return [_without_null_defaults(item) for item in schema]
|
|
479
|
+
return schema
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
def read_control_plane(models: dict[str, Any]) -> dict[str, Any]:
|
|
483
|
+
"""
|
|
484
|
+
Return the models' document with the control plane's answers and operations.
|
|
485
|
+
|
|
486
|
+
Parameters
|
|
487
|
+
----------
|
|
488
|
+
models : dict[str, Any]
|
|
489
|
+
The document `read_models` produced.
|
|
490
|
+
|
|
491
|
+
Returns
|
|
492
|
+
-------
|
|
493
|
+
dict[str, Any]
|
|
494
|
+
The canonical schemas, the control plane's own schemas beside them,
|
|
495
|
+
and the control plane's paths.
|
|
496
|
+
|
|
497
|
+
Raises
|
|
498
|
+
------
|
|
499
|
+
SystemExit
|
|
500
|
+
When the document is missing, or serves a canonical record in a shape
|
|
501
|
+
the models do not give.
|
|
502
|
+
"""
|
|
503
|
+
if not CONTROL_PLANE.exists():
|
|
504
|
+
raise SystemExit(
|
|
505
|
+
f"No orchestration OpenAPI document at {CONTROL_PLANE}: run "
|
|
506
|
+
"`make openapi-orchestration` in services/ai-agents, or name a copy "
|
|
507
|
+
"with DATALAYER_ORCHESTRATION_OPENAPI."
|
|
508
|
+
)
|
|
509
|
+
document = json.loads(CONTROL_PLANE.read_text())
|
|
510
|
+
schemas = dict(models["components"]["schemas"])
|
|
511
|
+
for name, schema in sorted(document["components"]["schemas"].items()):
|
|
512
|
+
if name in FRAMEWORK_SCHEMAS:
|
|
513
|
+
continue
|
|
514
|
+
if name in schemas:
|
|
515
|
+
if _without_null_defaults(schema) != _without_null_defaults(schemas[name]):
|
|
516
|
+
raise SystemExit(
|
|
517
|
+
f"The control plane serves '{name}' in a shape the "
|
|
518
|
+
f"{PACKAGE} models do not give; regenerate its document "
|
|
519
|
+
"from the models it imports."
|
|
520
|
+
)
|
|
521
|
+
continue
|
|
522
|
+
schemas[name] = schema
|
|
523
|
+
return {"components": {"schemas": schemas}, "paths": document["paths"]}
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
def _enum_schema(enum: type[Enum]) -> dict[str, Any]:
|
|
527
|
+
"""
|
|
528
|
+
Return the schema pydantic gives an enum, for one no model references.
|
|
529
|
+
|
|
530
|
+
Parameters
|
|
531
|
+
----------
|
|
532
|
+
enum : type[Enum]
|
|
533
|
+
The vocabulary.
|
|
534
|
+
|
|
535
|
+
Returns
|
|
536
|
+
-------
|
|
537
|
+
dict[str, Any]
|
|
538
|
+
Its JSON Schema.
|
|
539
|
+
"""
|
|
540
|
+
schema: dict[str, Any] = {
|
|
541
|
+
"enum": [member.value for member in enum],
|
|
542
|
+
"title": enum.__name__,
|
|
543
|
+
"type": "string",
|
|
544
|
+
}
|
|
545
|
+
if enum.__doc__:
|
|
546
|
+
schema["description"] = enum.__doc__.strip()
|
|
547
|
+
return dict(sorted(schema.items()))
|
|
548
|
+
|
|
549
|
+
|
|
550
|
+
def generate(document: dict[str, Any]) -> str:
|
|
551
|
+
"""
|
|
552
|
+
Return the whole of the generated TypeScript file.
|
|
553
|
+
|
|
554
|
+
Parameters
|
|
555
|
+
----------
|
|
556
|
+
document : dict[str, Any]
|
|
557
|
+
The document `read_models` produced.
|
|
558
|
+
|
|
559
|
+
Returns
|
|
560
|
+
-------
|
|
561
|
+
str
|
|
562
|
+
The file's content.
|
|
563
|
+
"""
|
|
564
|
+
schemas = document["components"]["schemas"]
|
|
565
|
+
lines = [LICENCE, "", HEADER, ""]
|
|
566
|
+
for name, schema in sorted(schemas.items()):
|
|
567
|
+
described = summary(schema.get("description"))
|
|
568
|
+
if described:
|
|
569
|
+
lines.append(f"/** {described} */")
|
|
570
|
+
if schema.get("type") == "object" and "properties" in schema:
|
|
571
|
+
lines.append(f"export interface {name} {{")
|
|
572
|
+
required = set(schema.get("required", []))
|
|
573
|
+
for property_name, property_schema in schema["properties"].items():
|
|
574
|
+
field = summary(property_schema.get("description"))
|
|
575
|
+
if field:
|
|
576
|
+
lines.append(f"{INDENT}/** {field} */")
|
|
577
|
+
optional = "" if property_name in required else "?"
|
|
578
|
+
lines.append(
|
|
579
|
+
f"{INDENT}{property_name}{optional}: {ts_type(property_schema)};"
|
|
580
|
+
)
|
|
581
|
+
lines.extend(["}", ""])
|
|
582
|
+
elif "enum" in schema:
|
|
583
|
+
lines.extend(_union(name, [quote(value) for value in schema["enum"]]))
|
|
584
|
+
lines.append("")
|
|
585
|
+
else:
|
|
586
|
+
lines.extend([f"export type {name} = {ts_type(schema)};", ""])
|
|
587
|
+
lines.extend(_lifecycle())
|
|
588
|
+
lines.extend(_commands())
|
|
589
|
+
lines.extend(_limits())
|
|
590
|
+
lines.extend(_fields(schemas))
|
|
591
|
+
lines.extend(_api(document.get("paths", {})))
|
|
592
|
+
return "\n".join(lines)
|
|
593
|
+
|
|
594
|
+
|
|
595
|
+
def _api(paths: dict[str, Any]) -> list[str]:
|
|
596
|
+
"""
|
|
597
|
+
Render every operation of the control plane (O1-01).
|
|
598
|
+
|
|
599
|
+
Parameters
|
|
600
|
+
----------
|
|
601
|
+
paths : dict[str, Any]
|
|
602
|
+
The `paths` of the control plane's OpenAPI document.
|
|
603
|
+
|
|
604
|
+
Returns
|
|
605
|
+
-------
|
|
606
|
+
list[str]
|
|
607
|
+
The lines of the type and the constant.
|
|
608
|
+
"""
|
|
609
|
+
value = [
|
|
610
|
+
{
|
|
611
|
+
"operation": operation["operationId"],
|
|
612
|
+
"method": method.upper(),
|
|
613
|
+
"path": path,
|
|
614
|
+
}
|
|
615
|
+
for path, operations in sorted(paths.items())
|
|
616
|
+
for method, operation in sorted(operations.items())
|
|
617
|
+
]
|
|
618
|
+
return [
|
|
619
|
+
"/** One operation of the control plane: its name, method and path. */",
|
|
620
|
+
"export interface OrchestrationOperation {",
|
|
621
|
+
f"{INDENT}readonly operation: string;",
|
|
622
|
+
f"{INDENT}readonly method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';",
|
|
623
|
+
f"{INDENT}readonly path: string;",
|
|
624
|
+
"}",
|
|
625
|
+
"",
|
|
626
|
+
*_declare("ORCHESTRATION_API", "readonly OrchestrationOperation[]", value),
|
|
627
|
+
"",
|
|
628
|
+
]
|
|
629
|
+
|
|
630
|
+
|
|
631
|
+
def _lifecycle() -> list[str]:
|
|
632
|
+
"""
|
|
633
|
+
Render the lifecycle of section 6.1 as data (O0-02).
|
|
634
|
+
|
|
635
|
+
Returns
|
|
636
|
+
-------
|
|
637
|
+
list[str]
|
|
638
|
+
The lines of the type and the constant.
|
|
639
|
+
"""
|
|
640
|
+
transitions = {
|
|
641
|
+
state.value: {event.value: target.value for event, target in moves.items()}
|
|
642
|
+
for state, moves in TRANSITIONS.items()
|
|
643
|
+
}
|
|
644
|
+
value = {
|
|
645
|
+
"initial": INITIAL_STATE.value,
|
|
646
|
+
"states": [state.value for state in ExecutionState],
|
|
647
|
+
"terminal": [
|
|
648
|
+
state.value for state in ExecutionState if state in TERMINAL_STATES
|
|
649
|
+
],
|
|
650
|
+
"events": [event.value for event in LifecycleEvent],
|
|
651
|
+
"transitions": transitions,
|
|
652
|
+
}
|
|
653
|
+
return [
|
|
654
|
+
"/** The lifecycle of section 6.1: what a state accepts, and where it leads. */",
|
|
655
|
+
"export interface OrchestrationLifecycle {",
|
|
656
|
+
f"{INDENT}readonly initial: ExecutionState;",
|
|
657
|
+
f"{INDENT}readonly states: readonly ExecutionState[];",
|
|
658
|
+
f"{INDENT}readonly terminal: readonly ExecutionState[];",
|
|
659
|
+
f"{INDENT}readonly events: readonly LifecycleEvent[];",
|
|
660
|
+
f"{INDENT}readonly transitions: Readonly<",
|
|
661
|
+
f"{INDENT}{INDENT}Record<ExecutionState, Readonly<LifecycleMoves>>",
|
|
662
|
+
f"{INDENT}>;",
|
|
663
|
+
"}",
|
|
664
|
+
"",
|
|
665
|
+
"/** Where one state goes, for the events it accepts. */",
|
|
666
|
+
"export type LifecycleMoves = Partial<Record<LifecycleEvent, ExecutionState>>;",
|
|
667
|
+
"",
|
|
668
|
+
*_declare("ORCHESTRATION_LIFECYCLE", "OrchestrationLifecycle", value),
|
|
669
|
+
"",
|
|
670
|
+
]
|
|
671
|
+
|
|
672
|
+
|
|
673
|
+
def _commands() -> list[str]:
|
|
674
|
+
"""
|
|
675
|
+
Render the twelve commands and which of them mutate (O0-03).
|
|
676
|
+
|
|
677
|
+
Returns
|
|
678
|
+
-------
|
|
679
|
+
list[str]
|
|
680
|
+
The lines of the type and the constants.
|
|
681
|
+
"""
|
|
682
|
+
commands = [
|
|
683
|
+
{"name": name.value, "mutating": is_mutating(name)} for name in COMMAND_MODELS
|
|
684
|
+
]
|
|
685
|
+
return [
|
|
686
|
+
"/** One of the twelve commands, and whether it needs an idempotency key. */",
|
|
687
|
+
"export interface OrchestrationCommand {",
|
|
688
|
+
f"{INDENT}readonly name: CommandName;",
|
|
689
|
+
f"{INDENT}readonly mutating: boolean;",
|
|
690
|
+
"}",
|
|
691
|
+
"",
|
|
692
|
+
*_declare(
|
|
693
|
+
"ORCHESTRATION_COMMANDS", "readonly OrchestrationCommand[]", commands
|
|
694
|
+
),
|
|
695
|
+
"",
|
|
696
|
+
"/** The five milestones of section 6.3, in the order they are reached. */",
|
|
697
|
+
*_declare(
|
|
698
|
+
"ORCHESTRATION_ACKNOWLEDGEMENT_ORDER",
|
|
699
|
+
"readonly AcknowledgementKind[]",
|
|
700
|
+
[kind.value for kind in ACKNOWLEDGEMENT_ORDER],
|
|
701
|
+
),
|
|
702
|
+
"",
|
|
703
|
+
]
|
|
704
|
+
|
|
705
|
+
|
|
706
|
+
def _limits() -> list[str]:
|
|
707
|
+
"""
|
|
708
|
+
Render the default delegation and retry limits of 19.8.
|
|
709
|
+
|
|
710
|
+
Returns
|
|
711
|
+
-------
|
|
712
|
+
list[str]
|
|
713
|
+
The lines of the type and the constant.
|
|
714
|
+
"""
|
|
715
|
+
value = {
|
|
716
|
+
"delegation": DelegationLimits().to_wire(),
|
|
717
|
+
"retry": RetryPolicy().to_wire(),
|
|
718
|
+
}
|
|
719
|
+
return [
|
|
720
|
+
"/** The default limits of 19.8, read from the models' own defaults. */",
|
|
721
|
+
"export interface OrchestrationLimits {",
|
|
722
|
+
f"{INDENT}readonly delegation: DelegationLimits;",
|
|
723
|
+
f"{INDENT}readonly retry: RetryPolicy;",
|
|
724
|
+
"}",
|
|
725
|
+
"",
|
|
726
|
+
*_declare("ORCHESTRATION_LIMITS", "OrchestrationLimits", value),
|
|
727
|
+
"",
|
|
728
|
+
]
|
|
729
|
+
|
|
730
|
+
|
|
731
|
+
def _is_record(schema: Any) -> bool:
|
|
732
|
+
"""
|
|
733
|
+
Say whether a schema describes a model rather than a vocabulary.
|
|
734
|
+
|
|
735
|
+
Parameters
|
|
736
|
+
----------
|
|
737
|
+
schema : Any
|
|
738
|
+
The schema node.
|
|
739
|
+
|
|
740
|
+
Returns
|
|
741
|
+
-------
|
|
742
|
+
bool
|
|
743
|
+
True for an object with properties.
|
|
744
|
+
"""
|
|
745
|
+
return (
|
|
746
|
+
isinstance(schema, dict)
|
|
747
|
+
and schema.get("type") == "object"
|
|
748
|
+
and "properties" in schema
|
|
749
|
+
)
|
|
750
|
+
|
|
751
|
+
|
|
752
|
+
def _referenced(schema: dict[str, Any], schemas: dict[str, Any]) -> str | None:
|
|
753
|
+
"""
|
|
754
|
+
Return the model a property holds, through an array or an optional.
|
|
755
|
+
|
|
756
|
+
Parameters
|
|
757
|
+
----------
|
|
758
|
+
schema : dict[str, Any]
|
|
759
|
+
The property's schema.
|
|
760
|
+
schemas : dict[str, Any]
|
|
761
|
+
Every collected schema, to tell a model from a vocabulary.
|
|
762
|
+
|
|
763
|
+
Returns
|
|
764
|
+
-------
|
|
765
|
+
str | None
|
|
766
|
+
The model's name, or None when the property holds no model.
|
|
767
|
+
"""
|
|
768
|
+
if "$ref" in schema:
|
|
769
|
+
name = type_name(schema["$ref"])
|
|
770
|
+
return name if _is_record(schemas.get(name)) else None
|
|
771
|
+
if schema.get("type") == "array":
|
|
772
|
+
return _referenced(schema.get("items", {}), schemas)
|
|
773
|
+
for option in schema.get("anyOf", []) + schema.get("oneOf", []):
|
|
774
|
+
found = _referenced(option, schemas)
|
|
775
|
+
if found:
|
|
776
|
+
return found
|
|
777
|
+
return None
|
|
778
|
+
|
|
779
|
+
|
|
780
|
+
def _fields(schemas: dict[str, Any]) -> list[str]:
|
|
781
|
+
"""
|
|
782
|
+
Render each model's fields, for the tests that run without a type checker.
|
|
783
|
+
|
|
784
|
+
``refs`` says which model each nested property holds, so the vitest
|
|
785
|
+
suite can walk a fixture document to its leaves the way pydantic does
|
|
786
|
+
rather than checking only its top level.
|
|
787
|
+
|
|
788
|
+
Parameters
|
|
789
|
+
----------
|
|
790
|
+
schemas : dict[str, Any]
|
|
791
|
+
The collected schemas.
|
|
792
|
+
|
|
793
|
+
Returns
|
|
794
|
+
-------
|
|
795
|
+
list[str]
|
|
796
|
+
The lines of the type and the constant.
|
|
797
|
+
"""
|
|
798
|
+
value = {
|
|
799
|
+
name: {
|
|
800
|
+
"required": sorted(schema.get("required", [])),
|
|
801
|
+
"optional": sorted(
|
|
802
|
+
set(schema["properties"]) - set(schema.get("required", []))
|
|
803
|
+
),
|
|
804
|
+
"refs": {
|
|
805
|
+
prop: referenced
|
|
806
|
+
for prop, sub in sorted(schema["properties"].items())
|
|
807
|
+
if (referenced := _referenced(sub, schemas))
|
|
808
|
+
},
|
|
809
|
+
}
|
|
810
|
+
for name, schema in sorted(schemas.items())
|
|
811
|
+
if _is_record(schema)
|
|
812
|
+
}
|
|
813
|
+
return [
|
|
814
|
+
"/** What each model declares, for the fixture checks of O0-01. */",
|
|
815
|
+
"export interface OrchestrationModelFields {",
|
|
816
|
+
f"{INDENT}readonly required: readonly string[];",
|
|
817
|
+
f"{INDENT}readonly optional: readonly string[];",
|
|
818
|
+
f"{INDENT}/** The model each nested property holds, by property name. */",
|
|
819
|
+
f"{INDENT}readonly refs: Readonly<Record<string, string>>;",
|
|
820
|
+
"}",
|
|
821
|
+
"",
|
|
822
|
+
*_declare(
|
|
823
|
+
"ORCHESTRATION_FIELDS", "Record<string, OrchestrationModelFields>", value
|
|
824
|
+
),
|
|
825
|
+
"",
|
|
826
|
+
]
|
|
827
|
+
|
|
828
|
+
|
|
829
|
+
#: The same operations for the Python client (O1-13), generated beside the
|
|
830
|
+
#: TypeScript so that neither client spells a route of its own.
|
|
831
|
+
PYTHON_OUTPUT = ROOT / "datalayer_core/orchestration/api.py"
|
|
832
|
+
|
|
833
|
+
|
|
834
|
+
def generate_python(document: dict[str, Any]) -> str:
|
|
835
|
+
"""
|
|
836
|
+
Return the generated Python route table of the control plane.
|
|
837
|
+
|
|
838
|
+
Parameters
|
|
839
|
+
----------
|
|
840
|
+
document : dict[str, Any]
|
|
841
|
+
The document `read_control_plane` produced.
|
|
842
|
+
|
|
843
|
+
Returns
|
|
844
|
+
-------
|
|
845
|
+
str
|
|
846
|
+
The module's content.
|
|
847
|
+
"""
|
|
848
|
+
operations = [
|
|
849
|
+
(operation["operationId"], method.upper(), path)
|
|
850
|
+
for path, methods in sorted(document.get("paths", {}).items())
|
|
851
|
+
for method, operation in sorted(methods.items())
|
|
852
|
+
]
|
|
853
|
+
lines = [
|
|
854
|
+
"# Copyright (c) 2023-2025 Datalayer, Inc.",
|
|
855
|
+
"# Distributed under the terms of the Modified BSD License.",
|
|
856
|
+
"",
|
|
857
|
+
'"""',
|
|
858
|
+
"Every operation of the orchestration control plane: its name, method and path.",
|
|
859
|
+
"",
|
|
860
|
+
"Generated from the control plane's OpenAPI document by",
|
|
861
|
+
"``scripts/generate-orchestration-types.py``; do not edit. The Python client",
|
|
862
|
+
"takes its paths from here as the TypeScript one takes them from",
|
|
863
|
+
"``ORCHESTRATION_API`` in ``src/api/orchestration/generated.ts``, so",
|
|
864
|
+
"neither spells a route of its own.",
|
|
865
|
+
'"""',
|
|
866
|
+
"",
|
|
867
|
+
"from __future__ import annotations",
|
|
868
|
+
"",
|
|
869
|
+
"from typing import NamedTuple",
|
|
870
|
+
"",
|
|
871
|
+
"",
|
|
872
|
+
"class OrchestrationOperation(NamedTuple):",
|
|
873
|
+
' """One operation of the control plane."""',
|
|
874
|
+
"",
|
|
875
|
+
" operation: str",
|
|
876
|
+
" method: str",
|
|
877
|
+
" path: str",
|
|
878
|
+
"",
|
|
879
|
+
"",
|
|
880
|
+
"ORCHESTRATION_API: tuple[OrchestrationOperation, ...] = (",
|
|
881
|
+
*[
|
|
882
|
+
f" OrchestrationOperation({json.dumps(name)}, {json.dumps(method)}, {json.dumps(path)}),"
|
|
883
|
+
for name, method, path in operations
|
|
884
|
+
],
|
|
885
|
+
")",
|
|
886
|
+
"",
|
|
887
|
+
]
|
|
888
|
+
return "\n".join(lines)
|
|
889
|
+
|
|
890
|
+
|
|
891
|
+
def main() -> None:
|
|
892
|
+
"""
|
|
893
|
+
Write the generated files, or check that the checked-in ones are current.
|
|
894
|
+
|
|
895
|
+
Raises
|
|
896
|
+
------
|
|
897
|
+
SystemExit
|
|
898
|
+
When `--check` is given and a file is stale.
|
|
899
|
+
"""
|
|
900
|
+
document = read_control_plane(read_models())
|
|
901
|
+
outputs = {OUTPUT: generate(document), PYTHON_OUTPUT: generate_python(document)}
|
|
902
|
+
if "--check" in sys.argv[1:]:
|
|
903
|
+
stale = [
|
|
904
|
+
str(path.relative_to(ROOT))
|
|
905
|
+
for path, expected in outputs.items()
|
|
906
|
+
if not path.exists() or path.read_text() != expected
|
|
907
|
+
]
|
|
908
|
+
if stale:
|
|
909
|
+
raise SystemExit(
|
|
910
|
+
f"Stale generated orchestration files ({', '.join(stale)}): "
|
|
911
|
+
"run `npm run generate:orchestration`"
|
|
912
|
+
)
|
|
913
|
+
return
|
|
914
|
+
for path, expected in outputs.items():
|
|
915
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
916
|
+
path.write_text(expected)
|
|
917
|
+
|
|
918
|
+
|
|
919
|
+
if __name__ == "__main__":
|
|
920
|
+
main()
|