rccn-gen 1.2.0__py3-none-any.whl → 1.3.1__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.
rccn_gen/utils.py CHANGED
@@ -6,6 +6,22 @@ from yamcs.pymdb import IntegerArgument, FloatArgument, BooleanArgument, Enumera
6
6
 
7
7
 
8
8
  def to_upper_camel_case(s):
9
+ """
10
+ Convert a string to UpperCamelCase (PascalCase).
11
+
12
+ Converts the first letter to uppercase and capitalizes each word
13
+ after spaces or underscores, removing all non-alphanumeric characters.
14
+
15
+ Parameters:
16
+ -----------
17
+ s : str
18
+ The input string to convert.
19
+
20
+ Returns:
21
+ --------
22
+ str
23
+ The converted string in UpperCamelCase.
24
+ """
9
25
  if s[0].islower():
10
26
  s = s[0].upper() + s[1:]
11
27
  if ' ' in s or '_' in s:
@@ -14,12 +30,48 @@ def to_upper_camel_case(s):
14
30
  return s
15
31
 
16
32
  def to_snake_case(s):
33
+ """
34
+ Convert a string to snake_case.
35
+
36
+ Inserts underscores before uppercase letters, converts the string to lowercase,
37
+ replaces non-alphanumeric characters with underscores, and eliminates consecutive underscores.
38
+
39
+ Parameters:
40
+ -----------
41
+ s : str
42
+ The input string to convert.
43
+
44
+ Returns:
45
+ --------
46
+ str
47
+ The converted string in snake_case.
48
+ """
17
49
  s = re.sub(r'(?<!^)(?=[A-Z])', '_', s).lower()
18
50
  s = re.sub(r'[^a-zA-Z0-9_]', '_', s)
19
51
  s = re.sub(r'__', '_', s)
20
52
  return s
21
53
 
22
54
  def replace_with_indentation(text, keyword, replacement):
55
+ """
56
+ Replace a keyword in text while preserving the original indentation.
57
+
58
+ This function finds the indentation level of the line containing the keyword
59
+ and applies the same indentation to each line of the replacement text.
60
+
61
+ Parameters:
62
+ -----------
63
+ text : str
64
+ The original text containing the keyword.
65
+ keyword : str
66
+ The keyword to replace.
67
+ replacement : str
68
+ The replacement text.
69
+
70
+ Returns:
71
+ --------
72
+ str
73
+ The text with the keyword replaced by properly indented replacement text.
74
+ """
23
75
  lines = text.split('\n')
24
76
  indent = 0
25
77
  for line in lines:
@@ -29,6 +81,26 @@ def replace_with_indentation(text, keyword, replacement):
29
81
  return text.replace(keyword, replacement.replace('\n', ('\n'+(indent*' '))))
30
82
 
31
83
  def insert_before_with_indentation(text, keyword, replacement):
84
+ """
85
+ Insert text before a keyword while preserving the original indentation.
86
+
87
+ This function finds the indentation level of the line containing the keyword
88
+ and applies the same indentation to each line of the text to be inserted.
89
+
90
+ Parameters:
91
+ -----------
92
+ text : str
93
+ The original text containing the keyword.
94
+ keyword : str
95
+ The keyword before which to insert text.
96
+ replacement : str
97
+ The text to insert before the keyword.
98
+
99
+ Returns:
100
+ --------
101
+ str
102
+ The text with the replacement inserted before the keyword with proper indentation.
103
+ """
32
104
  lines = text.split('\n')
33
105
  indent = 0
34
106
  for line in lines:
@@ -37,34 +109,146 @@ def insert_before_with_indentation(text, keyword, replacement):
37
109
  return text.replace(keyword, (replacement.replace('\n', ('\n'+(indent*' ')))+keyword))
38
110
 
39
111
  def get_keywords(text):
112
+ """
113
+ Extract all keywords from a text.
114
+
115
+ Keywords are identified as text enclosed between double angle brackets.
116
+ For example: <<KEYWORD>>
117
+
118
+ Parameters:
119
+ -----------
120
+ text : str
121
+ The text to search for keywords.
122
+
123
+ Returns:
124
+ --------
125
+ list
126
+ A list of all keywords found in the text.
127
+ """
40
128
  pattern = r'<<.*?>>'
