UncountablePythonSDK 0.0.185__py3-none-any.whl → 0.0.187__py3-none-any.whl
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.
- examples/integration-server/jobs/materials_auto/example_scale_connection.py +150 -0
- examples/integration-server/jobs/materials_auto/profile.yaml +5 -0
- pkgs/argument_parser/parser_inner.py +18 -3
- pkgs/serialization_util/_get_type_for_serialization.py +4 -4
- pkgs/type_spec/emit_python.py +58 -25
- pkgs/type_spec/non_discriminated_union_exceptions.py +0 -2
- pkgs/type_spec/test.py +4 -4
- pkgs/type_spec/ui_entry_actions/generate_ui_entry_actions.py +17 -17
- uncountable/integration/scan_profiles.py +11 -4
- uncountable/integration/scheduler.py +123 -65
- uncountable/integration/service.py +74 -0
- uncountable/integration/service_runner.py +44 -0
- uncountable/types/__init__.py +2 -0
- uncountable/types/api/entity/add_entity_comment.py +43 -0
- uncountable/types/api/uploader/invoke_uploader.py +2 -2
- uncountable/types/async_batch_processor.py +3 -2
- uncountable/types/client_base.py +29 -3
- uncountable/types/entity_t.py +18 -0
- uncountable/types/job_definition.py +1 -0
- uncountable/types/job_definition_t.py +15 -0
- {uncountablepythonsdk-0.0.185.dist-info → uncountablepythonsdk-0.0.187.dist-info}/METADATA +2 -2
- {uncountablepythonsdk-0.0.185.dist-info → uncountablepythonsdk-0.0.187.dist-info}/RECORD +24 -20
- {uncountablepythonsdk-0.0.185.dist-info → uncountablepythonsdk-0.0.187.dist-info}/WHEEL +0 -0
- {uncountablepythonsdk-0.0.185.dist-info → uncountablepythonsdk-0.0.187.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import json
|
|
3
|
+
import random
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from decimal import Decimal
|
|
6
|
+
|
|
7
|
+
from uncountable.core.client import Client
|
|
8
|
+
from uncountable.integration.service import Service, ServiceArguments, register_service
|
|
9
|
+
from uncountable.types import entity_t, identifier_t, listing_t, sockets_t
|
|
10
|
+
from uncountable.types.base_t import ObjectId
|
|
11
|
+
from uncountable.types.integration_session_t import IntegrationSessionInstrument
|
|
12
|
+
from websockets.asyncio.client import connect
|
|
13
|
+
|
|
14
|
+
from pkgs.serialization_util import serialize_for_api
|
|
15
|
+
|
|
16
|
+
SCALE_EQUIPMENT_NAMES = [
|
|
17
|
+
"Instrument Scale One",
|
|
18
|
+
"Instrument Scale Two",
|
|
19
|
+
"Instrument Scale Three",
|
|
20
|
+
]
|
|
21
|
+
EQUIPMENT_ID_COLUMN_REF_NAME = "id"
|
|
22
|
+
EQUIPMENT_NAME_COLUMN_REF_NAME = "name"
|
|
23
|
+
SOCKET_URL = "ws://host.docker.internal:8765"
|
|
24
|
+
READING_INTERVAL_SECONDS = 1.0
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(kw_only=True)
|
|
28
|
+
class InstrumentRoom:
|
|
29
|
+
equipment_name: str
|
|
30
|
+
token: str
|
|
31
|
+
room_key: str
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass(kw_only=True)
|
|
35
|
+
class InstrumentReading:
|
|
36
|
+
equipment_name: str
|
|
37
|
+
value: Decimal
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _build_equipment_column(ref_name: str) -> listing_t.ColumnIdentifierEntityRefName:
|
|
41
|
+
return listing_t.ColumnIdentifierEntityRefName(
|
|
42
|
+
entity_type=entity_t.EntityType.EQUIPMENT, ref_name=ref_name
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _resolve_equipment_id(*, client: Client, equipment_name: str) -> ObjectId:
|
|
47
|
+
id_column = _build_equipment_column(EQUIPMENT_ID_COLUMN_REF_NAME)
|
|
48
|
+
name_column = _build_equipment_column(EQUIPMENT_NAME_COLUMN_REF_NAME)
|
|
49
|
+
results = client.fetch_listing(
|
|
50
|
+
entity_type=entity_t.EntityType.EQUIPMENT,
|
|
51
|
+
columns=[id_column],
|
|
52
|
+
filters=listing_t.FilterNodeColumnAnd(
|
|
53
|
+
filters=(
|
|
54
|
+
listing_t.FilterSpecEquals(
|
|
55
|
+
relation=listing_t.FilterRelation.EQUALS,
|
|
56
|
+
column=name_column,
|
|
57
|
+
value=equipment_name,
|
|
58
|
+
),
|
|
59
|
+
)
|
|
60
|
+
),
|
|
61
|
+
).results
|
|
62
|
+
if len(results) != 1:
|
|
63
|
+
raise Exception(
|
|
64
|
+
f"expected exactly one equipment named {equipment_name}, found {len(results)}"
|
|
65
|
+
)
|
|
66
|
+
equipment_id = results[0].column_values[0]
|
|
67
|
+
if not isinstance(equipment_id, int):
|
|
68
|
+
raise Exception(
|
|
69
|
+
f"expected an integer id for equipment named {equipment_name}, found {equipment_id!r}"
|
|
70
|
+
)
|
|
71
|
+
return ObjectId(equipment_id)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@register_service
|
|
75
|
+
class ScaleConnectionService(Service):
|
|
76
|
+
def run(self, args: ServiceArguments) -> None:
|
|
77
|
+
rooms = [
|
|
78
|
+
self._register_room(args=args, equipment_name=equipment_name)
|
|
79
|
+
for equipment_name in SCALE_EQUIPMENT_NAMES
|
|
80
|
+
]
|
|
81
|
+
asyncio.run(self._stream_readings(args=args, rooms=rooms))
|
|
82
|
+
|
|
83
|
+
def _register_room(
|
|
84
|
+
self, *, args: ServiceArguments, equipment_name: str
|
|
85
|
+
) -> InstrumentRoom:
|
|
86
|
+
equipment_id = _resolve_equipment_id(
|
|
87
|
+
client=args.client, equipment_name=equipment_name
|
|
88
|
+
)
|
|
89
|
+
registration = args.client.register_sockets_token(
|
|
90
|
+
socket_request=sockets_t.SocketRequestIntegrationSession(
|
|
91
|
+
integration_session=IntegrationSessionInstrument(
|
|
92
|
+
equipment_key=identifier_t.IdentifierKeyId(id=equipment_id)
|
|
93
|
+
)
|
|
94
|
+
)
|
|
95
|
+
).response
|
|
96
|
+
return InstrumentRoom(
|
|
97
|
+
equipment_name=equipment_name,
|
|
98
|
+
token=registration.token,
|
|
99
|
+
room_key=registration.room_key,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
async def _stream_readings(
|
|
103
|
+
self, *, args: ServiceArguments, rooms: list[InstrumentRoom]
|
|
104
|
+
) -> None:
|
|
105
|
+
rooms_by_name = {room.equipment_name: room for room in rooms}
|
|
106
|
+
async with connect(
|
|
107
|
+
SOCKET_URL, additional_headers={"X-UNC-EXTERNAL": "true"}
|
|
108
|
+
) as ws:
|
|
109
|
+
for room in rooms:
|
|
110
|
+
await ws.send(
|
|
111
|
+
json.dumps(
|
|
112
|
+
serialize_for_api(
|
|
113
|
+
sockets_t.JoinRoomWithTokenSocketClientMessage(
|
|
114
|
+
token=room.token
|
|
115
|
+
)
|
|
116
|
+
)
|
|
117
|
+
)
|
|
118
|
+
)
|
|
119
|
+
args.logger.log_info(f"Joined room for instrument {room.equipment_name}")
|
|
120
|
+
|
|
121
|
+
readings: asyncio.Queue[InstrumentReading] = asyncio.Queue()
|
|
122
|
+
emit_task = asyncio.create_task(
|
|
123
|
+
self._emit_readings(rooms=rooms, readings=readings)
|
|
124
|
+
)
|
|
125
|
+
try:
|
|
126
|
+
while True:
|
|
127
|
+
reading = await readings.get()
|
|
128
|
+
room = rooms_by_name[reading.equipment_name]
|
|
129
|
+
await ws.send(
|
|
130
|
+
json.dumps(
|
|
131
|
+
serialize_for_api(
|
|
132
|
+
sockets_t.SendInstrumentReadingClientMessage(
|
|
133
|
+
value=reading.value, room_key=room.room_key
|
|
134
|
+
)
|
|
135
|
+
)
|
|
136
|
+
)
|
|
137
|
+
)
|
|
138
|
+
finally:
|
|
139
|
+
emit_task.cancel()
|
|
140
|
+
|
|
141
|
+
async def _emit_readings(
|
|
142
|
+
self, *, rooms: list[InstrumentRoom], readings: asyncio.Queue[InstrumentReading]
|
|
143
|
+
) -> None:
|
|
144
|
+
while True:
|
|
145
|
+
room = random.choice(rooms)
|
|
146
|
+
value = Decimal(str(round(random.uniform(0, 500), 2)))
|
|
147
|
+
await readings.put(
|
|
148
|
+
InstrumentReading(equipment_name=room.equipment_name, value=value)
|
|
149
|
+
)
|
|
150
|
+
await asyncio.sleep(READING_INTERVAL_SECONDS)
|
|
@@ -5,6 +5,7 @@ import datetime
|
|
|
5
5
|
import math
|
|
6
6
|
import types
|
|
7
7
|
import typing
|
|
8
|
+
from collections.abc import Mapping
|
|
8
9
|
from datetime import date
|
|
9
10
|
from decimal import Decimal
|
|
10
11
|
from enum import Enum
|
|
@@ -140,18 +141,32 @@ def _build_parser_discriminated_union(
|
|
|
140
141
|
else discriminator_raw
|
|
141
142
|
)
|
|
142
143
|
|
|
144
|
+
accepted = ", ".join(sorted(discriminator_map))
|
|
145
|
+
|
|
143
146
|
def parse(value: typing.Any) -> typing.Any:
|
|
144
147
|
if context.options.allow_direct_type and dataclasses.is_dataclass(value):
|
|
145
148
|
discriminant = getattr(value, discriminator)
|
|
146
|
-
|
|
149
|
+
elif isinstance(value, Mapping):
|
|
147
150
|
discriminant = value.get(discriminator)
|
|
151
|
+
else:
|
|
152
|
+
raise ParserError(
|
|
153
|
+
f"expected-object-for-union; found {type(value).__name__} where an "
|
|
154
|
+
f"object with {discriminator} set to one of: {accepted} was expected",
|
|
155
|
+
value=value,
|
|
156
|
+
)
|
|
148
157
|
if discriminant is None:
|
|
149
|
-
raise ParserError(
|
|
158
|
+
raise ParserError(
|
|
159
|
+
f"missing-union-discriminant; set {discriminator} to one of: {accepted}"
|
|
160
|
+
)
|
|
150
161
|
if not isinstance(discriminant, str):
|
|
151
162
|
raise ParserError("union-discriminant-is-not-string", value=discriminant)
|
|
152
163
|
parser = discriminator_map.get(discriminant)
|
|
153
164
|
if parser is None:
|
|
154
|
-
raise ParserError(
|
|
165
|
+
raise ParserError(
|
|
166
|
+
f"missing-type-for-union-discriminant; {discriminator} must be one of: "
|
|
167
|
+
f"{accepted}",
|
|
168
|
+
value=discriminant,
|
|
169
|
+
)
|
|
155
170
|
return parser(value)
|
|
156
171
|
|
|
157
172
|
return parse
|
|
@@ -17,11 +17,11 @@ class SerializationType(StrEnum):
|
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
@functools.lru_cache(maxsize=500000)
|
|
20
|
-
def get_serialization_type(
|
|
21
|
-
super_classes = set(
|
|
20
|
+
def get_serialization_type(obj_type: Any) -> SerializationType:
|
|
21
|
+
super_classes = set(obj_type.__mro__)
|
|
22
22
|
|
|
23
23
|
# check is named tuple
|
|
24
|
-
if tuple in super_classes and hasattr(
|
|
24
|
+
if tuple in super_classes and hasattr(obj_type, "_fields"):
|
|
25
25
|
return SerializationType.NAMED_TUPLE
|
|
26
26
|
|
|
27
27
|
if list in super_classes or set in super_classes or tuple in super_classes:
|
|
@@ -30,7 +30,7 @@ def get_serialization_type(type: Any) -> SerializationType:
|
|
|
30
30
|
if dict in super_classes:
|
|
31
31
|
return SerializationType.DICT
|
|
32
32
|
|
|
33
|
-
if dataclasses.is_dataclass(
|
|
33
|
+
if dataclasses.is_dataclass(obj_type):
|
|
34
34
|
return SerializationType.DATACLASS
|
|
35
35
|
|
|
36
36
|
if Enum in super_classes and str in super_classes:
|
pkgs/type_spec/emit_python.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import dataclasses
|
|
2
|
+
import functools
|
|
2
3
|
import io
|
|
3
4
|
import os
|
|
4
5
|
from decimal import Decimal
|
|
@@ -1148,6 +1149,17 @@ def refer_to(ctx: TrackingContext, stype: builder.SpecType) -> str:
|
|
|
1148
1149
|
SpecEndpoint = builder.SpecEndpoint
|
|
1149
1150
|
|
|
1150
1151
|
|
|
1152
|
+
def _route_identifier(
|
|
1153
|
+
endpoint: SpecEndpoint, *, endpoint_root: str
|
|
1154
|
+
) -> tuple[str, str, str]:
|
|
1155
|
+
endpoint_specific_path = endpoint.path_per_api_endpoint[endpoint_root]
|
|
1156
|
+
return (
|
|
1157
|
+
endpoint_specific_path.path_dirname,
|
|
1158
|
+
endpoint_specific_path.path_basename,
|
|
1159
|
+
endpoint.method,
|
|
1160
|
+
)
|
|
1161
|
+
|
|
1162
|
+
|
|
1151
1163
|
def _emit_routes(*, builder: builder.SpecBuilder, config: PythonConfig) -> None:
|
|
1152
1164
|
for endpoint_root in builder.api_endpoints:
|
|
1153
1165
|
endpoints: list[SpecEndpoint] = []
|
|
@@ -1188,17 +1200,13 @@ from main.site.framework.types import {static_route_imports}
|
|
|
1188
1200
|
"""
|
|
1189
1201
|
)
|
|
1190
1202
|
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
endpoint_specific_path.path_dirname,
|
|
1195
|
-
endpoint_specific_path.path_basename,
|
|
1196
|
-
endpoint.method,
|
|
1197
|
-
)
|
|
1203
|
+
route_identifier = functools.partial(
|
|
1204
|
+
_route_identifier, endpoint_root=endpoint_root
|
|
1205
|
+
)
|
|
1198
1206
|
|
|
1199
|
-
sorted_endpoints = sorted(endpoints, key=
|
|
1207
|
+
sorted_endpoints = sorted(endpoints, key=route_identifier)
|
|
1200
1208
|
|
|
1201
|
-
assert len(endpoints) == len(set(map(
|
|
1209
|
+
assert len(endpoints) == len(set(map(route_identifier, endpoints))), (
|
|
1202
1210
|
"Endpoints are not unique"
|
|
1203
1211
|
)
|
|
1204
1212
|
|
|
@@ -1309,39 +1317,64 @@ def _emit_namespace_imports(
|
|
|
1309
1317
|
|
|
1310
1318
|
def _emit_id_source(*, builder: builder.SpecBuilder, config: PythonConfig) -> None:
|
|
1311
1319
|
"""
|
|
1312
|
-
Create a map of full-type-name to
|
|
1320
|
+
Create a map of full-type-name to enum data for use in id-source. The values are
|
|
1321
|
+
plain data (member values, labels, deprecated) so importing this file does not pull
|
|
1322
|
+
in the generated type modules.
|
|
1313
1323
|
"""
|
|
1314
1324
|
id_source_output = config.id_source_output
|
|
1315
1325
|
if id_source_output is None:
|
|
1316
1326
|
return
|
|
1317
|
-
enum_out = io.StringIO()
|
|
1318
|
-
enum_out.write(f"{LINT_HEADER}{MODIFY_NOTICE}\n")
|
|
1319
|
-
enum_out.write("import typing\n")
|
|
1320
|
-
enum_out.write("from enum import StrEnum\n")
|
|
1321
1327
|
|
|
1322
|
-
ctx = TrackingContext()
|
|
1323
|
-
# In this context the propername
|
|
1324
1328
|
named_enums = {
|
|
1325
|
-
builder.resolve_proper_name(string_enum):
|
|
1329
|
+
builder.resolve_proper_name(string_enum): string_enum
|
|
1326
1330
|
for string_enum in builder.emit_id_source_enums
|
|
1327
1331
|
}
|
|
1328
1332
|
|
|
1329
|
-
|
|
1330
|
-
out=enum_out, namespaces=ctx.namespaces, from_namespace=None, config=config
|
|
1331
|
-
)
|
|
1332
|
-
|
|
1333
|
+
body = io.StringIO()
|
|
1333
1334
|
known_keys = []
|
|
1334
|
-
|
|
1335
|
+
any_labels = False
|
|
1336
|
+
body.write("\nENUM_MAP: dict[str, EnumRegistryEntry] = {\n")
|
|
1335
1337
|
for key in sorted(named_enums.keys()):
|
|
1336
|
-
|
|
1338
|
+
string_enum = named_enums[key]
|
|
1339
|
+
entries = list(string_enum.values.values())
|
|
1340
|
+
member_values = [f'"{entry.value}"' for entry in entries]
|
|
1341
|
+
members = ", ".join(member_values)
|
|
1342
|
+
single_element_comma = "," if len(member_values) == 1 else ""
|
|
1343
|
+
labels = {
|
|
1344
|
+
entry.value: entry.label for entry in entries if entry.label is not None
|
|
1345
|
+
}
|
|
1346
|
+
deprecated = [entry.value for entry in entries if entry.deprecated is True]
|
|
1347
|
+
body.write(f'{INDENT}"{key}": EnumRegistryEntry(\n')
|
|
1348
|
+
body.write(f"{2 * INDENT}members=({members}{single_element_comma}),\n")
|
|
1349
|
+
if labels:
|
|
1350
|
+
any_labels = True
|
|
1351
|
+
body.write(f"{2 * INDENT}labels=MappingProxyType({{\n")
|
|
1352
|
+
for value, label in labels.items():
|
|
1353
|
+
body.write(f'{3 * INDENT}"{value}": "{label}",\n')
|
|
1354
|
+
body.write(f"{2 * INDENT}}}),\n")
|
|
1355
|
+
if deprecated:
|
|
1356
|
+
dep = ", ".join(f'"{value}"' for value in deprecated)
|
|
1357
|
+
body.write(f"{2 * INDENT}deprecated=frozenset({{{dep}}}),\n")
|
|
1358
|
+
body.write(f"{INDENT}),\n")
|
|
1337
1359
|
known_keys.append(f'"{key}"')
|
|
1338
|
-
|
|
1360
|
+
body.write(f"}}\n{MODIFY_NOTICE}\n")
|
|
1361
|
+
|
|
1362
|
+
enum_out = io.StringIO()
|
|
1363
|
+
enum_out.write(f"{LINT_HEADER}{MODIFY_NOTICE}\n")
|
|
1364
|
+
enum_out.write("import typing\n")
|
|
1365
|
+
if any_labels:
|
|
1366
|
+
enum_out.write("from types import MappingProxyType\n")
|
|
1367
|
+
enum_out.write(
|
|
1368
|
+
"from main.unc.materials.id_source_construct.enum_registry import "
|
|
1369
|
+
"EnumRegistryEntry\n"
|
|
1370
|
+
)
|
|
1371
|
+
enum_out.write(body.getvalue())
|
|
1339
1372
|
|
|
1340
1373
|
enum_out.write(f"\nKnownEnumsType = typing.Literal[\n{INDENT}")
|
|
1341
1374
|
enum_out.write(f",\n{INDENT}".join(known_keys))
|
|
1342
1375
|
enum_out.write(f"\n]\n{MODIFY_NOTICE}\n")
|
|
1343
1376
|
|
|
1344
|
-
enum_out.write(f'\n{MODIFY_NOTICE}\n__all__ = ["ENUM_MAP"]\n')
|
|
1377
|
+
enum_out.write(f'\n{MODIFY_NOTICE}\n__all__ = ["ENUM_MAP", "KnownEnumsType"]\n')
|
|
1345
1378
|
enum_out.write(f"{MODIFY_NOTICE}\n{LINT_FOOTER}")
|
|
1346
1379
|
|
|
1347
1380
|
util.rewrite_file(id_source_output, enum_out.getvalue())
|
|
@@ -2,8 +2,6 @@ NON_DISCRIMINATED_UNION_EXCEPTIONS = [
|
|
|
2
2
|
"audit_log.AuditLogDataValues",
|
|
3
3
|
"generate_tool_parameters.UnionWithoutDiscrim",
|
|
4
4
|
"output_calculation_entities.ConditionParameterFilterCondition",
|
|
5
|
-
"output_parameters.AnalyticalMethodParameterOptions",
|
|
6
|
-
"output_parameters.AnalyticalMethodLinkedOptionValue",
|
|
7
5
|
"recipes_redirect.RecipesRedirectResult",
|
|
8
6
|
"value_spec.ResolvedPathAll",
|
|
9
7
|
"deprecated_calculation_types.DeprecatedWeightedSumEntitiesV0",
|
pkgs/type_spec/test.py
CHANGED
|
@@ -2,11 +2,11 @@ from .util import format_parsed_type, parse_type_str
|
|
|
2
2
|
|
|
3
3
|
|
|
4
4
|
def test_parse() -> None:
|
|
5
|
-
def check(
|
|
6
|
-
pt = parse_type_str(
|
|
5
|
+
def check(type_str: str) -> None:
|
|
6
|
+
pt = parse_type_str(type_str)
|
|
7
7
|
fmt = format_parsed_type(pt)
|
|
8
|
-
assert
|
|
9
|
-
print(
|
|
8
|
+
assert type_str == fmt
|
|
9
|
+
print(type_str, "=", fmt)
|
|
10
10
|
|
|
11
11
|
check("Simple.path.more")
|
|
12
12
|
check("path.List<Integer>")
|
|
@@ -53,12 +53,12 @@ def ui_entry_variable_to_type_spec_type(
|
|
|
53
53
|
|
|
54
54
|
|
|
55
55
|
def construct_inputs_type_data(
|
|
56
|
-
|
|
56
|
+
action_vars: dict[str, ui_entry_actions_t.UiEntryActionVariable],
|
|
57
57
|
) -> RawDict:
|
|
58
|
-
if len(
|
|
58
|
+
if len(action_vars) == 0:
|
|
59
59
|
return {"type": BaseTypeName.s_object}
|
|
60
60
|
properties: dict[str, dict[str, str]] = {}
|
|
61
|
-
for input_name, input_defn in (
|
|
61
|
+
for input_name, input_defn in (action_vars).items():
|
|
62
62
|
properties[f"{input_name}"] = {
|
|
63
63
|
"type": ui_entry_variable_to_type_spec_type(input_defn)
|
|
64
64
|
}
|
|
@@ -66,12 +66,12 @@ def construct_inputs_type_data(
|
|
|
66
66
|
|
|
67
67
|
|
|
68
68
|
def construct_outputs_type_data(
|
|
69
|
-
|
|
69
|
+
action_vars: dict[str, ui_entry_actions_t.UiEntryActionOutput],
|
|
70
70
|
) -> RawDict:
|
|
71
|
-
if len(
|
|
71
|
+
if len(action_vars) == 0:
|
|
72
72
|
return {"type": BaseTypeName.s_object}
|
|
73
73
|
properties: dict[str, dict[str, str]] = {}
|
|
74
|
-
for output_name, output_defn in (
|
|
74
|
+
for output_name, output_defn in (action_vars).items():
|
|
75
75
|
# All outputs are optional
|
|
76
76
|
properties[f"{output_name}"] = {
|
|
77
77
|
"type": f"Optional<{convert_from_value_spec_type(output_defn.vs_type)}>"
|
|
@@ -82,7 +82,7 @@ def construct_outputs_type_data(
|
|
|
82
82
|
def construct_outputs_type(
|
|
83
83
|
*,
|
|
84
84
|
action_scope: ui_entry_actions_t.ActionScope,
|
|
85
|
-
|
|
85
|
+
action_vars: dict[str, ui_entry_actions_t.UiEntryActionOutput],
|
|
86
86
|
builder: SpecBuilder,
|
|
87
87
|
namespace: SpecNamespace,
|
|
88
88
|
) -> SpecTypeDefnObject:
|
|
@@ -93,7 +93,7 @@ def construct_outputs_type(
|
|
|
93
93
|
namespace.types[stype.name] = stype
|
|
94
94
|
stype.process(
|
|
95
95
|
builder=builder,
|
|
96
|
-
data=construct_outputs_type_data(
|
|
96
|
+
data=construct_outputs_type_data(action_vars=action_vars),
|
|
97
97
|
)
|
|
98
98
|
return stype
|
|
99
99
|
|
|
@@ -101,7 +101,7 @@ def construct_outputs_type(
|
|
|
101
101
|
def construct_inputs_type(
|
|
102
102
|
*,
|
|
103
103
|
action_scope: ui_entry_actions_t.ActionScope,
|
|
104
|
-
|
|
104
|
+
action_vars: dict[str, ui_entry_actions_t.UiEntryActionVariable],
|
|
105
105
|
builder: SpecBuilder,
|
|
106
106
|
namespace: SpecNamespace,
|
|
107
107
|
) -> SpecTypeDefnObject:
|
|
@@ -109,7 +109,7 @@ def construct_inputs_type(
|
|
|
109
109
|
namespace=namespace,
|
|
110
110
|
name=emit_typescript_util.ts_type_name(f"{action_scope}_inputs"),
|
|
111
111
|
)
|
|
112
|
-
stype.process(builder=builder, data=construct_inputs_type_data(
|
|
112
|
+
stype.process(builder=builder, data=construct_inputs_type_data(action_vars))
|
|
113
113
|
namespace.types[stype.name] = stype
|
|
114
114
|
return stype
|
|
115
115
|
|
|
@@ -140,13 +140,13 @@ def emit_entry_action_definition(
|
|
|
140
140
|
) -> EntryActionTypeInfo:
|
|
141
141
|
inputs_type = construct_inputs_type(
|
|
142
142
|
action_scope=action_scope,
|
|
143
|
-
|
|
143
|
+
action_vars=defn.inputs,
|
|
144
144
|
builder=builder,
|
|
145
145
|
namespace=ctx.namespace,
|
|
146
146
|
)
|
|
147
147
|
outputs_type = construct_outputs_type(
|
|
148
148
|
action_scope=action_scope,
|
|
149
|
-
|
|
149
|
+
action_vars=defn.outputs,
|
|
150
150
|
builder=builder,
|
|
151
151
|
namespace=ctx.namespace,
|
|
152
152
|
)
|
|
@@ -158,12 +158,12 @@ def emit_entry_action_definition(
|
|
|
158
158
|
)
|
|
159
159
|
|
|
160
160
|
|
|
161
|
-
def _validate_input(
|
|
162
|
-
if "_" in
|
|
163
|
-
raise ValueError(f"Expected camelCase for variable {
|
|
164
|
-
if not re.fullmatch(base_t.REF_NAME_STRICT_REGEX,
|
|
161
|
+
def _validate_input(variable: ui_entry_actions_t.UiEntryActionVariable) -> None:
|
|
162
|
+
if "_" in variable.vs_var_name:
|
|
163
|
+
raise ValueError(f"Expected camelCase for variable {variable.vs_var_name}")
|
|
164
|
+
if not re.fullmatch(base_t.REF_NAME_STRICT_REGEX, variable.vs_var_name):
|
|
165
165
|
raise ValueError(
|
|
166
|
-
f"Variable {
|
|
166
|
+
f"Variable {variable.vs_var_name} has invalid syntax. See REF_NAME_STRICT_REGEX"
|
|
167
167
|
)
|
|
168
168
|
|
|
169
169
|
|
|
@@ -21,7 +21,7 @@ def load_profiles() -> list[job_definition_t.ProfileMetadata]:
|
|
|
21
21
|
if entry.is_dir() and entry.name not in _IGNORED_PROFILE_FOLDERS
|
|
22
22
|
]
|
|
23
23
|
profile_details: list[job_definition_t.ProfileMetadata] = []
|
|
24
|
-
|
|
24
|
+
seen_ids: set[str] = set()
|
|
25
25
|
for profile_file in profiles:
|
|
26
26
|
profile_name = profile_file.name
|
|
27
27
|
try:
|
|
@@ -30,9 +30,14 @@ def load_profiles() -> list[job_definition_t.ProfileMetadata]:
|
|
|
30
30
|
resource="profile.yaml",
|
|
31
31
|
)
|
|
32
32
|
for job in definition.jobs:
|
|
33
|
-
if job.id in
|
|
34
|
-
raise Exception(f"multiple jobs with id {job.id}")
|
|
35
|
-
|
|
33
|
+
if job.id in seen_ids:
|
|
34
|
+
raise Exception(f"multiple jobs or services with id {job.id}")
|
|
35
|
+
seen_ids.add(job.id)
|
|
36
|
+
services = definition.services if definition.services is not None else []
|
|
37
|
+
for service in services:
|
|
38
|
+
if service.id in seen_ids:
|
|
39
|
+
raise Exception(f"multiple jobs or services with id {service.id}")
|
|
40
|
+
seen_ids.add(service.id)
|
|
36
41
|
|
|
37
42
|
if definition.environments is not None:
|
|
38
43
|
for integration_env in integration_envs:
|
|
@@ -42,6 +47,7 @@ def load_profiles() -> list[job_definition_t.ProfileMetadata]:
|
|
|
42
47
|
job_definition_t.ProfileMetadata(
|
|
43
48
|
name=profile_name,
|
|
44
49
|
jobs=definition.jobs,
|
|
50
|
+
services=services,
|
|
45
51
|
base_url=environment_config.base_url,
|
|
46
52
|
integration_server_base_url=environment_config.integration_server_base_url,
|
|
47
53
|
app_base_url=environment_config.app_base_url,
|
|
@@ -58,6 +64,7 @@ def load_profiles() -> list[job_definition_t.ProfileMetadata]:
|
|
|
58
64
|
job_definition_t.ProfileMetadata(
|
|
59
65
|
name=profile_name,
|
|
60
66
|
jobs=definition.jobs,
|
|
67
|
+
services=services,
|
|
61
68
|
base_url=definition.base_url,
|
|
62
69
|
integration_server_base_url=definition.integration_server_base_url,
|
|
63
70
|
app_base_url=definition.app_base_url,
|