fprime-gds 3.5.2a1__py3-none-any.whl → 3.6.0__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.
@@ -131,9 +131,8 @@ class CmdData(sys_data.SysData):
131
131
  else:
132
132
  # The arguments are currently serializable objects which cannot be
133
133
  # used to fill in a format string. Convert them to values that can be
134
- arg_val_list = [arg_obj.val for arg_obj in self.args]
135
-
136
- arg_str = " ".join(str(arg_val_list))
134
+ arg_val_list = self.get_arg_vals()
135
+ arg_str = str(arg_val_list)
137
136
 
138
137
  if verbose and csv:
139
138
  return f"{time_str},{raw_time_str},{name},{self.id},{arg_str}"
@@ -149,13 +148,13 @@ class CmdData(sys_data.SysData):
149
148
  args = []
150
149
  for val, arg_tuple in zip(input_values, self.template.arguments):
151
150
  try:
152
- _, _, arg_type = arg_tuple
151
+ arg_name, _, arg_type = arg_tuple
153
152
  arg_value = arg_type()
154
153
  self.convert_arg_value(val, arg_value)
155
154
  args.append(arg_value)
156
155
  errors.append("")
157
156
  except Exception as exc:
158
- errors.append(str(exc))
157
+ errors.append(f"{arg_name}[{arg_type.__name__}]: {exc}")
159
158
  return args, errors
160
159
 
161
160
  @staticmethod
@@ -178,7 +178,10 @@ class JsonLoader(dict_loader.DictLoader):
178
178
  SerializableType: The constructed serializable type.
179
179
 
180
180
  """
181
- struct_members = []
181
+ # Note on struct_members: the order of the members list matter when calling construct_type() below.
182
+ # It should be ordered by incrementing index which corresponds to the order in the FPP declaration
183
+ # The JSON dictionary ordering is not guaranteed, so we use a dict() to sort by index below.
184
+ struct_members = {}
182
185
  for name, member_dict in qualified_type.get("members").items():
183
186
  member_type_dict = member_dict["type"]
184
187
  member_type_obj = self.parse_type(member_type_dict)
@@ -197,11 +200,17 @@ class JsonLoader(dict_loader.DictLoader):
197
200
  member_type_obj.FORMAT if hasattr(member_type_obj, "FORMAT") else "{}"
198
201
  )
199
202
  description = member_type_dict.get("annotation", "")
200
- struct_members.append((name, member_type_obj, fmt_str, description))
203
+ member_index = member_dict["index"]
204
+ if member_index in struct_members:
205
+ raise KeyError(
206
+ f"Invalid dictionary: Duplicate index {member_index} in serializable type {type_name}"
207
+ )
208
+ struct_members[member_index] = (name, member_type_obj, fmt_str, description)
201
209
 
210
+ # Construct the serializable type with list of members sorted by index
202
211
  ser_type = SerializableType.construct_type(
203
212
  type_name,
204
- struct_members,
213
+ [struct_members[i] for i in sorted(struct_members.keys())],
205
214
  )
206
215
  self.parsed_types[type_name] = ser_type
207
216
  return ser_type
@@ -241,7 +241,7 @@ class ThreadedTCPSocketClient(ThreadedTransportClient):
241
241
  assert self.dest is not None, "Cannot send data before connect call"
242
242
  self.sock.send(b"A5A5 %s %s" % (self.dest, data))
243
243
 
244
- def recv(self, timeout=100):
244
+ def recv(self, timeout=0.1):
245
245
  """Receives data from the threaded tcp server
246
246
 
247
247
  Receives raw data from the threaded tcp server. This data is expected to have no headers and will be passed as
@@ -49,7 +49,7 @@ class CommandArgumentsInvalidException(werkzeug.exceptions.BadRequest):
49
49
  """Command arguments failed to validate properly"""
50
50
 
51
51
  def __init__(self, errors):
52
- super().__init__("Failed to validate all arguments")
52
+ super().__init__(f"Failed to validate all arguments: {', '.join(errors)}")
53
53
  self.args = errors
54
54
 
55
55
 
@@ -74,7 +74,7 @@ export function command_argument_assignment_helper(argument, squashed_argument_v
74
74
  command_argument_array_serializable_assignment_helper(argument, squashed_argument_value);
75
75
  } else {
76
76
  let is_not_string = typeof(argument.type.MAX_LENGTH) === "undefined";
77
- argument.value = (is_not_string && (squashed_argument_value === FILL_NEEDED)) ? null : squashed_argument_value;
77
+ argument.value = (is_not_string && (squashed_argument_value === FILL_NEEDED)) ? null : squashed_argument_value.toString();
78
78
  }
79
79
  }
80
80
 
@@ -119,7 +119,21 @@ export function squashify_argument(argument) {
119
119
  let field = argument.type.MEMBER_LIST[i][0];
120
120
  value[field] = squashify_argument(argument.value[field]);
121
121
  }