41
129
  return re.findall(pattern, text)
42
130
 
43
131
  def get_var_keywords(text):
132
+ """
133
+ Extract variable keywords from a text.
134
+
135
+ Variable keywords are identified as text starting with <<VAR_ and ending with >>.
136
+ For example: <<VAR_NAME>>
137
+
138
+ Parameters:
139
+ -----------
140
+ text : str
141
+ The text to search for variable keywords.
142
+
143
+ Returns:
144
+ --------
145
+ list
146
+ A list of all variable keywords found in the text.
147
+ """
44
148
  pattern = r'<<VAR_.*?>>'
45
149
  return re.findall(pattern, text)
46
150
 
47
151
  def get_service_module_keywords(text):
152
+ """
153
+ Extract service module keywords from a text.
154
+
155
+ Service module keywords are identified as text starting with <<SERVICE_MODULE_ and ending with >>.
156
+ For example: <<SERVICE_MODULE_NAME>>
157
+
158
+ Parameters:
159
+ -----------
160
+ text : str
161
+ The text to search for service module keywords.
162
+
163
+ Returns:
164
+ --------
165
+ list
166
+ A list of all service module keywords found in the text.
167
+ """
48
168
  pattern = r'<<SERVICE_MODULE_.*?>>'
49
169
  return re.findall(pattern, text)
50
170
 
51
171
  def get_command_module_keywords(text):
172
+ """
173
+ Extract command module keywords from a text.
174
+
175
+ Command module keywords are identified as text starting with <<COMMAND_MODULE_ and ending with >>.
176
+ For example: <<COMMAND_MODULE_NAME>>
177
+
178
+ Parameters:
179
+ -----------
180
+ text : str
181
+ The text to search for command module keywords.
182
+
183
+ Returns:
184
+ --------
185
+ list
186
+ A list of all command module keywords found in the text.
187
+ """
52
188
  pattern = r'<<COMMAND_MODULE_.*?>>'
53
189
  return re.findall(pattern, text)
54
190
 
55
191
  def delete_all_keywords(text):
56
- keywords = get_keywords(text)
57
- for keyword in keywords:
58
- text = text.replace(keyword, '')
59
- return text
192
+ """
193
+ Remove all keywords from a text.
194
+
195
+ Finds all text enclosed in double angle brackets and removes them.
196
+
197
+ Parameters:
198
+ -----------
199
+ text : str
200
+ The text containing keywords to be removed.
201
+
202
+ Returns:
203
+ --------
204
+ str
205
+ The text with all keywords removed.
206
+ """
207
+ keywords = get_keywords(text)
208
+ for keyword in keywords:
209
+ text = text.replace(keyword, '')
210
+ return text
60
211
 
61
212
  def delete_all_command_module_keywords(text):
62
- keywords = get_command_module_keywords(text)
63
- for keyword in keywords:
64
- text = text.replace(keyword, '')
65
- return text
213
+ """
214
+ Remove all command module keywords from a text.
215
+
216
+ Finds all text starting with <<COMMAND_MODULE_ and enclosed in double angle brackets,
217
+ then removes them.
218
+
219
+ Parameters:
220
+ -----------
221
+ text : str
222
+ The text containing command module keywords to be removed.
223
+
224
+ Returns:
225
+ --------
226
+ str
227
+ The text with all command module keywords removed.
228
+ """
229
+ keywords = get_command_module_keywords(text)
230
+ for keyword in keywords:
231
+ text = text.replace(keyword, '')
232
+ return text
66
233
 
67
234
  def arg_type_to_rust(arg, bit_number_str='32'):
235
+ """
236
+ Convert a pymdb argument type to its corresponding Rust type.
237
+
238
+ Maps different argument types from pymdb to their equivalent Rust data types.
239
+
240
+ Parameters:
241
+ -----------
242
+ arg : object
243
+ A pymdb argument object (IntegerArgument, FloatArgument, etc.)
244
+ bit_number_str : str, optional
245
+ Bit width for numeric types. Default is '32'.
246
+
247
+ Returns:
248
+ --------
249
+ str
250
+ The corresponding Rust type name, or None if the type is not supported.
251
+ """
68
252
  if isinstance(arg, IntegerArgument):
