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.
@@ -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)
@@ -117,3 +117,8 @@ jobs:
117
117
  executor:
118
118
  type: script
119
119
  import_path: example_parse
120
+ services:
121
+ - id: example_scale_connection
122
+ name: Example Scale Connection
123
+ import_path: example_scale_connection
124
+ enabled: true
@@ -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
- else:
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("missing-union-discriminant")
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("missing-type-for-union-discriminant", value=discriminant)
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(type: Any) -> SerializationType:
21
- super_classes = set(type.__mro__)
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(type, "_fields"):
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(type):
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:
@@ -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
- def _route_identifier(endpoint: SpecEndpoint) -> tuple[str, str, str]:
1192
- endpoint_specific_path = endpoint.path_per_api_endpoint[endpoint_root]
1193
- return (
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=_route_identifier)
1207
+ sorted_endpoints = sorted(endpoints, key=route_identifier)
1200
1208
 
1201
- assert len(endpoints) == len(set(map(_route_identifier, endpoints))), (
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 enums for use in id-source.
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): refer_to(ctx, 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
- _emit_namespace_imports(
1330
- out=enum_out, namespaces=ctx.namespaces, from_namespace=None, config=config
1331
- )
1332
-
1333
+ body = io.StringIO()
1333
1334
  known_keys = []
1334
- enum_out.write("\nENUM_MAP: dict[str, type[StrEnum]] = {\n")
1335
+ any_labels = False
1336
+ body.write("\nENUM_MAP: dict[str, EnumRegistryEntry] = {\n")
1335
1337
  for key in sorted(named_enums.keys()):
1336
- enum_out.write(f'{INDENT}"{key}": {named_enums[key]},\n')
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
- enum_out.write(f"}}\n{MODIFY_NOTICE}\n")
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(input: str) -> None:
6
- pt = parse_type_str(input)
5
+ def check(type_str: str) -> None:
6
+ pt = parse_type_str(type_str)
7
7
  fmt = format_parsed_type(pt)
8
- assert input == fmt
9
- print(input, "=", fmt)
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
- vars: dict[str, ui_entry_actions_t.UiEntryActionVariable],
56
+ action_vars: dict[str, ui_entry_actions_t.UiEntryActionVariable],
57
57
  ) -> RawDict:
58
- if len(vars) == 0:
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 (vars).items():
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
- vars: dict[str, ui_entry_actions_t.UiEntryActionOutput],
69
+ action_vars: dict[str, ui_entry_actions_t.UiEntryActionOutput],
70
70
  ) -> RawDict:
71
- if len(vars) == 0:
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 (vars).items():
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
- vars: dict[str, ui_entry_actions_t.UiEntryActionOutput],
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(vars=vars),
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
- vars: dict[str, ui_entry_actions_t.UiEntryActionVariable],
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(vars))
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
- vars=defn.inputs,
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
- vars=defn.outputs,
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(input: ui_entry_actions_t.UiEntryActionVariable) -> None:
162
- if "_" in input.vs_var_name:
163
- raise ValueError(f"Expected camelCase for variable {input.vs_var_name}")
164
- if not re.fullmatch(base_t.REF_NAME_STRICT_REGEX, input.vs_var_name):
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 {input.vs_var_name} has invalid syntax. See REF_NAME_STRICT_REGEX"
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
- seen_job_ids: set[str] = set()
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 seen_job_ids:
34
- raise Exception(f"multiple jobs with id {job.id}")
35
- seen_job_ids.add(job.id)
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,