122
- } else if (["U64Type", "U32Type", "U16Type", "U8Type"].indexOf(argument.type.name) != -1) {
122
+ } else if (["U64Type"].indexOf(argument.type.name) !== -1) {
123
+ if (argument.value.startsWith("0x")) {
124
+ // Hexadecimal
125
+ value = BigInt(argument.value, 16);
126
+ } else if (argument.value.startsWith("0b")) {
127
+ // Binary
128
+ value = BigInt(argument.value.slice(2), 2);
129
+ } else if (argument.value.startsWith("0o")) {
130
+ // Octal
131
+ value = BigInt(argument.value.slice(2), 8);
132
+ } else {
133
+ // Decimal
134
+ value = BigInt(argument.value, 10);
135
+ }
136
+ } else if (["U32Type", "U16Type", "U8Type"].indexOf(argument.type.name) !== -1) {
123
137
  if (argument.value.startsWith("0x")) {
124
138
  // Hexadecimal
125
139
  value = parseInt(argument.value, 16);
@@ -134,10 +148,13 @@ export function squashify_argument(argument) {
134
148
  value = parseInt(argument.value, 10);
135
149
  }
136
150
  }
137
- else if (["I64Type", "I32Type", "I16Type", "I8Type"].indexOf(argument.type.name) != -1) {
151
+ else if (["I64Type"].indexOf(argument.type.name) !== -1) {
152
+ value = BigInt(argument.value, 10);
153
+ }
154
+ else if (["I32Type", "I16Type", "I8Type"].indexOf(argument.type.name) !== -1) {
138
155
  value = parseInt(argument.value, 10);
139
156
  }
140
- else if (["F64Type", "F32Type"].indexOf(argument.type.name) != -1) {
157
+ else if (["F64Type", "F32Type"].indexOf(argument.type.name) !== -1) {
141
158
  value = parseFloat(argument.value);
142
159
  }
143
160
  else if (argument.type.name == "BoolType") {
@@ -160,22 +177,35 @@ export function squashify_argument(argument) {
160
177
  * @returns: string to display
161
178
  */
162
179
  export function argument_display_string(argument) {
163
- // Base assignment of the value
164
- let string = `${(argument.value == null || argument.value === "") ? FILL_NEEDED: argument.value}`;
165
-
166
- if (argument.type.LENGTH) {
167
- string = `[${argument.value.map((argument) => argument_display_string(argument)).join(", ")}]`;
168
- } else if (argument.type.MEMBER_LIST) {
169
- let fields = [];
170
- for (let i = 0; i < argument.type.MEMBER_LIST.length; i++) {
171
- let field = argument.type.MEMBER_LIST[i][0];
172
- fields.push(`${field}: ${argument_display_string(argument.value[field])}`);
180
+ let string = FILL_NEEDED;
181
+ try {
182
+ // Check for array
183
+ if (argument.type.LENGTH) {
184
+ string = `[${argument.value.map((argument) => argument_display_string(argument)).join(", ")}]`;
185
+ }
186
+ // Serializable
187
+ else if (argument.type.MEMBER_LIST) {
188
+ let fields = [];
189
+ for (let i = 0; i < argument.type.MEMBER_LIST.length; i++) {
190
+ let field = argument.type.MEMBER_LIST[i][0];
191
+ fields.push(`${field}: ${argument_display_string(argument.value[field])}`);
192
+ }
193
+ string = `{${fields.join(", ")}}`
194
+ }
195
+ // String type
196
+ else if (argument.type.MAX_LENGTH) {
197
+ let value = (argument.value == null) ? "" : argument.value;
198
+ value = value.replace(/"/g, '\\\"');
199
+ string = `"${value}"`
200
+ }
201
+ // Unassigned values
202
+ else if (argument.value == null || argument.value === "") {
203
+ string = FILL_NEEDED;
204
+ } else {
205
+ string = squashify_argument(argument);
173
206
  }
174
- string = `{${fields.join(", ")}}`
175
- } else if (argument.type.MAX_LENGTH) {
176
- let value = (argument.value == null) ? "" : argument.value;
177
- value = value.replace(/"/g, '\\\"');
178
- string = `"${value}"`
207
+ } catch (e) {
208
+ string = FILL_NEEDED;
179
209
  }
180
210
  return string;
181
211
  }
@@ -291,7 +321,7 @@ Vue.component("command-scalar-argument", {
291
321
  return ["text", "0[bB][01]+|0[oO][0-7]+|0[xX][0-9a-fA-F]+|[1-9]\\d*|0", ""];
292
322
  }
293
323
  else if (["I64Type", "I32Type", "I16Type", "I8Type"].indexOf(this.argument.type.name) != -1) {
294
- return ["number", null, "1"];
324
+ return ["text", "-?[1-9]\\d*|0", ""];
295
325
  }
296
326
  else if (["F64Type", "F32Type"].indexOf(this.argument.type.name) != -1) {
297
327
  return ["number", null, "any"];
@@ -10,7 +10,8 @@ import {command_argument_assignment_helper} from "./arguments.js";
10
10
  import {listExistsAndItemNameNotInList, timeToString} from "../../js/vue-support/utils.js";
11
11
  import {command_history_template} from "./command-history-template.js";
12
12
  import {command_display_string} from "./command-string.js";
13
-
13
+ import { SaferParser } from "../../js/json.js";
14
+ SaferParser.register();
14
15
 
15
16
  /**
16
17
  * command-history:
@@ -103,27 +104,7 @@ Vue.component("command-history", {
103
104
  // Can only set command if it is a child of a command input
104
105
  if (this.$parent.selectCmd) {
105
106
  // command-input expects an array of strings as arguments
106
- this.$parent.selectCmd(cmd.full_name, this.preprocess_args(cmd.args));
107
- }
108
- },
109
- /**
110
- * Process the arguments for a command. If the argument is (or contains) a number, it
111
- * is converted to a string. Other types that should be pre-processed can be added here.
112
- *
113
- * @param {*} args
114
- * @returns args processed for command input (numbers converted to strings)
115
- */
116
- preprocess_args(args) {
117
- if (Array.isArray(args)) {
118
- return args.map(el => this.preprocess_args(el));
119
- } else if (typeof args === 'object' && args !== null) {
120
- return Object.fromEntries(
121
- Object.entries(args).map(([key, value]) => [key, this.preprocess_args(value)])
122
- );
123
- } else if (typeof args === 'number') {
124
- return args.toString();
125
- } else {
126
- return args;
107
+ this.$parent.selectCmd(cmd.full_name, cmd.args);
127
108
  }
128
109
  }
129
110
  }
@@ -15,6 +15,8 @@ import {
15
15
  } from "../../addons/commanding/arguments.js";
16
16
  import {_settings} from "../../js/settings.js";
17
17
  import {command_input_template} from "./command-input-template.js";
18
+ import { SaferParser } from "../../js/json.js";
19
+ SaferParser.register();
18
20
 
19
21
  /**
20
22
  * This helper will help assign command and values in a safe manner by searching the command store, finding a reference,
@@ -38,6 +40,7 @@ function command_assignment_helper(desired_command_name, desired_command_args, p
38
40
  return null;
39
41
  }
40
42
  let selected = _datastore.commands[command_name];
43
+
41
44
  // Set arguments here
42
45
  for (let i = 0; i < selected.args.length; i++) {
43
46
  let assign_value = (desired_command_args.length > i)? desired_command_args[i] : null;
@@ -147,6 +150,7 @@ Vue.component("command-input", {
147
150
  * command reaches the ground system.
148
151
  */
149
152
  sendCommand() {
153
+
150
154
  // Validate the command before sending anything
151
155
  if (!this.validate()) {
152
156
  return;
@@ -158,8 +162,9 @@ Vue.component("command-input", {
158
162
  let _self = this;
159
163
  _self.active = true;
160
164
  let command = this.selected;
165
+ let squashed_args = command.args.map(serialize_arg);
161
166
  this.loader.load("/commands/" + command.full_name, "PUT",
162
- {"key":0xfeedcafe, "arguments": command.args.map(serialize_arg)})
167
+ {"key":0xfeedcafe, "arguments": squashed_args})
163
168
  .then(function() {
164
169
  _self.active = false;
165
170
  // Clear errors, as there is not a problem further
@@ -3,7 +3,8 @@
3
3
  *
4
4
  * Contains the templates used to render the command string input box.
5
5
  */
6
- export let COMMAND_FORMAT_SPEC = "FULL_COMMAND_NAME[[[, ARG1], ARG2], ...]";
6
+ export let COMMAND_FORMAT_SPEC = "FULL_COMMAND_NAME[[[, ARG1], ARG2], ...] " +
7
+ "where ARGN is a decimal number, quoted string, or an enumerated constant";
7
8
 
8
9
  export let command_string_template = `
9
10
  <div class="fp-flex-repeater">
@@ -10,6 +10,8 @@ import {
10
10
  command_string_template
11
11
  } from "./command-string-template.js";
12
12
  import {argument_display_string, FILL_NEEDED} from "./arguments.js"
13
+ import {SaferParser} from "../../js/json.js";
14
+ SaferParser.register();
13
15
 
14
16
  let STRING_PREPROCESSOR = new RegExp(`(?:"((?:[^\"]|\\\")*)")|([a-zA-Z_][a-zA-Z_0-9.]*)|(${FILL_NEEDED})`, "g");
15
17
 
@@ -61,7 +63,9 @@ Vue.component("command-text", {
61
63
  } catch (e) {
62
64
  // JSON parsing exceptions
63
65
  if (e instanceof SyntaxError) {
64
- this.error = `Expected command string of the form: ${COMMAND_FORMAT_SPEC}`;
66
+ this.error = `Expected command string of the form: ${COMMAND_FORMAT_SPEC}.`;
67
+ } else {
68
+ throw e;
65
69
  }
66
70
  }
67
71
  }
@@ -16,6 +16,8 @@ import {_datastore} from "../../js/datastore.js";
16
16
  import {basicSetup, EditorState, EditorView, linter} from "./third/code-mirror.es.js"
17
17
  import {sequenceLanguageSupport} from "./autocomplete.js"
18
18
  import {processResponse} from "./lint.js";
19
+ import { SaferParser } from "../../js/json.js";
20
+ SaferParser.register();
19
21
 
20
22
  /**
21
23
  * Sequence sender function used to uplink the sequence and return a promise of how to handle the server's return.
@@ -0,0 +1,333 @@
1
+ /**
2
+ * json.js:
3
+ *
4
+ * Contains specialized JSON parser to handle non-standard JSON values from the JavaScript perspective. These values
5
+ * are legal in Python and scala, but not in JavaScript. This parser will safely handle these values.
6
+ *
7
+ * @author mstarch
8
+ */
9
+
10
+
11
+ /**
12
+ * Lexer for JSON built using JSON
13
+ */
14
+ class RegExLexer {
15
+ static TOKEN_EXPRESSIONS = new Map([
16
+ // String tokens: " then
17
+ // any number of:
18
+ // not a quote or \
19
+ // \ followed by not a quote
20
+ // even number of \
21
+ // odd number of \ then " (escaped quotation)
22
+ // then even number of \ then " (terminating non-escaped ")
23
+ ["STRING", /^"([^"\\]|(\\[^"\\])|((\\\\)*)|(\\(\\\\)*)")*(?!\\(\\\\)*)"/],
24
+ // Floating point tokens
25
+ ["NUMBER", /^-?\d+(\.\d+)?([eE][+\-]?\d+)?/],
26
+ // Infinity token
27
+ ["INFINITY", /^-?Infinity/],
28
+ // Null token
29
+ ["NULL", /^null/],
30
+ // NaN token
31
+ ["NAN", /^NaN/],
32
+ // boolean token
33
+ ["BOOLEAN", /^(true)|(false)/],
34
+ // Open object token
35
+ ["OPEN_OBJECT", /^\{/],
36
+ // Close object token
37
+ ["CLOSE_OBJECT", /^}/],
38
+ // Field separator token
39
+ ["FIELD_SEPARATOR", /^,/],
40
+ // Open list token
41
+ ["OPEN_ARRAY", /^\[/],
42
+ // Close a list token
43
+ ["CLOSE_ARRAY", /^]/],
44
+ // Key Value Separator
45
+ ["VALUE_SEPARATOR", /^:/],
46
+ // Any amount of whitespace is an implicit token
47
+ ["WHITESPACE", /^\s+/]
48
+ ]);
49
+
50
+ /**
51
+ * Tokenize the input string based on JSON tokens.
52
+ * @param input_string: input string to tokenize
53
+ * @return {*[]}: list of tokens in-order
54
+ */
55
+ static tokenize(original_string) {
56
+ let tokens = [];
57
+ let input_string = original_string;
58
+ // Consume the whole string
59
+ while (input_string !== "") {
60
+ let matched_something = false;
61
+ for (let [token_type, token_matcher] of RegExLexer.TOKEN_EXPRESSIONS.entries()) {
62
+ let match = token_matcher.exec(input_string)
63
+
64
+ // Token detected
65
+ if (match != null) {
66
+ matched_something = true;
67
+ let matched = match[0];
68
+ tokens.push([token_type, matched]);
69
+ // Consume the string
70
+ input_string = input_string.substring(matched.length);
71
+ break;
72
+ }
73
+ }
74
+ // Check for no token match
75
+ if (!matched_something) {
76
+ throw SyntaxError("Failed to match valid token: '" + input_string.substring(0, 20) + "'...");
77
+ }
78
+ }
79
+ return tokens;
80
+ }
81
+ }
82
+
83
+ /**
84
+ * Helper to determine if value is a string
85
+ * @param value: value to check.
86
+ * @return {boolean}: true if string, false otherwise
87
+ */
88
+ function isString(value) {
89
+ return value instanceof String || typeof value === 'string';
90
+ }
91
+
92
+ /**
93
+ * Helper to determine if value is a function
94
+ * @param value: value to check
95
+ * @return {boolean}: true if function, false otherwise
96
+ */
97
+ function isFunction(value) {
98
+ return value instanceof Function || typeof value == "function";
99
+ }
100
+
101
+ /**
102
+ * Convert a string to a number
103
+ * @param value: value to convert
104
+ * @return {bigint|number}: number to return
105
+ */
106
+ function stringToNumber(value) {
107
+ value = value.trim(); // Should be unnecessary
108
+ // Process floats (containing . e or E)
109
+ if (value.search(/[.eE]/) !== -1) {
110
+ return Number.parseFloat(value);
111
+ }
112
+ let number_value = Number.parseInt(value);
113
+ // When the big and normal numbers match, then return the normal number
114
+ if (value !== number_value.toString()) {
115
+ return BigInt(value);
116
+ }
117
+ return number_value;
118
+ }
119
+
120
+ /**
121
+ * Parser to safely handle potential JSON object from Python. Python can produce some non-standard values (infinities,
122
+ * NaNs, etc.) These values then break on the JS Javascript parser. To localize these faults, they are replaced before
123
+ * processing with strings and then formally set during parsing.
124
+ *
125
+ * This is done by looking for tokens in unquoted text and replacing them with string representations.
126
+ *
127
+ * This parser will handle:
128
+ * - -Infinity
129
+ * - Infinity
130
+ * - NaN
131
+ * - null
132
+ * - BigInt
133
+ */
134
+ export class SaferParser {
135
+ static CONVERSION_KEY = "fprime{replacement";
136
+
137
+ static CONVERSION_MAP = new Map([
138
+ ["INFINITY", (value) => (value[0] === "-") ? -Infinity : Infinity],
139
+ ["NAN", NaN],
140
+ ["NULL", null],
141
+ ["NUMBER", stringToNumber]
142
+ ]);
143
+
144
+ static STRINGIFY_TOKENS = [
145
+ Infinity,
146
+ -Infinity,
147
+ NaN,
148
+ "number",
149
+ "bigint",
150
+ null
151
+ ];
152
+
153
+
154
+ // Store the language variants the first time
155
+ static language_parse = JSON.parse;
156
+ static language_stringify = JSON.stringify;
157
+
158
+ /**
159
+ * @brief safely process F Prime JSON syntax
160
+ *
161
+ * Parse method that will replace JSON.parse. This method pre-processes the string data incoming (to be transformed
162
+ * into JavaScript objects) for detection of entities not expressible in JavaScript's JSON implementation. This will
163
+ * replace those entities with a JSON flag object.
164
+ *
165
+ * Then the data is processed by the JavaScript built-in JSON parser (now done safely). The reviver function will
166
+ * safely revive the flag objects into JavaScript representations of those object.
167
+ *
168
+ * Handles:
169
+ * 1. BigInts
170
+ * 2. Inf/-Inf
171
+ * 3. NaN
172
+ * 4. null
173
+ *
174
+ * @param json_string: JSON string data containing potentially bad values
175
+ * @param reviver: reviver function to be combined with our reviver
176
+ * @return {{}}: Javascript Object representation of data safely represented in JavaScript types
177
+ */
178
+ static parse(json_string, reviver) {
179
+ let converted_data = SaferParser.preprocess(json_string);
180
+ // Set up a composite reviver of the one passed in and ours
181
+ let input_reviver = reviver || ((key, value) => value);
182
+ let full_reviver = (key, value) => input_reviver(key, SaferParser.reviver(key, value));
183
+ try {
184
+ let language_parsed = SaferParser.language_parse(converted_data, full_reviver);
185
+ return language_parsed;
186
+ } catch (e) {
187
+ let message = e.toString();
188
+ const matcher = /line (\d+) column (\d+)/
189
+
190
+ // Process the match
191
+ let snippet = "";
192
+ let match = message.match(matcher);
193
+ if (match != null) {
194
+ let lines = converted_data.split("\n");
195
+ let line = lines[Number.parseInt(match[1]) - 1]
196
+ snippet = line.substring(Number.parseInt(match[2]) - 6, Number.parseInt(match[2]) + 5);
197
+ message += ". Offending snippet: " + snippet;
198
+ throw new SyntaxError(message);
199
+ }
200
+ throw e;
201
+ }
202
+ }
203
+
204
+ /**
205
+ * @brief safely write the F Prime JSON syntax
206
+ *
207
+ * Stringify method that will replace JSON.stringify. This method post-processes the string data outgoing from
208
+ * JavaScript's built-in stringify method to replace flag-objects with the correct F Prime representation in
209
+ * JavaScript.
210
+ *
211
+ * This uses the javascript stringify handler method to pre-convert unsupported types into a flag object. This flag
212
+ * object is post-converted into a normal string after JSON.stringify has done its best.
213
+ *
214
+ * Handles:
215
+ * 1. BigInts
216
+ * 2. Inf/-Inf
217
+ * 3. NaN
218
+ * 4. null
219
+ *
220
+ * @param data: data object to stringify
221
+ * @param replacer: replacer Array or Function
222
+ * @param space: space for passing into JSON.stringify
223
+ * @return {{}}: JSON string using JSON support for big-ints Int/-Inf, NaN and null.
224
+ */
225
+ static stringify(data, replacer, space) {
226
+ let full_replacer = (key, value) => {
227
+ // Handle array case for excluded field
228
+ if (Array.isArray(replacer) && replacer.indexOf(key) === -1) {
229
+ return undefined;
230
+ }
231
+ // Run input replacer first
232
+ else if (isFunction(replacer)) {
233
+ value = replacer(key, value);
234
+ }
235
+ // Then run our safe replacer
236
+ let replaced = SaferParser.replaceFromObject(key, value);
237
+ return replaced;
238
+ };
239
+ // Stringify JSON using built-in JSON parser and the special replacer
240
+ let json_string = SaferParser.language_stringify(data, full_replacer, space);
241
+ // Post-process JSON string to rework JSON into the wider specification
242
+ let post_replace = SaferParser.postReplacer(json_string);
243
+ return post_replace
244
+ }
245
+
246
+ /**
247
+ * Get replacement object from a JavaScript type
248
+ * @param _: unused
249
+ * @param value: value to replace
250
+ */
251
+ static replaceFromObject(_, value) {
252
+ for (let i = 0; i < SaferParser.STRINGIFY_TOKENS.length; i++) {
253
+ let replacer_type = SaferParser.STRINGIFY_TOKENS[i];
254
+ let mapper_is_string = isString(replacer_type);
255
+ if ((!mapper_is_string && value === replacer_type) || (mapper_is_string && typeof value === replacer_type)) {
256
+ let replace_object = {};
257
+ replace_object[SaferParser.CONVERSION_KEY] = (value == null) ? "null" : value.toString();
258
+ return replace_object;
259
+ }
260
+ }
261
+ return value;
262
+ }
263
+
264
+ /**
265
+ * Replace JSON notation for fprime-replacement objects with the wider JSON specification
266
+ *
267
+ * Replace {"fprime-replacement: "some value"} with <some value> restoring the full JSON specification for items not
268
+ * supported by JavaScript.
269
+ *
270
+ * @param json_string: JSON string to rework
271
+ * @return reworked JSON string
272
+ */
273
+ static postReplacer(json_string) {
274
+ return json_string.replace(/\{\s*"fprime\{replacement"\s*:\s*"([^"]+)"\s*}/sg, "$1");
275
+ }
276
+
277
+ /**
278
+ * Apply process function to raw json string only for data that is not qu
279
+ * @param json_string: JSON string to preprocess
280
+ * @return {string}
281
+ */
282
+ static preprocess(json_string) {
283
+ const CONVERSION_KEYS = Array.from(SaferParser.CONVERSION_MAP.keys());
284
+ let tokens = RegExLexer.tokenize(json_string);
285
+ let converted_text = tokens.map(
286
+ ([token_type, token_text]) => {
287
+ if (CONVERSION_KEYS.indexOf(token_type) !== -1) {
288
+ let replacement_object = {};
289
+ replacement_object[SaferParser.CONVERSION_KEY] = token_type;
290
+ replacement_object["value"] = token_text;
291
+ return SaferParser.language_stringify(replacement_object)
292
+ }
293
+ return token_text;
294
+ });
295
+ return converted_text.join("");
296
+ }
297
+
298
+ /**
299
+ * Inverse of convert removing string and replacing back invalid JSON tokens.
300
+ * @param key: JSON key
301
+ * @param value: JSON value search for the converted value.
302
+ * @return {*}: reverted value or value
303
+ */
304
+ static reviver(key, value) {
305
+ // Look for fprime-replacement and quickly abort if not there
306
+ let replacement_type = value[SaferParser.CONVERSION_KEY];
307
+ if (typeof replacement_type === "undefined") {
308
+ return value;
309
+ }
310
+ let string_value = value["value"];
311
+ let replacer = SaferParser.CONVERSION_MAP.get(replacement_type);
312
+ return isFunction(replacer) ? replacer(string_value) : replacer;
313
+ }
314
+
315
+ /**
316
+ * @brief force all calls to JSON.parse and JSON.stringify to use the SafeParser
317
+ */
318
+ static register() {
319
+ // Override the singleton
320
+ JSON.parse = SaferParser.parse;
321
+ JSON.stringify = SaferParser.stringify;
322
+ }
323
+
324
+ /**
325
+ * @brief remove the JSON.parse safe override
326
+ */
327
+ static deregister() {
328
+ JSON.parse = SaferParser.language_parse;
329
+ JSON.stringify = SaferParser.language_stringify;
330
+ }
331
+ }
332
+ // Take over all JSON.parse and JSON.stringify calls
333
+ SaferParser.register();
@@ -13,7 +13,8 @@
13
13
  */
14
14
  import {config} from "./config.js";
15
15
  import {_settings} from "./settings.js";
16
- import {_validator} from "./validate.js";
16
+ import {SaferParser} from "./json.js";
17
+ SaferParser.register();
17
18
 
18
19
  /**
19
20
  * Function allowing for the saving of some data to a downloadable file.
@@ -50,118 +51,6 @@ export function loadTextFileInputData(event) {
50
51
  });
51
52
  }
52
53
 
53
- /**
54
- * Parser to safely handle potential JSON object from Python. Python can produce some non-standard values (infinities,
55
- * NaNs, etc.) These values then break on the JS Javascript parser. To localize these faults, they are replaced before
56
- * processing with strings and then formally set during parsing.
57
- *
58
- * This is done by looking for tokens in unquoted text and replacing them with string representations.
59
- *
60
- */
61
- class SaferParser {
62
- /**
63
- * Set up the parser
64
- */
65
- constructor() {
66
- this.STATES = {
67
- UNQUOTED: 0,
68
- QUOTED: 1
69
- };
70
- this.FLAG = "-_-您好"; // Extended character usage make collisions less-likely
71
- this.MAPPINGS = [
72
- ["-Infinity", this.FLAG + "-inf", -Infinity],
73
- ["Infinity", this.FLAG + "inf", Infinity],
74
- ["NaN", this.FLAG + "nan", NaN],
75
- ["null", this.FLAG + "null", null]
76
- ];
77
- this.state = this.STATES.UNQUOTED;
78
- }
79
-
80
- /**
81
- * Parse method that will replace JSON.parse. This handles known bad cases and also prints better error messages
82
- * including the working snippets of text.
83
- * @param rawData: string data
84
- * @return {{}|any}: Javascript Object representation of data.
85
- */
86
- parse(rawData) {
87
- let converted_data = this.convert(rawData);
88
- try {
89
- return JSON.parse(converted_data, this.revert.bind(this));
90
- } catch (e) {
91
- let message = e.toString();
92
- const matcher = /line (\d+) column (\d+)/
93
-
94
- // Process the match
95
- let snippet = "";
96
- let match = message.match(matcher);
97
- if (match != null) {
98
- let lines = converted_data.split("\n");
99
- let line = lines[Number.parseInt(match[1]) - 1]
100
- snippet = line.substring(Number.parseInt(match[2]) - 6, Number.parseInt(match[2]) + 5);
101
- message += ". Offending snippet: " + snippet;
102
- }
103
- _validator.updateErrors([message]);
104
- }
105
- return {};
106
- }
107
-
108
- /**
109
- * Convert data from invalid form to strings.
110
- * @param rawData: raw data including potentially invalid data
111
- * @return {string}: string data in correct JSON format
112
- */
113
- convert(rawData) {
114
- let unprocessed = rawData;
115
- let transformed_data = "";
116
-
117
- while (unprocessed.length > 0) {
118
- let next_quote = unprocessed.indexOf("\"");
119
- let section = (next_quote !== -1) ? unprocessed.substring(0, next_quote + 1) : unprocessed.substring(0);
120
- unprocessed = unprocessed.substring(section.length);
121
- transformed_data += this.processChunk(section);
122
- this.state = (this.state === this.STATES.QUOTED) ? this.STATES.UNQUOTED : this.STATES.QUOTED;
123
- }
124
- return transformed_data;
125
- }
126
-
127
- /**
128
- * Inverse of convert removing string and replacing back invalid JSON tokens.
129
- * @param key: JSON key
130
- * @param value: JSON value search for the converted value.
131
- * @return {*}: reverted value or value
132
- */
133
- revert(key, value) {
134
- for (let i = 0; i < this.MAPPINGS.length; i++) {
135
- if ((this.MAPPINGS[i][1]) === value) {
136
- return this.MAPPINGS[i][2];
137
- }
138
- }
139
- return value;
140
- }
141
-
142
- /**
143
- * Process a section of the JSON string looking for values to convert. This is intended to handle a section of
144
- * quoted or unquoted text but should never handle quoted and unquoted data in one call.
145
- * @param section: section of the data
146
- * @return {*}: converted data
147
- */
148
- processChunk(section) {
149
- // Replaces all the above mappings with a flagged value
150
- let replace_all = (section) => {
151
- for (let i = 0; i < this.MAPPINGS.length; i++) {
152
- section = section.replace(this.MAPPINGS[i][0], "\"" + this.MAPPINGS[i][1] + "\"");
153
- }
154
- return section;
155
- }
156
-
157
- // When out of quoted space,
158
- if (this.state === this.STATES.UNQUOTED) {
159
- return replace_all(section);
160
- }
161
- return section;
162
- }
163
- }
164
-
165
54
  /**
166
55
  * Loader:
167
56
  *
@@ -308,7 +197,7 @@ class Loader {
308
197
  if (this.readyState === 4 && this.status === 200 && raw) {
309
198
  resolve(this.responseText);
310
199
  } else if (this.readyState === 4 && this.status === 200) {
311
- let dataObj = new SaferParser().parse(this.responseText);
200
+ let dataObj = JSON.parse(this.responseText);
312
201
  resolve(dataObj);
313
202
  } else if(this.readyState === 4) {
314
203
  reject(this.responseText);
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.2
2
2
  Name: fprime-gds
3
- Version: 3.5.2a1
3
+ Version: 3.6.0
4
4
  Summary: F Prime Flight Software Ground Data System layer
5
5
  Author-email: Michael Starch <Michael.D.Starch@jpl.nasa.gov>, Thomas Boyer-Chammard <Thomas.Boyer.Chammard@jpl.nasa.gov>
6
6
  License:
@@ -3,7 +3,7 @@ fprime_gds/__init__.py,sha256=y2ljhCEHnvyfSDvXIEgBGIk8oHjjjjCWFxfddOGeYFk,115
3
3
  fprime_gds/version.py,sha256=dlUlfOKTsGaqz_L7TjhCVC-Vanx5cK67kdZlqcHCM8M,395
4
4
  fprime_gds/common/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
5
  fprime_gds/common/handlers.py,sha256=t2-st-C3Z486kfcu2cpf-wHJQmpaaHQYj1dyXJEMmSU,2632
6
- fprime_gds/common/transport.py,sha256=y9HiupzsCRF5_JFMMtMWQxcEYPvPuxX1P_oBeqosKR0,10565
6
+ fprime_gds/common/transport.py,sha256=uYXWkM8TYEYz1vfY4AEn0PF8Gu4tkYmJ5t4w1YY1yW8,10565
7
7
  fprime_gds/common/zmq_transport.py,sha256=E_iBZ5sA4JKB99MWSOM6XnPrO-mbFyRvD9eQp9te6-Y,12397
8
8
  fprime_gds/common/communication/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
9
  fprime_gds/common/communication/checksum.py,sha256=f6W0Tr68U-XGnFmysMqsFzoGYZVE8clKf-VIJja_1YM,741
@@ -17,7 +17,7 @@ fprime_gds/common/communication/adapters/uart.py,sha256=6SrN42ShVjwNubFg-1YrO09o
17
17
  fprime_gds/common/controllers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
18
18
  fprime_gds/common/data_types/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
19
19
  fprime_gds/common/data_types/ch_data.py,sha256=RP9zSyzNcH0nJ3MYyW_IATnmnHYZ6d0KmoJUJantdBI,6111
20
- fprime_gds/common/data_types/cmd_data.py,sha256=nC6qhtBvqSgEueDn075XA_3dAwjnPHpLwhElhdMG4QQ,7080
20
+ fprime_gds/common/data_types/cmd_data.py,sha256=hsNJHCPKfqDRx4EzZ5DjobyD-oFlcXEWBS8NbDNfqgk,7090
21
21
  fprime_gds/common/data_types/event_data.py,sha256=7_vA6Xwvs9kK1-xJzc6lwO_TtUeWdI7p29B6QJNMc40,5372
22
22
  fprime_gds/common/data_types/exceptions.py,sha256=C16L2lofigH8UmnsYO_fuY6yR20U-ckRcl14HZjQlJc,1054
23
23
  fprime_gds/common/data_types/file_data.py,sha256=4_G9kf4ThC5NzkxnKa0xNYBdi8UDvZg8f5Vw0DdGIBE,3904
@@ -67,7 +67,7 @@ fprime_gds/common/loaders/dict_loader.py,sha256=TasuICjsRYPWAsgmHGmsioxa8F7xmgAj
67
67
  fprime_gds/common/loaders/event_json_loader.py,sha256=DPVJQ1wIY3r13rxTWrE9n7i6kSAF5m4jB-XRsxaRaDA,3572
68
68
  fprime_gds/common/loaders/event_py_loader.py,sha256=m4KlDl0mXn8ZQr-IfpUg0KaGIOJUErZkcIohlW9jNPc,2598
69
69
  fprime_gds/common/loaders/event_xml_loader.py,sha256=Q3Vm7ROTVgolSp5umkNMp0Eh95sir6ZAyAegrSjkiis,2875
70
- fprime_gds/common/loaders/json_loader.py,sha256=afD873WyIsbzWTz4SzydLfAKD2yFirj-l79xb4g_TBk,8051
70
+ fprime_gds/common/loaders/json_loader.py,sha256=nXdu3eDI7_FSVbNmjbldFjReBQTxMLg14BpZlSGIAeM,8750
71
71
  fprime_gds/common/loaders/pkt_xml_loader.py,sha256=ZS4qchqQnIBx0Tw69ehP8yqm1g_uYSQzmnijR3FxqJg,4795
72
72
  fprime_gds/common/loaders/python_loader.py,sha256=FUNQbFy75bpqvss1JDu2UWZBMrtnMpFegM6mcglh42I,4858
73
73
  fprime_gds/common/loaders/xml_loader.py,sha256=8AlTTHddJbJqUr6St-zJI8CTqoPuCNtNoRBmdwCorcg,14820
@@ -117,7 +117,7 @@ fprime_gds/executables/utils.py,sha256=SbzXRe1p41qMPdifvPap5_4v0T42gZZ_Rs_OYfITd
117
117
  fprime_gds/flask/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
118
118
  fprime_gds/flask/app.py,sha256=kJDCziri_BwZWKUszkR7u3RaNG_FWRzDkdCPsVDAtYM,6720
119
119
  fprime_gds/flask/channels.py,sha256=sOeL-UmWPh2hqYvqj81STpABLlPcjdPgkRwjd3Qx77k,735
120
- fprime_gds/flask/commands.py,sha256=pizE0AQ2Id5xAMBucdAf93ARVinnBQD8y5afAp2i5oo,3636
120
+ fprime_gds/flask/commands.py,sha256=62R3b0mnjc3_XpULpqJyUSvAcfOjAyZNifQ3wqHKO7s,3658
121
121
  fprime_gds/flask/components.py,sha256=a-eG8XJfSrqR8MIzIc9StwbNwxcBqkxYMEYq46S2Bmk,4176
122
122
  fprime_gds/flask/default_settings.py,sha256=SkNfd5R4tv59rcmPiHERIZNIEmzXP3KJcJZektgtZCA,603
123
123
  fprime_gds/flask/errors.py,sha256=yN3jDsJd30jL6aOIF-SqbVoesvReHqPvXlIt8qWB87M,2133
@@ -154,20 +154,20 @@ fprime_gds/flask/static/addons/chart-display/vendor/chartjs-adapter-luxon.min.js
154
154
  fprime_gds/flask/static/addons/chart-display/vendor/hammer.min.js,sha256=SKSRJkZzVMqQzhFeghSWUsCtUon42GUc4gDWk914lDw,20727
155
155
  fprime_gds/flask/static/addons/commanding/addon.js,sha256=d027BtN0LTSIWh1vtL1LIdf0k33tAw1kJ9GIDPbNY4o,263
156
156
  fprime_gds/flask/static/addons/commanding/argument-templates.js,sha256=vx5FP1ZCFT2w_Rc5fIIpaXnOoEq6vhy3tjIbOddwX3w,4528
157
- fprime_gds/flask/static/addons/commanding/arguments.js,sha256=_pcoHumf8qJhw-qfAzZ0_AU1SNAuXSRt55LHYG9t8p4,13304
157
+ fprime_gds/flask/static/addons/commanding/arguments.js,sha256=6UYPwa7KW5nso-pBPs9n9TsGbEq9FUDBtG3eFectThY,14274
158
158
  fprime_gds/flask/static/addons/commanding/command-history-template.js,sha256=2ak2B9eio3PLq6Bnie8iEsQN3HDJYokl0usMMP1D6lE,753
159
- fprime_gds/flask/static/addons/commanding/command-history.js,sha256=iVyMFP_GkqxiMAkK_U2JeTqo96Q-nTghO8eKo3NFXQ0,4816
159
+ fprime_gds/flask/static/addons/commanding/command-history.js,sha256=rEJBsYCFtCXCQewlIUjW2ATG0vPVjQywsbZQ9u8iN0g,3995
160
160
  fprime_gds/flask/static/addons/commanding/command-input-template.js,sha256=Z3fHmPTaAnXDhHMu07bRMBse6wjJSexAStgV9pSeh8Q,2959
161
- fprime_gds/flask/static/addons/commanding/command-input.js,sha256=utcmiLCkzU-CsBvBZsV32_UvKPVTuIOCafHsm3o2104,8973
162
- fprime_gds/flask/static/addons/commanding/command-string-template.js,sha256=7Mq4BPcAS57WoyF9aAMdKLtMFN4DLbQVTAQ8YUQEITI,743
163
- fprime_gds/flask/static/addons/commanding/command-string.js,sha256=q3ThUW45RHDE1gnKPxz44LOJgxXD24YTrYYlvBSfXrA,2553
161
+ fprime_gds/flask/static/addons/commanding/command-input.js,sha256=I2SZslctv7DyOmlmEhVsYRq6tPRAkWuNgikGDOmJn0o,9094
162
+ fprime_gds/flask/static/addons/commanding/command-string-template.js,sha256=tb0NJ24XtiEWaeY6e2MpNnb2uATRi7q7d2TrZEumvqw,825
163
+ fprime_gds/flask/static/addons/commanding/command-string.js,sha256=MAPG9Q0QyWTNxF1Urnxbz1AYC06p8mWPdThEZwpxFF8,2686
164
164
  fprime_gds/flask/static/addons/dictionary/addon-templates.js,sha256=19hYvx_Gf8B5j4po0YKeQrhS2UbUIBAEfqgbDGE2peE,950
165
165
  fprime_gds/flask/static/addons/dictionary/addon.js,sha256=eKggJNvOzj2ssXOenc7ccRUTRgeOJ5d4grWliaxgV40,1350
166
166
  fprime_gds/flask/static/addons/image-display/addon.js,sha256=Uo1JzqsqF4be9B3DAPH-LIm2MhqwUAhvTvobigpZB0Y,1522
167
167
  fprime_gds/flask/static/addons/image-display/dashboard.xml,sha256=t0g0v35YxV85MBhGSR_EkKBLRyglQ2Z7IQwfwvhuJAU,263
168
168
  fprime_gds/flask/static/addons/sequencer/README.md,sha256=-9DULIhPIZIWNSHZmenqrPA-G4OAxmclHukoaHlgIeM,1681
169
169
  fprime_gds/flask/static/addons/sequencer/addon-templates.js,sha256=jdZqBQBAKCwKsv60uWhF1Sd6A6WBeBcZ39yXAUlGpLE,3241
170
- fprime_gds/flask/static/addons/sequencer/addon.js,sha256=lH__umiGj836mEB4BupOBXyLPYRlKZjDq3lICfKJhCU,5116
170
+ fprime_gds/flask/static/addons/sequencer/addon.js,sha256=FX3gkVtA7TuvvjmHNRAZ1wAfJvQ5qk4wtY0o3jYUetc,5188
171
171
  fprime_gds/flask/static/addons/sequencer/autocomplete.js,sha256=O6qT8SjN7xuK0edaCpphENN_dLD8-ceRvE6lPNsZui8,6990
172
172
  fprime_gds/flask/static/addons/sequencer/lint.js,sha256=QPt9bt0SMZxMu6cXFkqILwgLlcTmcO0WbRQE8X2XIbQ,1472
173
173
  fprime_gds/flask/static/addons/sequencer/third/code-mirror.es.js,sha256=u-m1Nh7jnodMrnO1zR7eg0k7Lhwp8nt4nUwwJiKwyuU,813045
@@ -184,7 +184,8 @@ fprime_gds/flask/static/img/success.svg,sha256=wCfYG4cPfSCcsZ76JI4SwAJ-y62rahx9r
184
184
  fprime_gds/flask/static/js/config.js,sha256=3CNrVmUtUGeiomAuoAE22w34r7wA-X0OtXN3JtLZYJ8,1066
185
185
  fprime_gds/flask/static/js/datastore.js,sha256=mx0ZaUq-Nlb2LParYfnhu0WMUH-JGKKKgRQD_nOcZNE,15437
186
186
  fprime_gds/flask/static/js/gds.js,sha256=OeDJrNmNA8hUPi8QIHP-s33MW_IYT3QIccxzL75MsjA,1297
187
- fprime_gds/flask/static/js/loader.js,sha256=cpvd1P6jMaoVovGPR6iOHR5K6LR8Ga0F69qb6dwZ4cI,15521
187
+ fprime_gds/flask/static/js/json.js,sha256=KUiuQBWKsSNA5TjU57azEIAzCb5zNsge7JEiR8c3i5M,12022
188
+ fprime_gds/flask/static/js/loader.js,sha256=IbuJ7Jh70umyZog0iIupOuRGwc1tO5thT88jTW0XZc4,11414
188
189
  fprime_gds/flask/static/js/performance.js,sha256=fGBbK5anf5UB9iAr6rO4u9sCkrQGlOHHQLm1b3nvrvg,5359
189
190
  fprime_gds/flask/static/js/settings.js,sha256=Cfnn1ybZUOmsp48t_b23TDaHBRctvpD_h2ivBVKz6HM,1101
190
191
  fprime_gds/flask/static/js/uploader.js,sha256=HdFUGwJ-SN_OEMXiXXJw0YblC0j_QiOA1y9yVWyS4SI,2730
@@ -227,10 +228,10 @@ fprime_gds/flask/static/third-party/webfonts/fa-solid-900.woff2,sha256=mDS4KtJuK
227
228
  fprime_gds/plugin/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
228
229
  fprime_gds/plugin/definitions.py,sha256=5rHGSOrr62qRNVfX9bZIo4HDAKG62lKteNum9G40y3g,2347
229
230
  fprime_gds/plugin/system.py,sha256=uWd6DVW90Re0FoNMPNCx0cXXTJUdpgAAO0mtakzRNgk,8564
230
- fprime_gds-3.5.2a1.dist-info/LICENSE.txt,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
231
- fprime_gds-3.5.2a1.dist-info/METADATA,sha256=CpfCc4EoQcCptAW0sMgR25EgrSc897Y4ckCvg_K4ims,24772
232
- fprime_gds-3.5.2a1.dist-info/NOTICE.txt,sha256=vXjA_xRcQhd83Vfk5D_vXg5kOjnnXvLuMi5vFKDEVmg,1612
233
- fprime_gds-3.5.2a1.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
234
- fprime_gds-3.5.2a1.dist-info/entry_points.txt,sha256=oqUiO3xhJCR943jdU3zcxbqEvSXNeVgshk7dVaf_nGY,322
235
- fprime_gds-3.5.2a1.dist-info/top_level.txt,sha256=6vzFLIX6ANfavKaXFHDMSLFtS94a6FaAsIWhjgYuSNE,27
236
- fprime_gds-3.5.2a1.dist-info/RECORD,,
231
+ fprime_gds-3.6.0.dist-info/LICENSE.txt,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
232
+ fprime_gds-3.6.0.dist-info/METADATA,sha256=3nRj3Lb0NjIJFRlzOGBlf3G9khk_Kk1XM_Wp5VFLlGM,24770
233
+ fprime_gds-3.6.0.dist-info/NOTICE.txt,sha256=vXjA_xRcQhd83Vfk5D_vXg5kOjnnXvLuMi5vFKDEVmg,1612
234
+ fprime_gds-3.6.0.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
235
+ fprime_gds-3.6.0.dist-info/entry_points.txt,sha256=oqUiO3xhJCR943jdU3zcxbqEvSXNeVgshk7dVaf_nGY,322
236
+ fprime_gds-3.6.0.dist-info/top_level.txt,sha256=6vzFLIX6ANfavKaXFHDMSLFtS94a6FaAsIWhjgYuSNE,27
237
+ fprime_gds-3.6.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.6.0)
2
+ Generator: setuptools (75.8.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5