69
253
  if arg.signed:
70
254
  return 'i'+bit_number_str
@@ -83,6 +267,26 @@ def arg_type_to_rust(arg, bit_number_str='32'):
83
267
  return None
84
268
 
85
269
  def arg_enum_rust_definition(arg):
270
+ """
271
+ Generate Rust enum definition from an EnumeratedArgument.
272
+
273
+ Creates a Rust enum definition with all choices from the enumerated argument.
274
+
275
+ Parameters:
276
+ -----------
277
+ arg : EnumeratedArgument
278
+ The enumerated argument to convert to a Rust enum definition.
279
+
280
+ Returns:
281
+ --------
282
+ str
283
+ A string containing the complete Rust enum definition.
284
+
285
+ Raises:
286
+ -------
287
+ ValueError
288
+ If the provided argument is not an EnumeratedArgument.
289
+ """
86
290
  if not isinstance(arg, EnumeratedArgument):
87
291
  raise ValueError('Provided Argument is not of type EnumeratedArgument.')
88
292
  definition_text = 'pub enum '+arg.name+' {\n'
@@ -92,23 +296,74 @@ def arg_enum_rust_definition(arg):
92
296
  return definition_text
93
297
 
94
298
  def engineering_bit_number(raw_bit_number):
299
+ """
300
+ Calculate an appropriate engineering bit width based on a raw bit width.
301
+
302
+ Rounds up the raw bit width to the next power of 2, with a minimum of 8 bits.
303
+
304
+ Parameters:
305
+ -----------
306
+ raw_bit_number : int
307
+ The raw bit width (1-128).
308
+
309
+ Returns:
310
+ --------
311
+ int
312
+ The calculated engineering bit width (a power of 2, minimum 8).
313
+
314
+ Raises:
315
+ -------
316
+ ValueError
317
+ If raw_bit_number is not between 1 and 128.
318
+ """
95
319
  if raw_bit_number < 1 or raw_bit_number > 128:
96
320
  raise ValueError("raw_bit_number must be between 1 and 128")
97
321
  power = 1
98
322
  while 2**power < raw_bit_number:
99
323
  power += 1
100
324
  bit_number = 2**power
325
+ if bit_number < 8:
326
+ bit_number = 8
101
327
  return (bit_number)
102
328
 
103
329
  def get_data_type(parent_classes):
104
- """Extract the data type from a list of parent class names.
105
- Returns the class name that ends with 'DataType' or None if not found."""
330
+ """
331
+ Extract the data type from a list of parent class names.
332
+
333
+ Searches through a list of class names to find one ending with 'DataType'.
334
+
335
+ Parameters:
336
+ -----------
337
+ parent_classes : list
338
+ A list of class names to search through.
339
+
340
+ Returns:
341
+ --------
342
+ str or None
343
+ The name of the class ending with 'DataType', or None if none is found.
344
+ """
106
345
  for class_name in parent_classes:
107
346
  if class_name.endswith('DataType'):
108
347
  return class_name
109
348
  return None
110
349
 
111
350
  def get_base_type(parent_classes):
351
+ """
352
+ Determine the base type from a list of parent class names.
353
+
354
+ Checks if the parent classes include common base types like Argument, Member,
355
+ Parameter, or DataType.
356
+
357
+ Parameters:
358
+ -----------
359
+ parent_classes : list
360
+ A list of class names to search through.
361
+
362
+ Returns:
363
+ --------
364
+ str or None
365
+ The base type name if found, or None if none of the target base types are found.
366
+ """
112
367
  for base_type in ["Argument", "Member", "Parameter"]:
113
368
  if base_type in parent_classes:
114
369
  return base_type
@@ -117,6 +372,32 @@ def get_base_type(parent_classes):
117
372
  return None
118
373
 
119
374
  def rust_type_definition(pymdb_data_instance, parent_name="MyStruct"):
375
+ """
376
+ Generate Rust type definition code from a pymdb data instance.
377
+
378
+ This is the main function for converting pymdb data types to Rust code.
379
+ It analyzes the pymdb instance, determines its data type, and generates
380
+ appropriate Rust code including struct fields and necessary type definitions.
381
+
382
+ Parameters:
383
+ -----------
384
+ pymdb_data_instance : object
385
+ A pymdb data instance (Parameter, Argument, Member, etc.).
386
+ parent_name : str, optional
387
+ The name of the parent struct, used for inferring unnamed elements. Default is "MyStruct".
388
+
389
+ Returns:
390
+ --------
391
+ list
392
+ A list with two elements:
393
+ - [0]: The struct field definition including attributes
394
+ - [1]: Any supporting type definitions needed (like enums)
395
+
396
+ Raises:
397
+ -------
398
+ ValueError
399
+ If the data type cannot be determined or is not supported.
400
+ """
120
401
  parent_classes = list(map(lambda type: type.__name__, type(pymdb_data_instance).mro()))
121
402
  data_type = get_data_type(parent_classes)
122
403
  base_type = get_base_type(parent_classes)
@@ -136,6 +417,8 @@ def rust_type_definition(pymdb_data_instance, parent_name="MyStruct"):
136
417
  definition_text = ["",""]
137
418
  if pymdb_data_instance.short_description is not None:
138
419
  definition_text[0] += ("\n\t/// "+str(pymdb_data_instance.short_description)+"\n")
420
+
421
+ # Handle IntegerDataType
139
422
  if data_type == 'IntegerDataType':
140
423
  if pymdb_data_instance.encoding is None or pymdb_data_instance.encoding.bits is None:
141
424
  raw_bit_number = 8
@@ -151,12 +434,15 @@ def rust_type_definition(pymdb_data_instance, parent_name="MyStruct"):
151
434
  else:
152
435
  definition_text[0] += ("\tpub "+sc_instance_name+": u"+eng_bit_number_str+",\n")
153
436
 
437
+ # Handle BooleanDataType
154
438
  elif data_type == 'BooleanDataType':
155
439
  definition_text[0] += ("\t#[bits(1)]\n\tpub "+sc_instance_name+": bool,\n")
156
440
 
441
+ # Handle StringDataType
157
442
  elif data_type == 'StringDataType':
158
443
  definition_text[0] += "\t#[null_terminated]\n\tpub "+sc_instance_name+": String,\n"
159
444
 
445
+ # Handle ArrayDataType
160
446
  elif data_type == 'ArrayDataType':
161
447
  definition_text = rust_type_definition(pymdb_data_instance.data_type, parent_name=pymdb_data_instance.name)
162
448
  definition_text[0] = definition_text[0].replace(': ', ': Vec<').replace(',\n', '>,\n')
@@ -165,6 +451,7 @@ def rust_type_definition(pymdb_data_instance, parent_name="MyStruct"):
165
451
  if pymdb_data_instance.long_description is not None:
166
452
  definition_text[1] = "/// "+pymdb_data_instance.long_description+"\n" + definition_text[1]
167
453
 
454
+ # Handle EnumeratedDataType
168
455
  elif data_type == 'EnumeratedDataType':
169
456
  definition_text[0] += "\t#[bits("+str(pymdb_data_instance.encoding.bits)+")]\n"
170
457
  definition_text[0] += "\tpub "+pymdb_data_instance.name+": "+pascalcase(pymdb_data_instance.name)+",\n"
@@ -175,6 +462,7 @@ def rust_type_definition(pymdb_data_instance, parent_name="MyStruct"):
175
462
  if pymdb_data_instance.long_description is not None:
176
463
  definition_text[1] = "/// "+pymdb_data_instance.long_description+"\n" + definition_text[1]
177
464
 
465
+ # Handle AggregateDataType
178
466
  elif data_type == 'AggregateDataType':
179
467
  struct_name = pascalcase(pymdb_data_instance.name)
180
468
  definition_text[0] += "\tpub "+sc_instance_name+": "+struct_name+",\n"
@@ -190,6 +478,28 @@ def rust_type_definition(pymdb_data_instance, parent_name="MyStruct"):
190
478
  definition_text[1] += insert
191
479
  definition_text[1] += "}\n\n"
192
480
  definition_text[1] += append
481
+
482
+ # Handle FloatDataType
483
+ elif data_type == 'FloatDataType':
484
+ if pymdb_data_instance.encoding is None or pymdb_data_instance.encoding.bits is None:
485
+ raw_bit_number = 32
486
+ print("RCCN-Warning: No encoding for "+base_type+" "+pymdb_data_instance.name+" found. Using 32 as default for raw bit number.")
487
+ else:
488
+ raw_bit_number = pymdb_data_instance.encoding.bits
489
+ raw_bit_number_str = str(raw_bit_number)
490
+ if raw_bit_number == 32:
491
+ eng_bit_number = 32
492
+ elif raw_bit_number == 64:
493
+ eng_bit_number = 64
494
+ else:
495
+ print("RCCN-Warning: Given raw bit number for "+base_type+" \'"+pymdb_data_instance.name+"\' is not equal to 32 or 64. A engineering bit number of 64 will be used.")
496
+ eng_bit_number = 64
497
+ eng_bit_number_str = str(eng_bit_number)
498
+ definition_text[0] += "\t#[bits("+raw_bit_number_str+")]\n"
499
+ definition_text[0] += ("\tpub "+sc_instance_name+": f"+eng_bit_number_str+",\n")
500
+
501
+ # Handle unsupported data types
193
502
  else:
194
503
  definition_text = ["\t// Please implement datatype "+data_type+" here.\n", ""]
504
+
195
505
  return definition_text
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rccn_gen
3
- Version: 1.2.0
3
+ Version: 1.3.1
4
4
  Summary: A python based generator for RACCOON OS source files in Rust from yamcs-pymdb config files.
5
5
  Project-URL: Homepage, https://gitlab.com/rccn/pymdb_code_generation
6
6
  Project-URL: Issues, https://gitlab.com/rccn/pymdb_code_generation/issues
@@ -29,10 +29,11 @@ To see whats new, see the [CHANGELOG](CHANGELOG.md).
29
29
  ## Using the Generator
30
30
  The generator is based on [`pymdb`](https://github.com/yamcs/pymdb) and uses the same structure. Where `pymdb` gives the user freedom in defining systems, subsystems and their respective relations, the definitions for rust code generation is more restricted. For the rust generation, the user is bound to the following classes:
31
31
  - `Application`,
32
- - `Service`, and
33
- - `RCCNCommand`.
32
+ - `Service`,
33
+ - `RCCNCommand`, and
34
+ - `RCCNContainer`.
34
35
 
35
- All classes inherit from pymdb's Subsystem class and extend their functionality. This means that an existing pymdb definition can be used to generate rust code by renaming the respective instances. No functionality for pymdb's XTCE generation will be lost.
36
+ The Application and Service classes inherit from pymdb's Subsystem class and extend their functionality. The RCCNCommand extends the Command class and the RCCNContainer extends the Container class of pymdb. This means that an existing pymdb definition can be used to generate rust code by renaming the respective instances. No functionality for pymdb's XTCE generation will be lost.
36
37
 
37
38
  A root system may be defined for the satellite.
38
39
  ```python
@@ -44,7 +45,7 @@ An application can be defined with the following statement.
44
45
  ```python
45
46
  app = Application(system=root_system, name="ExampleApp", apid=42)
46
47
  ```
47
- It has the obligatory arguments **system**, **name**, **apid** and **export_directory**. After all Applications, Services and RCCNCommands are defined, the rust code generator can be called on the application with `app.generate_rccn_code(export_directory='.')`.
48
+ It has the obligatory arguments **system**, **name** and **apid**. After all Applications, Services and RCCNCommands are defined, the rust code generator can be called on the application with `app.generate_rccn_code(export_directory='.')`.
48
49
 
49
50
  ### Service
50
51
 
@@ -81,7 +82,7 @@ my_command = RCCNCommand(
81
82
  service.add_command(my_command)
82
83
  ```
83
84
 
84
- The only obligatory arguments are **name** and a **subtype assignment**, like shown in the example. The connection to a service can also be achieved with base commands, where every base command must be unique to a service. For example:
85
+ The only obligatory argument is **name**. If the subtype assignment is not given, a value will be chosen automatically. The connection to a service can also be achieved with base commands, where every base command must be unique to a service. For example:
85
86
 
86
87
  ```python
87
88
  base_cmd = RCCNCommand(
@@ -98,6 +99,34 @@ my_command = RCCNCommand(
98
99
  )
99
100
  ```
100
101
 
102
+ ### RCCNContainer
103
+ A container to hold telemetry information can be created with:
104
+ ```python
105
+ my_container = RCCNContainer(
106
+ system=service,
107
+ name='BatteryInformation',
108
+ short_description='This container holds information on battery voltage and current'
109
+ )
110
+ my_container.add_integer_parameter_entry(
111
+ name='BatteryNumber',
112
+ minimum=1,
113
+ maximum=4,
114
+ encoding=IntegerEncoding(bits=3),
115
+ short_description='Number of the battery'
116
+ )
117
+ my_container.add_float_parameter_entry(
118
+ name='Current',
119
+ units='Ampere',
120
+ encoding=FloatEncoding(bits=32),
121
+ short_description='Electric current of the battery.'
122
+ )
123
+ my_container.add_float_parameter_entry(
124
+ name='Voltage',
125
+ units='Volts',
126
+ encoding=FloatEncoding(bits=32),
127
+ short_description='Electric voltage of the battery.'
128
+ )
129
+ ```
101
130
  ## Output
102
131
  From the python configuration, the `main.rs`, `service.rs`, `command.rs`, `mod.rs`, `Cargo.toml` and `telemetry.rs` files are generated and are structured accordingly:
103
132
  - rccn_usr_example_app/
@@ -1,7 +1,7 @@
1
1
  rccn_gen/LICENSE,sha256=ixuiBLtpoK3iv89l7ylKkg9rs2GzF9ukPH7ynZYzK5s,35148
2
2
  rccn_gen/__init__.py,sha256=rBnqIw3uQk-uBbRh9VnungoTRSr2V0Bqos32xFZ44Eo,168
3
- rccn_gen/systems.py,sha256=vGsCyVkwrsFgYqHDL_bPPv8FuX3pIHYUwfIRB5YUWrY,37414
4
- rccn_gen/utils.py,sha256=VKnTC2hrgMyLdneksAifnqEgXO27zLQJ9x5igaF35rE,8269
3
+ rccn_gen/systems.py,sha256=NewVHJVITt3svMBkAigD23Wl3_-srjNBbiMsOqUstg4,83907
4
+ rccn_gen/utils.py,sha256=q5YSmyc3qADNYcycxQJBvrG6Df8CJelL4lhXF-dN_Ms,17016
5
5
  rccn_gen/text_modules/cargo_toml/cargo.txt,sha256=AYjSo3WJE7lhOcJaiNgXP9Y-DXHDIFIt6p42rDTVNVE,427
6
6
  rccn_gen/text_modules/command/command.txt,sha256=8Y-uJilhFLoinftIbn7uKfia9LLMZno2LkoDJ-4Y-9M,345
7
7
  rccn_gen/text_modules/command/command_module_enum.txt,sha256=35sBlAV_CzQw95Uf2dNynrYOxVD2tT2XWfEvS4Zx_KY,121
@@ -14,6 +14,6 @@ rccn_gen/text_modules/mod/mod.txt,sha256=BF8LablBE4ddutdl5m0prvpvLdBRejueVOujkyr
14
14
  rccn_gen/text_modules/service/command_module_match_cmd.txt,sha256=eVGo6ltuerG37rVxpXtL-JYuLyLW4c0i6NXb5g1_U-A,89
15
15
  rccn_gen/text_modules/service/service.txt,sha256=qTxoOD5i7wH4yFiDn13rOJW9hIZyACA8W3m6UABe22U,695
16
16
  rccn_gen/text_modules/telemetry/telemetry.txt,sha256=Re1d3BfpyXT_CEe7jJzLF3MARik0-J-K98K85iPOE40,193
17
- rccn_gen-1.2.0.dist-info/METADATA,sha256=h-4mJGYkmnAxOFD5Pn4ART7ml2VjQPrVdENO2eIVj7s,8967
18
- rccn_gen-1.2.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
19
- rccn_gen-1.2.0.dist-info/RECORD,,
17
+ rccn_gen-1.3.1.dist-info/METADATA,sha256=xYy5MKpPdYKRgTCLHeabF2vOG_laMSP1NidCw5FiAeM,9911
18
+ rccn_gen-1.3.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
19
+ rccn_gen-1.3.1.dist-info/RECORD,,