systemlink-cli 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.
Files changed (74) hide show
  1. slcli/__init__.py +1 -0
  2. slcli/__main__.py +23 -0
  3. slcli/_version.py +4 -0
  4. slcli/asset_click.py +1289 -0
  5. slcli/cli_formatters.py +218 -0
  6. slcli/cli_utils.py +504 -0
  7. slcli/comment_click.py +602 -0
  8. slcli/completion_click.py +418 -0
  9. slcli/config.py +81 -0
  10. slcli/config_click.py +498 -0
  11. slcli/dff_click.py +979 -0
  12. slcli/dff_decorators.py +24 -0
  13. slcli/example_click.py +404 -0
  14. slcli/example_loader.py +274 -0
  15. slcli/example_provisioner.py +2777 -0
  16. slcli/examples/README.md +134 -0
  17. slcli/examples/_schema/schema-v1.0.json +169 -0
  18. slcli/examples/demo-complete-workflow/README.md +323 -0
  19. slcli/examples/demo-complete-workflow/config.yaml +638 -0
  20. slcli/examples/demo-test-plans/README.md +132 -0
  21. slcli/examples/demo-test-plans/config.yaml +154 -0
  22. slcli/examples/exercise-5-1-parametric-insights/README.md +101 -0
  23. slcli/examples/exercise-5-1-parametric-insights/config.yaml +1589 -0
  24. slcli/examples/exercise-7-1-test-plans/README.md +93 -0
  25. slcli/examples/exercise-7-1-test-plans/config.yaml +323 -0
  26. slcli/examples/spec-compliance-notebooks/README.md +140 -0
  27. slcli/examples/spec-compliance-notebooks/config.yaml +112 -0
  28. slcli/examples/spec-compliance-notebooks/notebooks/SpecAnalysis_ComplianceCalculation.ipynb +1553 -0
  29. slcli/examples/spec-compliance-notebooks/notebooks/SpecComplianceCalculation.ipynb +1577 -0
  30. slcli/examples/spec-compliance-notebooks/notebooks/SpecfileExtractionAndIngestion.ipynb +912 -0
  31. slcli/examples/spec-compliance-notebooks/spec_template.xlsx +0 -0
  32. slcli/feed_click.py +892 -0
  33. slcli/file_click.py +932 -0
  34. slcli/function_click.py +1400 -0
  35. slcli/function_templates.py +85 -0
  36. slcli/main.py +406 -0
  37. slcli/mcp_click.py +269 -0
  38. slcli/mcp_server.py +748 -0
  39. slcli/notebook_click.py +1770 -0
  40. slcli/platform.py +345 -0
  41. slcli/policy_click.py +679 -0
  42. slcli/policy_utils.py +411 -0
  43. slcli/profiles.py +411 -0
  44. slcli/response_handlers.py +359 -0
  45. slcli/routine_click.py +763 -0
  46. slcli/skill_click.py +253 -0
  47. slcli/skills/slcli/SKILL.md +713 -0
  48. slcli/skills/slcli/references/analysis-recipes.md +474 -0
  49. slcli/skills/slcli/references/filtering.md +236 -0
  50. slcli/skills/systemlink-webapp/SKILL.md +744 -0
  51. slcli/skills/systemlink-webapp/references/deployment.md +123 -0
  52. slcli/skills/systemlink-webapp/references/nimble-angular.md +380 -0
  53. slcli/skills/systemlink-webapp/references/systemlink-services.md +192 -0
  54. slcli/ssl_trust.py +93 -0
  55. slcli/system_click.py +2216 -0
  56. slcli/table_utils.py +124 -0
  57. slcli/tag_click.py +794 -0
  58. slcli/templates_click.py +599 -0
  59. slcli/testmonitor_click.py +1667 -0
  60. slcli/universal_handlers.py +305 -0
  61. slcli/user_click.py +1218 -0
  62. slcli/utils.py +832 -0
  63. slcli/web_editor.py +295 -0
  64. slcli/webapp_click.py +981 -0
  65. slcli/workflow_preview.py +287 -0
  66. slcli/workflows_click.py +988 -0
  67. slcli/workitem_click.py +2258 -0
  68. slcli/workspace_click.py +576 -0
  69. slcli/workspace_utils.py +206 -0
  70. systemlink_cli-1.3.1.dist-info/METADATA +20 -0
  71. systemlink_cli-1.3.1.dist-info/RECORD +74 -0
  72. systemlink_cli-1.3.1.dist-info/WHEEL +4 -0
  73. systemlink_cli-1.3.1.dist-info/entry_points.txt +7 -0
  74. systemlink_cli-1.3.1.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,1553 @@
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "metadata": {},
6
+ "source": [
7
+ "### How to use this notebook\n",
8
+ "\n",
9
+ "1. Upload the specs to the product.\n",
10
+ "1. Upload the measurement data file to the product.\n",
11
+ "1. Run the BDC extraction notebook on the measurement data file.\n",
12
+ "1. Select one or more specs, click Analyze button in the tool bar, select this notebook and click Analyze.\n",
13
+ "\n",
14
+ "### About the notebook\n",
15
+ "\n",
16
+ "1. The notebook queries the parametric specs from the product.\n",
17
+ "1. For each of the parametric spec in the product, the valid measurement data is stored as an array.\n",
18
+ "1. Min, Max, Mean, Median, Standard deviation, Cp, Cpk and Health compliance is calculated for each spec.\n",
19
+ "1. Compliance selected specs will be updated as custom properties for respective specs and the compliance report is generated and uploaded to the product.\n",
20
+ "\n",
21
+ "### Rules for condition mapping\n",
22
+ "\n",
23
+ "1. If spec condition does not have unit, step condition has unit - it will not be considered for\n",
24
+ " mapping.\n",
25
+ "1. If spec condition has unit and step condition does not have unit - it will be considered for\n",
26
+ " mapping. Condition value will be assigned directly.\n",
27
+ "1. If spec condition does not have unit, step condition does not have unit - it will be considered\n",
28
+ " for mapping. Condition value will be assigned directly without any unit conversion.\n",
29
+ "1. If spec condition and step condition both have the same unit - it will be considered for mapping.\n",
30
+ " Condition value will be assigned directly.\n",
31
+ "1. If spec condition and step condition have different units with same base unit - it will be\n",
32
+ " considered for mapping. Condition value will be converted to spec condition unit and assigned.\n",
33
+ "1. If spec condition and step condition have different units with different base unit - it will not\n",
34
+ " be considered for mapping.\n",
35
+ "\n",
36
+ "### Rules for step condition within spec condition\n",
37
+ "\n",
38
+ "1. If the step condition value is not present - it can be considered as within range.\n",
39
+ "1. If the step condition value is present and within spec condition range and/or discrete array - it\n",
40
+ " can be considered as within range.\n",
41
+ "\n",
42
+ "### Rules for measurement data\n",
43
+ "\n",
44
+ "1. If spec and measurement data does not have unit - the measurement data will be considered for\n",
45
+ " compliance calculation.\n",
46
+ "1. If spec has unit and measurement data does not have unit or spec does not have unit and\n",
47
+ " measurement data has unit - it will not be considered for compliance calculation.\n",
48
+ "1. If spec and measurement data has different base unit - it will not be considered for compliance\n",
49
+ " calculation.\n",
50
+ "1. If spec and measurement data has unit - the measurement data will be converted from base unit to \n",
51
+ " the spec unit (Assuming measurement data will be stored in base unit. Ex:BDC extraction notebook\n",
52
+ " will store the measurement data in base unit).\n",
53
+ "1. If measurement data for a row is empty - it will not be considered for compliance calculation.\n",
54
+ "1. If measurement data for a row is not convertible to Decimal - it will not be considered for\n",
55
+ " compliance calculation.\n",
56
+ "\n",
57
+ "### Rules for spec health calculation\n",
58
+ "\n",
59
+ "1. If spec min value is null and spec max value is not null, spec health will be 'Pass' if max\n",
60
+ " compliance <= spec max, else spec health will be 'Fail'.\n",
61
+ "1. If spec max value is null and spec min value is not null, spec health will be 'Pass' if min\n",
62
+ " compliance >= spec min, else spec health will be 'Fail'.\n",
63
+ "1. If spec min and spec max value is null, spec health will be 'Pass'.\n",
64
+ "1. If both spec min and spec max value are not null, spec health will be 'Pass' when min and max\n",
65
+ " compliance is within spec min and spec max, else spec health will be 'Fail'.\n",
66
+ "\n",
67
+ "**Note: Any other cases not mentioned above may/may not work. The notebook is designed to work**\n",
68
+ "**only with the above mentioned cases**\n",
69
+ "\n",
70
+ "### Known cases where notebook does not work\n",
71
+ "1. If a spec has two or more conditions with the same name but different units, the compliance might\n",
72
+ " not be calculated properly, only one of these conditions will be displayed in the compliance\n",
73
+ " excel report and the condition data might not match with the actual spec condition. (Ex:\n",
74
+ " Spec01 has two condition entries vcc(V), Vcc(I))\n",
75
+ "\n"
76
+ ]
77
+ },
78
+ {
79
+ "cell_type": "code",
80
+ "execution_count": null,
81
+ "metadata": {},
82
+ "outputs": [],
83
+ "source": [
84
+ "import json\n",
85
+ "import os\n",
86
+ "import re\n",
87
+ "from decimal import Decimal, InvalidOperation\n",
88
+ "from http import HTTPStatus\n",
89
+ "from typing import Any, Callable, Dict, List, Tuple\n",
90
+ "\n",
91
+ "import aiohttp\n",
92
+ "import backoff\n",
93
+ "import pandas as pd\n",
94
+ "import requests\n",
95
+ "import scrapbook as sb\n",
96
+ "from ni_unit_converter import convert_from_base_unit, convert_to_base_unit\n",
97
+ "from requests import Response\n",
98
+ "from requests.exceptions import HTTPError\n",
99
+ "from requests.packages.urllib3.exceptions import InsecureRequestWarning\n",
100
+ "from systemlink.clients.nitestmonitor import StepsApi\n",
101
+ "from systemlink.clients.nitestmonitor.models import StepsAdvancedQuery\n",
102
+ "\n",
103
+ "requests.packages.urllib3.disable_warnings(InsecureRequestWarning)"
104
+ ]
105
+ },
106
+ {
107
+ "cell_type": "code",
108
+ "execution_count": null,
109
+ "metadata": {},
110
+ "outputs": [],
111
+ "source": [
112
+ "api_key = os.getenv(\"SYSTEMLINK_API_KEY\")\n",
113
+ "systemlink_uri = os.getenv(\"SYSTEMLINK_HTTP_URI\")"
114
+ ]
115
+ },
116
+ {
117
+ "cell_type": "markdown",
118
+ "metadata": {},
119
+ "source": [
120
+ "### Input Parameters\n"
121
+ ]
122
+ },
123
+ {
124
+ "cell_type": "code",
125
+ "execution_count": null,
126
+ "metadata": {
127
+ "papermill": {
128
+ "parameters": {
129
+ "file_ids": [],
130
+ "product_ids": []
131
+ }
132
+ },
133
+ "systemlink": {
134
+ "namespaces": [],
135
+ "parameters": [
136
+ {
137
+ "display_name": "spec_ids",
138
+ "id": "spec_ids",
139
+ "type": "string[]"
140
+ },
141
+ {
142
+ "display_name": "product_id",
143
+ "id": "product_id",
144
+ "type": "string"
145
+ }
146
+ ],
147
+ "version": 2
148
+ },
149
+ "tags": [
150
+ "parameters"
151
+ ]
152
+ },
153
+ "outputs": [],
154
+ "source": [
155
+ "spec_ids = [\"\"]\n",
156
+ "product_id = \"\"\n",
157
+ "excel_file_name = \"Spec_Compliance.xlsx\""
158
+ ]
159
+ },
160
+ {
161
+ "cell_type": "markdown",
162
+ "metadata": {},
163
+ "source": [
164
+ "### HTTP Status Codes and Constants"
165
+ ]
166
+ },
167
+ {
168
+ "cell_type": "code",
169
+ "execution_count": null,
170
+ "metadata": {},
171
+ "outputs": [],
172
+ "source": [
173
+ "MAX_HTTP_RETRIES = 6\n",
174
+ "TIMEOUT_IN_SECONDS = 60\n",
175
+ "VERIFY_SSL_CERTIFICATE = False\n",
176
+ "QUERY_SPECS_TAKE_COUNT = 1000\n",
177
+ "QUERY_STEPS_TAKE_COUNT = 1000\n",
178
+ "HTTP_RETRY_CODES = [\n",
179
+ " HTTPStatus.TOO_MANY_REQUESTS,\n",
180
+ " HTTPStatus.INTERNAL_SERVER_ERROR,\n",
181
+ " HTTPStatus.BAD_GATEWAY,\n",
182
+ " HTTPStatus.SERVICE_UNAVAILABLE,\n",
183
+ " HTTPStatus.GATEWAY_TIMEOUT\n",
184
+ "]"
185
+ ]
186
+ },
187
+ {
188
+ "cell_type": "markdown",
189
+ "metadata": {},
190
+ "source": [
191
+ "### API URL's"
192
+ ]
193
+ },
194
+ {
195
+ "cell_type": "code",
196
+ "execution_count": null,
197
+ "metadata": {},
198
+ "outputs": [],
199
+ "source": [
200
+ "class ApiUrls:\n",
201
+ " QUERY_PRODUCTS_URL = f\"{systemlink_uri}/nitestmonitor/v2/query-products\"\n",
202
+ " UPDATE_PRODUCT_URL = f\"{systemlink_uri}/nitestmonitor/v2/update-products\"\n",
203
+ " QUERY_SPECS_URL = f\"{systemlink_uri}/nispec/v1/query-specs\"\n",
204
+ " UPDATE_SPECS_URL = f\"{systemlink_uri}/nispec/v1/update-specs\"\n",
205
+ " QUERY_STEPS_URL = f\"{systemlink_uri}/nitestmonitor/v2/query-steps\"\n",
206
+ " GET_PRODUCT_URL = f\"{systemlink_uri}/nitestmonitor/v2/products\"\n",
207
+ " GET_FILE_PROPERTIES_URL = f\"{systemlink_uri}/nifile/v1/service-groups/Default/files\"\n",
208
+ " UPLOAD_FILE_URL = f\"{systemlink_uri}/nifile/v1/service-groups/Default/upload-files\""
209
+ ]
210
+ },
211
+ {
212
+ "cell_type": "markdown",
213
+ "metadata": {},
214
+ "source": [
215
+ "### API Utility functions\n"
216
+ ]
217
+ },
218
+ {
219
+ "cell_type": "code",
220
+ "execution_count": null,
221
+ "metadata": {},
222
+ "outputs": [],
223
+ "source": [
224
+ "@backoff.on_exception(\n",
225
+ " backoff.expo,\n",
226
+ " HTTPError,\n",
227
+ " max_tries=MAX_HTTP_RETRIES,\n",
228
+ " giveup=lambda e: e.response.status_code not in HTTP_RETRY_CODES,\n",
229
+ ")\n",
230
+ "def retry_request(callable_function: Callable) -> Response:\n",
231
+ " \"\"\"Run the callable function and raise for status\n",
232
+ "\n",
233
+ " Args:\n",
234
+ " callable_function (Callable): Callable request function\n",
235
+ "\n",
236
+ " Returns:\n",
237
+ " Response: Callable function response\n",
238
+ " \"\"\"\n",
239
+ " response = callable_function()\n",
240
+ " response.raise_for_status()\n",
241
+ "\n",
242
+ " return response\n",
243
+ "\n",
244
+ "\n",
245
+ "def create_get_request(url: str, headers: Dict = None) -> Response:\n",
246
+ " \"\"\"Get request\n",
247
+ "\n",
248
+ " Args:\n",
249
+ " url (str): API URL\n",
250
+ " headers (Dict, optional): API request headers. Defaults to None.\n",
251
+ "\n",
252
+ " Returns:\n",
253
+ " Response: Get request response\n",
254
+ " \"\"\"\n",
255
+ " if not headers:\n",
256
+ " headers = {}\n",
257
+ " default_headers = {\"x-ni-api-key\": api_key}\n",
258
+ " headers = {**default_headers, **headers}\n",
259
+ "\n",
260
+ " return retry_request(\n",
261
+ " lambda: requests.get(\n",
262
+ " url, headers=headers, verify=VERIFY_SSL_CERTIFICATE, timeout=TIMEOUT_IN_SECONDS\n",
263
+ " )\n",
264
+ " )\n",
265
+ "\n",
266
+ "\n",
267
+ "def create_post_request(url: str, body, headers: Dict = None) -> Response:\n",
268
+ " \"\"\"Post request\n",
269
+ "\n",
270
+ " Args:\n",
271
+ " url (str): API URL\n",
272
+ " body (_type_): Request body\n",
273
+ " headers (Dict, optional): API request headers. Defaults to None.\n",
274
+ "\n",
275
+ " Returns:\n",
276
+ " Response: Post request response\n",
277
+ " \"\"\"\n",
278
+ " if not headers:\n",
279
+ " headers = {}\n",
280
+ " default_headers = {\n",
281
+ " \"accept\": \"application/json\",\n",
282
+ " \"Content-Type\": \"application/json\",\n",
283
+ " \"x-ni-api-key\": api_key,\n",
284
+ " }\n",
285
+ " headers = {**default_headers, **headers}\n",
286
+ "\n",
287
+ " return retry_request(\n",
288
+ " lambda: requests.post(\n",
289
+ " url,\n",
290
+ " json=body,\n",
291
+ " headers=headers,\n",
292
+ " verify=VERIFY_SSL_CERTIFICATE,\n",
293
+ " timeout=TIMEOUT_IN_SECONDS,\n",
294
+ " )\n",
295
+ " )\n",
296
+ "\n",
297
+ "\n",
298
+ "def upload_file_request(url: str, file: Any, headers: Dict = None) -> Response:\n",
299
+ " \"\"\"Upload file post request\n",
300
+ "\n",
301
+ " Args:\n",
302
+ " url (str): API URL\n",
303
+ " file (Any): File content\n",
304
+ " headers (Dict, optional): API request headers. Defaults to None.\n",
305
+ "\n",
306
+ " Returns:\n",
307
+ " Response: Upload file request response\n",
308
+ " \"\"\"\n",
309
+ " if not headers:\n",
310
+ " headers = {}\n",
311
+ " default_headers = {\"accept\": \"application/json\", \"x-ni-api-key\": api_key}\n",
312
+ " headers = {**default_headers, **headers}\n",
313
+ "\n",
314
+ " return retry_request(\n",
315
+ " lambda: requests.post(\n",
316
+ " url, files=file, headers=headers, verify=False, timeout=TIMEOUT_IN_SECONDS\n",
317
+ " )\n",
318
+ " )"
319
+ ]
320
+ },
321
+ {
322
+ "cell_type": "markdown",
323
+ "metadata": {},
324
+ "source": [
325
+ "### Constants"
326
+ ]
327
+ },
328
+ {
329
+ "cell_type": "code",
330
+ "execution_count": null,
331
+ "metadata": {},
332
+ "outputs": [],
333
+ "source": [
334
+ "class ApiBody:\n",
335
+ " PRODUCT_IDS = \"productIds\"\n",
336
+ " TAKE = \"take\"\n",
337
+ " TYPE = \"type\"\n",
338
+ " PARAMETRIC = \"PARAMETRIC\"\n",
339
+ " SPECS = \"specs\"\n",
340
+ " CONTINUATION_TOKEN = \"continuationToken\"\n",
341
+ " JSON = \"JSON\"\n",
342
+ " RESPONSE_FORMAT = \"responseFormat\"\n",
343
+ " SPEC_ID = \"SpecID\"\n",
344
+ " STEP_ID = \"STEP_ID\"\n",
345
+ " FILTER = \"filter\"\n",
346
+ " RESULT_FILTER = \"resultFilter\"\n",
347
+ " ORDER_BY = \"orderBy\"\n",
348
+ " FILE_IDS = \"fileIds\"\n",
349
+ " PART_NUMBER = \"partNumber\"\n",
350
+ " DATA = \"data\"\n",
351
+ " PARAMETERS = \"parameters\"\n",
352
+ " ANY = \"Any\"\n",
353
+ " IT = \"it\"\n",
354
+ " PROJECTION = \"projection\"\n",
355
+ " PRODUCTS = \"products\"\n",
356
+ " REPLACE = \"replace\"\n",
357
+ " FALSE = \"false\"\n",
358
+ " WORKSPACE = \"workspace\"\n",
359
+ "\n",
360
+ "\n",
361
+ "class ApiResponse:\n",
362
+ " TYPE = \"type\"\n",
363
+ " ID = \"id\"\n",
364
+ " SPEC_ID = \"specId\"\n",
365
+ " SPECS = \"specs\"\n",
366
+ " STEPS = \"steps\"\n",
367
+ " PARAMETRIC = \"PARAMETRIC\"\n",
368
+ " PRODUCTS = \"products\"\n",
369
+ " AVAILABLE_FILES = \"availableFiles\"\n",
370
+ " WORKSPACE = \"workspace\"\n",
371
+ " FILE_IDS = \"fileIds\"\n",
372
+ " UPDATED_AT = \"updatedAt\"\n",
373
+ " URI = \"uri\"\n",
374
+ " EMPTY = \"Empty\"\n",
375
+ "\n",
376
+ "\n",
377
+ "class Projection:\n",
378
+ " INPUTS = \"INPUTS\"\n",
379
+ " DATA = \"DATA\"\n",
380
+ " STEP_ID = \"STEP_ID\"\n",
381
+ "\n",
382
+ "\n",
383
+ "class Spec:\n",
384
+ " SPECID = \"specId\"\n",
385
+ " CATEGORY = \"category\"\n",
386
+ " BLOCK = \"block\"\n",
387
+ " SYMBOL = \"symbol\"\n",
388
+ " LIMIT = \"limit\"\n",
389
+ " CONDITIONS = \"conditions\"\n",
390
+ " NAME = \"name\"\n",
391
+ " VALUE = \"value\"\n",
392
+ " RANGE = \"range\"\n",
393
+ " MIN = \"min\"\n",
394
+ " MAX = \"max\"\n",
395
+ " STEP = \"step\"\n",
396
+ " TYPICAL = \"typical\"\n",
397
+ " UNIT = \"unit\"\n",
398
+ " DISCRETE = \"discrete\"\n",
399
+ " PROPERTIES = \"properties\"\n",
400
+ "\n",
401
+ "\n",
402
+ "class Step:\n",
403
+ " INPUTS = \"inputs\"\n",
404
+ " NAME = \"name\"\n",
405
+ " VALUE = \"value\"\n",
406
+ " DATA = \"data\"\n",
407
+ " PARAMETERS = \"parameters\"\n",
408
+ " MEASUREMENT = \"measurement\"\n",
409
+ " SPEC_ID = \"SpecID\"\n",
410
+ " UNITS = \"units\"\n",
411
+ "\n",
412
+ "\n",
413
+ "class DataframeHeaders:\n",
414
+ " SPEC_DETAILS = \"Spec Details\"\n",
415
+ " SPEC_ID = \"SpecID\"\n",
416
+ " CATEGORY = \"Category\"\n",
417
+ " BLOCK = \"Block\"\n",
418
+ " SYMBOL = \"Symbol\"\n",
419
+ " NAME = \"Name\"\n",
420
+ " MIN = \"Min\"\n",
421
+ " TYPICAL = \"Typical\"\n",
422
+ " MAX = \"Max\"\n",
423
+ " UNIT = \"Unit\"\n",
424
+ " DISCRETE = \"Discrete\"\n",
425
+ " STEP = \"Step\"\n",
426
+ " CONDITION = \"Condition\"\n",
427
+ " COMPLIANCE = \"Compliance\"\n",
428
+ " CP = \"Cp\"\n",
429
+ " CPK = \"Cpk\"\n",
430
+ " STANDARD_DEVIATION = \"Standard Deviation\"\n",
431
+ "\n",
432
+ "\n",
433
+ "class ComplianceParameters:\n",
434
+ " MIN = \"Min\"\n",
435
+ " MAX = \"Max\"\n",
436
+ " MEAN = \"Mean\"\n",
437
+ " MEDIAN = \"Median\"\n",
438
+ " HEALTH = \"Health\"\n",
439
+ " CP = \"Cp\"\n",
440
+ " CPK = \"Cpk\"\n",
441
+ " STANDARD_DEVIATION = \"Standard Deviation\"\n",
442
+ "\n",
443
+ "\n",
444
+ "class SpecComplianceProperties:\n",
445
+ " MIN = \"Compliance Min\"\n",
446
+ " MAX = \"Compliance Max\"\n",
447
+ " MEAN = \"Compliance Mean\"\n",
448
+ " MEDIAN = \"Compliance Median\"\n",
449
+ " HEALTH = \"Compliance Health\"\n",
450
+ " CP = \"Compliance Cp\"\n",
451
+ " CPK = \"Compliance Cpk\"\n",
452
+ " STANDARD_DEVIATION = \"Compliance Standard Deviation\"\n",
453
+ "\n",
454
+ "\n",
455
+ "class SpecHealth:\n",
456
+ " PASS = \"Pass\"\n",
457
+ " FAIL = \"Fail\"\n",
458
+ " INF = \"inf\"\n",
459
+ " NEGATIVE_INF = \"-inf\"\n",
460
+ " NAN = \"nan\"\n",
461
+ "\n",
462
+ "MAX_SPECS_PER_UPDATE_REQUEST = 100"
463
+ ]
464
+ },
465
+ {
466
+ "cell_type": "markdown",
467
+ "metadata": {},
468
+ "source": [
469
+ "### Error Messages"
470
+ ]
471
+ },
472
+ {
473
+ "cell_type": "code",
474
+ "execution_count": null,
475
+ "metadata": {},
476
+ "outputs": [],
477
+ "source": [
478
+ "class ErrorMessages:\n",
479
+ " MISMATCHING_BASE_UNITS = \"Input and output units are having different base unit\""
480
+ ]
481
+ },
482
+ {
483
+ "cell_type": "markdown",
484
+ "metadata": {},
485
+ "source": [
486
+ "### Get Workspace ID of the file"
487
+ ]
488
+ },
489
+ {
490
+ "cell_type": "code",
491
+ "execution_count": null,
492
+ "metadata": {},
493
+ "outputs": [],
494
+ "source": [
495
+ "def get_workspace_id(file_id: str) -> str:\n",
496
+ " \"\"\"Retrieve the associated workspace ID for the given file ID\n",
497
+ "\n",
498
+ " Args:\n",
499
+ " FILE_ID (str): File ID\n",
500
+ "\n",
501
+ " Returns:\n",
502
+ " str: Workspace ID\n",
503
+ " \"\"\"\n",
504
+ " get_file_properties_url = f\"{ApiUrls.GET_FILE_PROPERTIES_URL}?id={file_id}\"\n",
505
+ " resp_json = create_get_request(get_file_properties_url)\n",
506
+ " resp = resp_json.json()\n",
507
+ " workspace_id = resp[ApiResponse.AVAILABLE_FILES][0][ApiResponse.WORKSPACE]\n",
508
+ "\n",
509
+ " return str(workspace_id)"
510
+ ]
511
+ },
512
+ {
513
+ "cell_type": "markdown",
514
+ "metadata": {},
515
+ "source": [
516
+ "### Get Product Information"
517
+ ]
518
+ },
519
+ {
520
+ "cell_type": "code",
521
+ "execution_count": null,
522
+ "metadata": {},
523
+ "outputs": [],
524
+ "source": [
525
+ "def get_product_information(product_id: str) -> Response:\n",
526
+ " \"\"\"Get the product response\n",
527
+ "\n",
528
+ " Args:\n",
529
+ " product_id (str): Product ID\n",
530
+ "\n",
531
+ " Returns:\n",
532
+ " Response: Product response\n",
533
+ " \"\"\"\n",
534
+ " headers = {\"accept\": \"application/json\", \"Content-Type\": \"application/json\"}\n",
535
+ " response = create_get_request(f\"{ApiUrls.GET_PRODUCT_URL}/{product_id}\", headers)\n",
536
+ "\n",
537
+ " return response"
538
+ ]
539
+ },
540
+ {
541
+ "cell_type": "code",
542
+ "execution_count": null,
543
+ "metadata": {},
544
+ "outputs": [],
545
+ "source": [
546
+ "product_information = get_product_information(product_id).json()\n",
547
+ "part_number = product_information[ApiBody.PART_NUMBER]"
548
+ ]
549
+ },
550
+ {
551
+ "cell_type": "markdown",
552
+ "metadata": {},
553
+ "source": [
554
+ "### Utility Functions"
555
+ ]
556
+ },
557
+ {
558
+ "cell_type": "code",
559
+ "execution_count": null,
560
+ "metadata": {},
561
+ "outputs": [],
562
+ "source": [
563
+ "def __batch_query_request(url: str, body: Dict[str, Any], response_key: str) -> List:\n",
564
+ " \"\"\"Query request in batches, accumulate and return the data\n",
565
+ "\n",
566
+ " Args:\n",
567
+ " url (str): URL for query request\n",
568
+ " body (Dict[str, Any]): Body of the query request\n",
569
+ " response_key (str): Key to retrieve data from the response\n",
570
+ "\n",
571
+ " Returns:\n",
572
+ " List: List of data retrieved from the response\n",
573
+ " \"\"\"\n",
574
+ " data = []\n",
575
+ "\n",
576
+ " response = create_post_request(url, body)\n",
577
+ " response = response.json()\n",
578
+ " if response is not None and response[response_key] is not None:\n",
579
+ " data.extend(response[response_key])\n",
580
+ " while response[ApiBody.CONTINUATION_TOKEN]:\n",
581
+ " body[ApiBody.CONTINUATION_TOKEN] = response[ApiBody.CONTINUATION_TOKEN]\n",
582
+ " response = create_post_request(url, body)\n",
583
+ " response = response.json()\n",
584
+ " if response is not None and response[response_key] is not None:\n",
585
+ " data.extend(response[response_key])\n",
586
+ "\n",
587
+ " return data\n",
588
+ "\n",
589
+ "\n",
590
+ "def __generate_spec_ids_filter() -> str:\n",
591
+ " filter = ''\n",
592
+ " for index, spec_id in enumerate(spec_ids):\n",
593
+ " filter += f'specId == \\\"{spec_id}\\\"'\n",
594
+ " if index < len(spec_ids) - 1:\n",
595
+ " filter += \" || \"\n",
596
+ " return filter\n",
597
+ " \n",
598
+ "def query_parametric_specs(product_id: str) -> List:\n",
599
+ " \"\"\"\n",
600
+ " Query Parametric specs in the product\n",
601
+ "\n",
602
+ " Args:\n",
603
+ " product_id (str): Product ID\n",
604
+ "\n",
605
+ " Returns:\n",
606
+ " List: Parametric specs in the product\n",
607
+ " \"\"\"\n",
608
+ " spec_ids_filter = __generate_spec_ids_filter()\n",
609
+ " body = {\n",
610
+ " ApiBody.PRODUCT_IDS: [product_id],\n",
611
+ " ApiBody.FILTER: f\"(({spec_ids_filter}) && ({ApiBody.TYPE} == \\\"{ApiBody.PARAMETRIC}\\\"))\",\n",
612
+ " ApiBody.TAKE: QUERY_SPECS_TAKE_COUNT\n",
613
+ " }\n",
614
+ " specs = __batch_query_request(ApiUrls.QUERY_SPECS_URL, body, ApiResponse.SPECS)\n",
615
+ "\n",
616
+ " return specs\n",
617
+ "\n",
618
+ "def update_specs(updated_specs: List) -> Response:\n",
619
+ " body = {\n",
620
+ " ApiBody.SPECS: updated_specs\n",
621
+ " }\n",
622
+ " return create_post_request(ApiUrls.UPDATE_SPECS_URL, body)\n",
623
+ "\n",
624
+ "async def __query_steps(request: StepsAdvancedQuery) -> aiohttp.ClientResponse:\n",
625
+ " \"\"\"Query steps\n",
626
+ "\n",
627
+ " Args:\n",
628
+ " request (StepsAdvancedQuery): Request body for query steps api\n",
629
+ "\n",
630
+ " Returns:\n",
631
+ " aiohttp.ClientResponse: Client response\n",
632
+ " \"\"\" \n",
633
+ " return await StepsApi().query_steps_v2(post_body=request, _preload_content=False)\n",
634
+ "\n",
635
+ "\n",
636
+ "async def __batch_query_steps(part_number: str, spec_id: str) -> List:\n",
637
+ " \"\"\"Batch query steps\n",
638
+ "\n",
639
+ " Args:\n",
640
+ " part_number (str): Part number\n",
641
+ " spec_id (str): Specification ID\n",
642
+ "\n",
643
+ " Raises:\n",
644
+ " Exception: Raises exception when there is a client response error\n",
645
+ "\n",
646
+ " Returns:\n",
647
+ " List: Steps which contains the measurement data for the given specification ID\n",
648
+ " \"\"\" \n",
649
+ " data = []\n",
650
+ " request = StepsAdvancedQuery(\n",
651
+ " filter=f'({ApiBody.DATA}.{ApiBody.PARAMETERS}.{ApiBody.ANY}({ApiBody.IT}[\"{ApiBody.SPEC_ID}\"] = \"{spec_id}\"))',\n",
652
+ " result_filter=f'({ApiBody.PART_NUMBER} == \"{part_number}\")',\n",
653
+ " take=QUERY_STEPS_TAKE_COUNT,\n",
654
+ " projection=[Projection.DATA, Projection.INPUTS],\n",
655
+ " response_format= ApiBody.JSON,\n",
656
+ " )\n",
657
+ " try:\n",
658
+ " response = await __query_steps(request)\n",
659
+ " response = await response.json()\n",
660
+ " if response is not None and response[ApiResponse.STEPS] is not None:\n",
661
+ " data.extend(response[ApiResponse.STEPS])\n",
662
+ " while response[ApiBody.CONTINUATION_TOKEN]:\n",
663
+ " request = StepsAdvancedQuery(\n",
664
+ " filter=f'({ApiBody.DATA}.{ApiBody.PARAMETERS}.{ApiBody.ANY}({ApiBody.IT}[\"{ApiBody.SPEC_ID}\"] = \"{spec_id}\"))',\n",
665
+ " result_filter=f'({ApiBody.PART_NUMBER} == \"{part_number}\")',\n",
666
+ " take=QUERY_STEPS_TAKE_COUNT,\n",
667
+ " projection=[Projection.DATA, Projection.INPUTS],\n",
668
+ " response_format= ApiBody.JSON,\n",
669
+ " continuation_token=response[ApiBody.CONTINUATION_TOKEN],\n",
670
+ " )\n",
671
+ " response = await __query_steps(request)\n",
672
+ " response = await response.json()\n",
673
+ " if response is not None and response[ApiResponse.STEPS] is not None:\n",
674
+ " data.extend(response[ApiResponse.STEPS])\n",
675
+ " except aiohttp.ClientResponseError:\n",
676
+ " raise Exception\n",
677
+ " return data\n",
678
+ "\n",
679
+ "\n",
680
+ "async def query_steps(part_number: str, spec_id: str) -> List:\n",
681
+ " \"\"\"\n",
682
+ " Query steps\n",
683
+ "\n",
684
+ " Args:\n",
685
+ " part_number (str): Part number\n",
686
+ " spec_id (str): Specification ID\n",
687
+ "\n",
688
+ " Returns:\n",
689
+ " List: Steps which contains the measurement data for the given specification ID\n",
690
+ " \"\"\"\n",
691
+ " steps = await __batch_query_steps(part_number, spec_id)\n",
692
+ "\n",
693
+ " return steps\n",
694
+ "\n",
695
+ "\n",
696
+ "def __convert_to_decimal(value: str) -> Decimal | None:\n",
697
+ " \"\"\"Converts the value to decimal\n",
698
+ "\n",
699
+ " Args:\n",
700
+ " value (str): value in string format\n",
701
+ "\n",
702
+ " Returns:\n",
703
+ " Decimal | None: returns the converted value\n",
704
+ " \"\"\"\n",
705
+ " try:\n",
706
+ " return Decimal(value)\n",
707
+ " except Exception:\n",
708
+ " return None\n",
709
+ "\n",
710
+ "\n",
711
+ "def __convert_to_bool(value: str) -> bool | None:\n",
712
+ " \"\"\"Converts the value to boolean\n",
713
+ "\n",
714
+ " Args:\n",
715
+ " value (str): value in string format\n",
716
+ "\n",
717
+ " Returns:\n",
718
+ " bool | None: returns the converted value\n",
719
+ " \"\"\"\n",
720
+ " if value in [\"True\", \"true\"]:\n",
721
+ " return True\n",
722
+ " if value in [\"False\", \"false\"]:\n",
723
+ " return False\n",
724
+ " return None\n",
725
+ "\n",
726
+ "\n",
727
+ "def __convert_to_datatype(value: str) -> bool | Decimal | str:\n",
728
+ " \"\"\"Convert the given string to any one of these types (decimal, bool)\n",
729
+ "\n",
730
+ " Args:\n",
731
+ " value (str): String value\n",
732
+ "\n",
733
+ " Returns:\n",
734
+ " bool | Decimal | str: returns the converted value\n",
735
+ " \"\"\"\n",
736
+ " bool_value = __convert_to_bool(value=value)\n",
737
+ " if bool_value is not None:\n",
738
+ " return bool_value\n",
739
+ "\n",
740
+ " decimal_value = __convert_to_decimal(value=value)\n",
741
+ " if decimal_value is not None:\n",
742
+ " return decimal_value\n",
743
+ "\n",
744
+ " return value\n",
745
+ "\n",
746
+ "\n",
747
+ "def get_spec_condition(spec: Dict) -> Dict:\n",
748
+ " \"\"\"\n",
749
+ " Get the specification conditions as a dictionary\n",
750
+ "\n",
751
+ " Args:\n",
752
+ " spec (Dict): Specification data\n",
753
+ "\n",
754
+ " Returns:\n",
755
+ " Dict: Specification Conditions as a dict\n",
756
+ " \"\"\"\n",
757
+ " spec_condition = {}\n",
758
+ "\n",
759
+ " for condition in spec[Spec.CONDITIONS]:\n",
760
+ " spec_condition[condition[Spec.NAME]] = condition[Spec.VALUE]\n",
761
+ "\n",
762
+ " return spec_condition\n",
763
+ "\n",
764
+ "\n",
765
+ "def extract_condition_name_and_unit(condition_string: str) -> Tuple[str, str | None]:\n",
766
+ " \"\"\"\n",
767
+ " Extract the condition name and unit seperately from the condition string\n",
768
+ "\n",
769
+ " Args:\n",
770
+ " condition_string (str): Condition string which contains the name and unit\n",
771
+ "\n",
772
+ " Returns:\n",
773
+ " Tuple[str, str | None]: Condition name, Condition Unit\n",
774
+ " \"\"\"\n",
775
+ " pattern = r'^(.*?)\\((.*?)\\)$'\n",
776
+ " match = re.match(pattern, condition_string)\n",
777
+ " if match:\n",
778
+ " condition_name = match.group(1).strip()\n",
779
+ " condition_unit = match.group(2).strip()\n",
780
+ "\n",
781
+ " return condition_name, condition_unit\n",
782
+ "\n",
783
+ " return condition_string.strip(), None\n",
784
+ "\n",
785
+ "\n",
786
+ "def is_different_base_unit(input_unit: str, output_unit: str) -> bool:\n",
787
+ " \"\"\"Check if both the units have different base unit\n",
788
+ "\n",
789
+ " Args:\n",
790
+ " input_unit (str): Input unit\n",
791
+ " output_unit (str): Output unit\n",
792
+ "\n",
793
+ " Returns:\n",
794
+ " bool: Returns true if both the units have different base unit, else returns false\n",
795
+ " \"\"\"\n",
796
+ " input_base_unit = convert_to_base_unit(input_unit, 1).base_unit\n",
797
+ " output_base_unit = convert_to_base_unit(output_unit, 1).base_unit\n",
798
+ "\n",
799
+ " if input_base_unit != output_base_unit:\n",
800
+ " return True\n",
801
+ "\n",
802
+ " return False\n",
803
+ "\n",
804
+ "\n",
805
+ "def convert_unit(input_unit: str, output_unit: str, value: str) -> Decimal:\n",
806
+ " \"\"\"\n",
807
+ " Converts the value from input unit to output unit\n",
808
+ "\n",
809
+ " Args:\n",
810
+ " input_unit (str): Input unit\n",
811
+ " output_unit (str): Ouput unit\n",
812
+ " value (str): Value to be converted\n",
813
+ "\n",
814
+ " Raises:\n",
815
+ " Exception: Input and output units are having different base unit\n",
816
+ "\n",
817
+ " Returns:\n",
818
+ " Decimal: Value converted from input unit to output unit\n",
819
+ " \"\"\"\n",
820
+ " if is_different_base_unit(input_unit, output_unit):\n",
821
+ " raise Exception(ErrorMessages.MISMATCHING_BASE_UNITS)\n",
822
+ " output_value = convert_to_base_unit(input_unit, value).converted_value\n",
823
+ " output_value = convert_from_base_unit(output_unit, output_value)\n",
824
+ "\n",
825
+ " return output_value\n",
826
+ "\n",
827
+ "\n",
828
+ "def get_step_condition_matching_the_spec_condition(step: Dict, spec_condition: Dict) -> Dict:\n",
829
+ " \"\"\"\n",
830
+ " Returns the step conditions matching the spec conditions as a dictionary\n",
831
+ "\n",
832
+ " Args:\n",
833
+ " step (Dict): Step data\n",
834
+ " spec_condition (Dict): Spec Conditions dictionary\n",
835
+ "\n",
836
+ " Returns:\n",
837
+ " Dict: Step conditions matching the spec conditions\n",
838
+ " \"\"\"\n",
839
+ " step_condition = {}\n",
840
+ "\n",
841
+ " if Step.INPUTS not in step:\n",
842
+ " return step_condition\n",
843
+ " for step_input in step[Step.INPUTS]:\n",
844
+ " step_condition_name, step_condition_unit = extract_condition_name_and_unit(\n",
845
+ " step_input[Step.NAME]\n",
846
+ " )\n",
847
+ "\n",
848
+ " if step_condition_name not in spec_condition:\n",
849
+ " continue\n",
850
+ "\n",
851
+ " try:\n",
852
+ " spec_condition_unit = spec_condition[step_condition_name][Spec.UNIT]\n",
853
+ " if spec_condition_unit is None and step_condition_unit:\n",
854
+ " continue\n",
855
+ " except KeyError:\n",
856
+ " spec_condition_unit = None\n",
857
+ " if step_condition_unit:\n",
858
+ " continue\n",
859
+ "\n",
860
+ " if (step_condition_unit == spec_condition_unit) or (\n",
861
+ " spec_condition_unit and not step_condition_unit\n",
862
+ " ):\n",
863
+ " try:\n",
864
+ " step_condition[step_condition_name] = (\n",
865
+ " Decimal(str(step_input[Step.VALUE])) if Step.VALUE in step_input else ''\n",
866
+ " )\n",
867
+ " except InvalidOperation:\n",
868
+ " step_condition[step_condition_name] = step_input[Step.VALUE]\n",
869
+ " else:\n",
870
+ " if is_different_base_unit(step_condition_unit, spec_condition_unit):\n",
871
+ " continue\n",
872
+ " if Step.VALUE not in step_input or (\n",
873
+ " Step.VALUE in step_input and step_input[Step.VALUE] == str()\n",
874
+ " ):\n",
875
+ " step_condition[step_condition_name] = str()\n",
876
+ " else:\n",
877
+ " try:\n",
878
+ " step_condition_value = convert_unit(\n",
879
+ " step_condition_unit, spec_condition_unit, str(step_input[Step.VALUE])\n",
880
+ " )\n",
881
+ " step_condition[step_condition_name] = step_condition_value\n",
882
+ " except (ValueError, InvalidOperation):\n",
883
+ " step_condition[step_condition_name] = step_input[Step.VALUE]\n",
884
+ "\n",
885
+ " return step_condition\n",
886
+ "\n",
887
+ "\n",
888
+ "def check_step_condition_within_spec_condition_range(\n",
889
+ " spec_condition: Dict,\n",
890
+ " condition_name: str,\n",
891
+ " condition_value: Decimal\n",
892
+ ") -> bool:\n",
893
+ " \"\"\"\n",
894
+ " Checks if the step condition value is within the spec condition range\n",
895
+ "\n",
896
+ " Args:\n",
897
+ " spec_condition (Dict): Specification condition\n",
898
+ " condition_name (str): Condition name\n",
899
+ " condition_value (Decimal): Condition value\n",
900
+ "\n",
901
+ " Returns:\n",
902
+ " bool: Returns true if step condition is within spec condition range , else returns false\n",
903
+ " \"\"\"\n",
904
+ " if Spec.RANGE in spec_condition[condition_name] and spec_condition[condition_name][Spec.RANGE]:\n",
905
+ " for spec_condition_range in spec_condition[condition_name][Spec.RANGE]:\n",
906
+ " min_limit = __convert_to_decimal(str(spec_condition_range[Spec.MIN]))\n",
907
+ " max_limit = __convert_to_decimal(str(spec_condition_range[Spec.MAX]))\n",
908
+ " if (min_limit is None or condition_value >= min_limit) and (\n",
909
+ " max_limit is None or condition_value <= max_limit\n",
910
+ " ):\n",
911
+ " return True\n",
912
+ "\n",
913
+ " return False\n",
914
+ "\n",
915
+ "\n",
916
+ "def check_step_condition_within_spec_condition_discrete_array(\n",
917
+ " spec_condition: Dict,\n",
918
+ " condition_name: str,\n",
919
+ " condition_value: Decimal | str\n",
920
+ ") -> bool:\n",
921
+ " \"\"\"\n",
922
+ " Check if the step condition value is within the spec condition discrete array\n",
923
+ "\n",
924
+ " Args:\n",
925
+ " spec_condition (Dict): Specification condition\n",
926
+ " condition_name (str): Condition name\n",
927
+ " condition_value (Decimal | str): Condition value\n",
928
+ "\n",
929
+ " Returns:\n",
930
+ " bool: Returns true if step condition value is within the spec condition discrete array else returns false\n",
931
+ " \"\"\"\n",
932
+ " if Spec.DISCRETE in spec_condition[condition_name]:\n",
933
+ " discrete_values = spec_condition[condition_name][Spec.DISCRETE]\n",
934
+ " converted_discrete_values = []\n",
935
+ " for discrete_value in discrete_values:\n",
936
+ " converted_discrete_values.append(__convert_to_datatype(str(discrete_value)))\n",
937
+ " if condition_value in converted_discrete_values:\n",
938
+ " return True\n",
939
+ "\n",
940
+ " return False\n",
941
+ "\n",
942
+ "\n",
943
+ "def check_step_condition_within_spec_condition(spec_condition: Dict, step_condition: Dict) -> bool:\n",
944
+ " \"\"\"\n",
945
+ " Check if the step condition value is within the spec condition\n",
946
+ "\n",
947
+ " Condition mapping rules:\n",
948
+ " 1. If condition value in measurement data is empty, it is considered as within bounds\n",
949
+ " 2. If condition value in measurement data is within bounds of either range or discrete array of spec condition, it is considered as within bounds\n",
950
+ "\n",
951
+ " Args:\n",
952
+ " spec_condition (Dict): Specification condition\n",
953
+ " step_condition (Dict): Step condition\n",
954
+ "\n",
955
+ " Returns:\n",
956
+ " bool: Returns true if step condition is within the spec condition, else returns false\n",
957
+ " \"\"\"\n",
958
+ " condition_within_range = True\n",
959
+ "\n",
960
+ " for condition_name, condition_value in step_condition.items():\n",
961
+ " try:\n",
962
+ " condition_value = Decimal(str(condition_value))\n",
963
+ " except InvalidOperation:\n",
964
+ " pass\n",
965
+ "\n",
966
+ " if not condition_value and isinstance(condition_value, str):\n",
967
+ " continue\n",
968
+ "\n",
969
+ " condition_within_range = (\n",
970
+ " (\n",
971
+ " check_step_condition_within_spec_condition_range(\n",
972
+ " spec_condition, condition_name, condition_value\n",
973
+ " )\n",
974
+ " or check_step_condition_within_spec_condition_discrete_array(\n",
975
+ " spec_condition, condition_name, condition_value\n",
976
+ " )\n",
977
+ " )\n",
978
+ " if isinstance(condition_value, Decimal)\n",
979
+ " else check_step_condition_within_spec_condition_discrete_array(\n",
980
+ " spec_condition, condition_name, condition_value\n",
981
+ " )\n",
982
+ " )\n",
983
+ "\n",
984
+ " if not condition_within_range:\n",
985
+ " return condition_within_range\n",
986
+ "\n",
987
+ " return condition_within_range\n",
988
+ "\n",
989
+ "\n",
990
+ "def fetch_measurement_data_for_spec(\n",
991
+ " steps: List,\n",
992
+ " spec_id: str,\n",
993
+ " spec_condition: Dict,\n",
994
+ " spec_unit: str | None\n",
995
+ ") -> List:\n",
996
+ " \"\"\"\n",
997
+ " Returns a list of measurement data associated with the spec after condition mapping\n",
998
+ "\n",
999
+ " Measurement data rules:\n",
1000
+ " 1. If Measurement value is empty, it is not considered for compliance\n",
1001
+ " 2. If Measurement value is not a valid number, it is not considered for compliance\n",
1002
+ "\n",
1003
+ " Args:\n",
1004
+ " steps (List): Steps data\n",
1005
+ " spec_id (str): Specification ID\n",
1006
+ " spec_condition (Dict): Specification condition\n",
1007
+ " spec_unit (str | None): Specification unit\n",
1008
+ "\n",
1009
+ " Returns:\n",
1010
+ " List: Measurement data\n",
1011
+ " \"\"\"\n",
1012
+ " measurement_data = []\n",
1013
+ "\n",
1014
+ " for step in steps:\n",
1015
+ " step_condition = get_step_condition_matching_the_spec_condition(step, spec_condition)\n",
1016
+ " if not check_step_condition_within_spec_condition(spec_condition, step_condition):\n",
1017
+ " continue\n",
1018
+ " if Step.DATA not in step or Step.PARAMETERS not in step[Step.DATA]:\n",
1019
+ " continue\n",
1020
+ " for parameter in step[Step.DATA][Step.PARAMETERS]:\n",
1021
+ " step_unit = parameter[Step.UNITS]\n",
1022
+ " if parameter[Step.UNITS] is str():\n",
1023
+ " step_unit = None\n",
1024
+ " if (spec_unit and not step_unit) or (step_unit and not spec_unit):\n",
1025
+ " continue\n",
1026
+ " if spec_unit and step_unit and is_different_base_unit(step_unit, spec_unit):\n",
1027
+ " continue\n",
1028
+ " if Step.SPEC_ID in parameter and parameter[Step.SPEC_ID] != spec_id:\n",
1029
+ " continue\n",
1030
+ " measurement_value = (\n",
1031
+ " parameter[Step.MEASUREMENT] if Step.MEASUREMENT in parameter else str()\n",
1032
+ " )\n",
1033
+ " if measurement_value:\n",
1034
+ " try:\n",
1035
+ " if not spec_unit:\n",
1036
+ " measurement_data.append(Decimal(str(measurement_value)))\n",
1037
+ " else:\n",
1038
+ " measurement_value = convert_from_base_unit(spec_unit, measurement_value)\n",
1039
+ " measurement_data.append(measurement_value)\n",
1040
+ " except (TypeError, ValueError, InvalidOperation):\n",
1041
+ " continue\n",
1042
+ "\n",
1043
+ " return measurement_data\n",
1044
+ "\n",
1045
+ "\n",
1046
+ "def calculate_compliance(measurement_data: pd.DataFrame, parameter: str) -> pd.Series:\n",
1047
+ " \"\"\"Calculate compliance for the measurement data\n",
1048
+ "\n",
1049
+ " Args:\n",
1050
+ " measurement_data (pd.DataFrame): Spec measurement data\n",
1051
+ " parameter (str): Compliance parameter\n",
1052
+ "\n",
1053
+ " Returns:\n",
1054
+ " pd.Series: Calculated compliance\n",
1055
+ " \"\"\"\n",
1056
+ " if parameter == ComplianceParameters.MIN:\n",
1057
+ " return measurement_data.min(skipna=True)\n",
1058
+ " if parameter == ComplianceParameters.MAX:\n",
1059
+ " return measurement_data.max(skipna=True)\n",
1060
+ " if parameter == ComplianceParameters.MEAN:\n",
1061
+ " return measurement_data.mean(skipna=True)\n",
1062
+ " if parameter == ComplianceParameters.MEDIAN:\n",
1063
+ " return measurement_data.median(skipna=True)\n",
1064
+ " return pd.Series()\n",
1065
+ "\n",
1066
+ "\n",
1067
+ "def get_spec_health(spec, min_compliance: str, max_compliance: str) -> str | None:\n",
1068
+ " \"\"\"Calculate spec health based on min and max compliance\n",
1069
+ "\n",
1070
+ " Args:\n",
1071
+ " spec (_type_): Specification data\n",
1072
+ " min_compliance (str): Min compliance value\n",
1073
+ " max_compliance (str): Max compliance value\n",
1074
+ "\n",
1075
+ " Returns:\n",
1076
+ " str | None: Returns Pass or Fail, None if there are any errors\n",
1077
+ " \"\"\"\n",
1078
+ " if spec[Spec.UNIT] is not None:\n",
1079
+ " spec_min = (\n",
1080
+ " convert_to_base_unit(spec[Spec.UNIT], spec[Spec.LIMIT][Spec.MIN]).converted_value\n",
1081
+ " if spec[Spec.LIMIT][Spec.MIN] is not None\n",
1082
+ " else Decimal(SpecHealth.NEGATIVE_INF)\n",
1083
+ " )\n",
1084
+ " spec_max = (\n",
1085
+ " convert_to_base_unit(spec[Spec.UNIT], spec[Spec.LIMIT][Spec.MAX]).converted_value\n",
1086
+ " if spec[Spec.LIMIT][Spec.MAX] is not None\n",
1087
+ " else Decimal(SpecHealth.INF)\n",
1088
+ " )\n",
1089
+ " min_compliance = convert_to_base_unit(spec[Spec.UNIT], min_compliance).converted_value\n",
1090
+ " max_compliance = convert_to_base_unit(spec[Spec.UNIT], max_compliance).converted_value\n",
1091
+ " else:\n",
1092
+ " spec_min = (\n",
1093
+ " Decimal(str(spec[Spec.LIMIT][Spec.MIN]))\n",
1094
+ " if spec[Spec.LIMIT][Spec.MIN] is not None\n",
1095
+ " else Decimal(SpecHealth.NEGATIVE_INF)\n",
1096
+ " )\n",
1097
+ " spec_max = (\n",
1098
+ " Decimal(str(spec[Spec.LIMIT][Spec.MAX]))\n",
1099
+ " if spec[Spec.LIMIT][Spec.MAX] is not None\n",
1100
+ " else Decimal(SpecHealth.INF)\n",
1101
+ " )\n",
1102
+ " min_compliance = Decimal(str(min_compliance))\n",
1103
+ " max_compliance = Decimal(str(max_compliance))\n",
1104
+ "\n",
1105
+ " try:\n",
1106
+ " if spec_min is Decimal(SpecHealth.NEGATIVE_INF) and spec_max is not None:\n",
1107
+ " return SpecHealth.PASS if max_compliance <= spec_max else SpecHealth.FAIL\n",
1108
+ " if spec_max is Decimal(SpecHealth.INF) and spec_min is not None:\n",
1109
+ " return SpecHealth.PASS if min_compliance <= spec_min else SpecHealth.FAIL\n",
1110
+ " if spec_min is Decimal(SpecHealth.NEGATIVE_INF) and spec_max is Decimal(SpecHealth.INF):\n",
1111
+ " return SpecHealth.PASS\n",
1112
+ " if spec_min <= min_compliance <= spec_max and spec_min <= max_compliance <= spec_max:\n",
1113
+ " return SpecHealth.PASS\n",
1114
+ " return SpecHealth.FAIL\n",
1115
+ " except InvalidOperation:\n",
1116
+ " return None\n",
1117
+ "\n",
1118
+ "\n",
1119
+ "def concatenate_dataframes(\n",
1120
+ " dfs: List[pd.DataFrame],\n",
1121
+ " reset_index: bool = True,\n",
1122
+ " axis: int = 1\n",
1123
+ ") -> pd.DataFrame:\n",
1124
+ " \"\"\"Concatenate multiple dataframes\n",
1125
+ "\n",
1126
+ " Args:\n",
1127
+ " dfs (List[pd.DataFrame]): List of dataframes\n",
1128
+ " reset_index (bool, optional): Reset index. Defaults to True.\n",
1129
+ " axis (int, optional): Axis to concatenate. Defaults to 1.\n",
1130
+ "\n",
1131
+ " Returns:\n",
1132
+ " pd.DataFrame: Returns the concatenated dataframe\n",
1133
+ " \"\"\"\n",
1134
+ " if reset_index:\n",
1135
+ " for df in dfs:\n",
1136
+ " df.reset_index(drop=True, inplace=True)\n",
1137
+ " concatenated_df = pd.concat(dfs, axis=axis)\n",
1138
+ "\n",
1139
+ " return concatenated_df\n",
1140
+ "\n",
1141
+ "\n",
1142
+ "def generate_spec_details_dataframe(spec: Dict) -> pd.DataFrame:\n",
1143
+ " \"\"\"Generate spec details dataframe\n",
1144
+ "\n",
1145
+ " Args:\n",
1146
+ " spec (Dict): Specification data\n",
1147
+ "\n",
1148
+ " Returns:\n",
1149
+ " pd.DataFrame: Spec detail dataframe\n",
1150
+ " \"\"\"\n",
1151
+ " spec_df = pd.DataFrame(\n",
1152
+ " {\n",
1153
+ " (DataframeHeaders.SPEC_DETAILS, DataframeHeaders.SPEC_ID): [spec[Spec.SPECID]],\n",
1154
+ " (DataframeHeaders.SPEC_DETAILS, DataframeHeaders.CATEGORY): [spec[Spec.CATEGORY]],\n",
1155
+ " (DataframeHeaders.SPEC_DETAILS, DataframeHeaders.BLOCK): [spec[Spec.BLOCK]],\n",
1156
+ " (DataframeHeaders.SPEC_DETAILS, DataframeHeaders.SYMBOL): [spec[Spec.SYMBOL]],\n",
1157
+ " (DataframeHeaders.SPEC_DETAILS, DataframeHeaders.NAME): [spec[Spec.NAME]],\n",
1158
+ " (DataframeHeaders.SPEC_DETAILS, DataframeHeaders.MIN): [spec[Spec.LIMIT][Spec.MIN]],\n",
1159
+ " (DataframeHeaders.SPEC_DETAILS, DataframeHeaders.TYPICAL): [\n",
1160
+ " spec[Spec.LIMIT][Spec.TYPICAL]\n",
1161
+ " ],\n",
1162
+ " (DataframeHeaders.SPEC_DETAILS, DataframeHeaders.MAX): [spec[Spec.LIMIT][Spec.MAX]],\n",
1163
+ " (DataframeHeaders.SPEC_DETAILS, DataframeHeaders.UNIT): [spec[Spec.UNIT]],\n",
1164
+ " }\n",
1165
+ " )\n",
1166
+ "\n",
1167
+ " return spec_df\n",
1168
+ "\n",
1169
+ "\n",
1170
+ "def generate_spec_conditions_dataframe(spec_condition: Dict) -> pd.DataFrame:\n",
1171
+ " \"\"\"Generate spec condition dataframe\n",
1172
+ "\n",
1173
+ " Args:\n",
1174
+ " spec_condition (Dict): Specification condition dictionary\n",
1175
+ "\n",
1176
+ " Returns:\n",
1177
+ " pd.DataFrame: Spec condition dataframe\n",
1178
+ " \"\"\"\n",
1179
+ " condition_df = pd.DataFrame()\n",
1180
+ "\n",
1181
+ " for condition_name, condition_data in spec_condition.items():\n",
1182
+ " condition_header = f\"{DataframeHeaders.CONDITION} ({condition_name})\"\n",
1183
+ " if Spec.RANGE in condition_data and len(condition_data[Spec.RANGE]) > 0:\n",
1184
+ " condition_data = {\n",
1185
+ " (condition_header, DataframeHeaders.MIN): [\n",
1186
+ " range[Spec.MIN] for range in condition_data[Spec.RANGE]\n",
1187
+ " ],\n",
1188
+ " (condition_header, DataframeHeaders.STEP): [\n",
1189
+ " range[Spec.STEP] for range in condition_data[Spec.RANGE]\n",
1190
+ " ],\n",
1191
+ " (condition_header, DataframeHeaders.MAX): [\n",
1192
+ " range[Spec.MAX] for range in condition_data[Spec.RANGE]\n",
1193
+ " ],\n",
1194
+ " (condition_header, DataframeHeaders.DISCRETE): [\n",
1195
+ " condition_data.get(Spec.DISCRETE)\n",
1196
+ " if len(condition_data.get(Spec.DISCRETE, [])) > 0\n",
1197
+ " else None\n",
1198
+ " for _ in condition_data[Spec.RANGE]\n",
1199
+ " ],\n",
1200
+ " (condition_header, DataframeHeaders.UNIT): [\n",
1201
+ " condition_data.get(Spec.UNIT, None) for _ in condition_data[Spec.RANGE]\n",
1202
+ " ],\n",
1203
+ " }\n",
1204
+ " else:\n",
1205
+ " condition_data = {\n",
1206
+ " (condition_header, DataframeHeaders.MIN): [None],\n",
1207
+ " (condition_header, DataframeHeaders.STEP): [None],\n",
1208
+ " (condition_header, DataframeHeaders.MAX): [None],\n",
1209
+ " (condition_header, DataframeHeaders.DISCRETE): [\n",
1210
+ " condition_data.get(Spec.DISCRETE)\n",
1211
+ " if len(condition_data.get(Spec.DISCRETE, [])) > 0\n",
1212
+ " else None\n",
1213
+ " ],\n",
1214
+ " (condition_header, DataframeHeaders.UNIT): [\n",
1215
+ " condition_data.get(Spec.UNIT, None)\n",
1216
+ " ],\n",
1217
+ " }\n",
1218
+ " spec_condition_df = pd.DataFrame(condition_data)\n",
1219
+ " condition_df = concatenate_dataframes([condition_df, spec_condition_df])\n",
1220
+ "\n",
1221
+ " return condition_df\n",
1222
+ "\n",
1223
+ "\n",
1224
+ "def generate_spec_compliance_dataframe(\n",
1225
+ " spec: Dict,\n",
1226
+ " rows: int,\n",
1227
+ " measurement_data: List\n",
1228
+ ") -> pd.DataFrame:\n",
1229
+ " \"\"\"Generate spec compliance dataframe\n",
1230
+ "\n",
1231
+ " Args:\n",
1232
+ " spec (Dict): Specification data\n",
1233
+ " rows (int): Number of rows to append compliance data\n",
1234
+ " measurement_data (List): Measurement data\n",
1235
+ "\n",
1236
+ " Returns:\n",
1237
+ " pd.DataFrame: Spec compliance dataframe\n",
1238
+ " \"\"\"\n",
1239
+ " min_compliance_heading = (DataframeHeaders.COMPLIANCE, ComplianceParameters.MIN)\n",
1240
+ " max_compliance_heading = (DataframeHeaders.COMPLIANCE, ComplianceParameters.MAX)\n",
1241
+ " mean_compliance_heading = (DataframeHeaders.COMPLIANCE, ComplianceParameters.MEAN)\n",
1242
+ " median_compliance_heading = (DataframeHeaders.COMPLIANCE, ComplianceParameters.MEDIAN)\n",
1243
+ " spec_health_heading = (DataframeHeaders.COMPLIANCE, ComplianceParameters.HEALTH)\n",
1244
+ " spec_cpk_heading = (DataframeHeaders.COMPLIANCE, ComplianceParameters.CPK)\n",
1245
+ " spec_ck_heading = (DataframeHeaders.COMPLIANCE, ComplianceParameters.CP)\n",
1246
+ " spec_std_heading = (DataframeHeaders.COMPLIANCE, ComplianceParameters.STANDARD_DEVIATION)\n",
1247
+ " min_rows = max(1, rows)\n",
1248
+ " if len(measurement_data) > 0:\n",
1249
+ " measurement_df = pd.DataFrame({spec[Spec.SPECID]: measurement_data})\n",
1250
+ " min_compliance = calculate_compliance(measurement_df, ComplianceParameters.MIN)\n",
1251
+ " max_compliance = calculate_compliance(measurement_df, ComplianceParameters.MAX)\n",
1252
+ " mean_compliance = calculate_compliance(measurement_df, ComplianceParameters.MEAN)\n",
1253
+ " median_compliance = calculate_compliance(measurement_df, ComplianceParameters.MEDIAN)\n",
1254
+ " spec_health = (\n",
1255
+ " get_spec_health(spec, str(min_compliance.iloc[0]), str(max_compliance.iloc[0]))\n",
1256
+ " if (str(min_compliance.iloc[0]) != SpecHealth.NAN and str(max_compliance.iloc[0])) != SpecHealth.NAN\n",
1257
+ " else str()\n",
1258
+ " )\n",
1259
+ "\n",
1260
+ " std = Decimal(measurement_df.astype('double').std(skipna=True).iloc[0])\n",
1261
+ "\n",
1262
+ " maxima = Decimal(max_compliance.iloc[0])\n",
1263
+ " minima = Decimal(min_compliance.iloc[0])\n",
1264
+ " mean = Decimal(mean_compliance.iloc[0])\n",
1265
+ "\n",
1266
+ " is_valid_max = bool(maxima)\n",
1267
+ " is_valid_min = bool(minima)\n",
1268
+ " cpk_high = (\n",
1269
+ " np.nan if not is_valid_max\n",
1270
+ " else __calculate_cpk_high(maxima, mean, std)\n",
1271
+ " )\n",
1272
+ " cpk_low = (\n",
1273
+ " np.nan if not is_valid_min \n",
1274
+ " else __calculate_cpk_low(minima, mean, std)\n",
1275
+ " )\n",
1276
+ " cpk = min(cpk_high, cpk_low)\n",
1277
+ " \n",
1278
+ " cp = (\n",
1279
+ " np.nan if not (is_valid_max and is_valid_min)\n",
1280
+ " else __calculate_cp(maxima, minima, std)\n",
1281
+ " )\n",
1282
+ "\n",
1283
+ " compliance_data = {\n",
1284
+ " min_compliance_heading: [min_compliance.iloc[0]] * min_rows,\n",
1285
+ " max_compliance_heading: [max_compliance.iloc[0]] * min_rows,\n",
1286
+ " mean_compliance_heading: [mean_compliance.iloc[0]] * min_rows,\n",
1287
+ " median_compliance_heading: [median_compliance.iloc[0]] * min_rows,\n",
1288
+ " spec_health_heading: [spec_health] * min_rows,\n",
1289
+ " spec_std_heading: [std] * min_rows,\n",
1290
+ " spec_cpk_heading: [cpk] * min_rows,\n",
1291
+ " spec_ck_heading: [cp] * min_rows\n",
1292
+ " }\n",
1293
+ " else:\n",
1294
+ " empty_compliance_data = [None] * min_rows\n",
1295
+ " compliance_data = {\n",
1296
+ " min_compliance_heading: empty_compliance_data,\n",
1297
+ " max_compliance_heading: empty_compliance_data,\n",
1298
+ " mean_compliance_heading: empty_compliance_data,\n",
1299
+ " median_compliance_heading: empty_compliance_data,\n",
1300
+ " spec_health_heading: empty_compliance_data,\n",
1301
+ " spec_std_heading: empty_compliance_data,\n",
1302
+ " spec_cpk_heading: empty_compliance_data,\n",
1303
+ " spec_ck_heading: empty_compliance_data\n",
1304
+ " }\n",
1305
+ " compliance_df = pd.DataFrame(compliance_data)\n",
1306
+ "\n",
1307
+ " return compliance_df\n",
1308
+ "\n",
1309
+ "def __calculate_cpk_high(high_limit, mean, std):\n",
1310
+ " diff = (high_limit) - (mean)\n",
1311
+ " if std == 0:\n",
1312
+ " return np.inf\n",
1313
+ " return diff / (3 * (std))\n",
1314
+ "\n",
1315
+ "def __calculate_cpk_low(low_limit, mean, std):\n",
1316
+ " diff = (mean) - (low_limit)\n",
1317
+ " if std == 0:\n",
1318
+ " return np.inf\n",
1319
+ " return diff / (3 * (std))\n",
1320
+ "\n",
1321
+ "def __calculate_cp(high_limit, low_limit, std):\n",
1322
+ " diff = (high_limit) - (low_limit)\n",
1323
+ " if std == 0:\n",
1324
+ " return np.inf\n",
1325
+ " return diff / (6 * (std))\n",
1326
+ "\n",
1327
+ "async def generate_spec_dataframe(specs: List, part_number: str) -> Dict:\n",
1328
+ " \"\"\"Generate a spec dataframe for each spec category\n",
1329
+ "\n",
1330
+ " Args:\n",
1331
+ " specs (List): Specifications data\n",
1332
+ " part_number (str): Part number of the product\n",
1333
+ "\n",
1334
+ " Returns:\n",
1335
+ " Dict: Dictionary of spec category and dataframe\n",
1336
+ " \"\"\"\n",
1337
+ " spec_data = {}\n",
1338
+ " spec_compliance_data = {}\n",
1339
+ "\n",
1340
+ " for spec in specs:\n",
1341
+ " category = spec.get(Spec.CATEGORY, ApiResponse.EMPTY)\n",
1342
+ " spec_id = spec[Spec.SPECID]\n",
1343
+ " spec_unit = spec[Spec.UNIT]\n",
1344
+ " spec_condition = get_spec_condition(spec)\n",
1345
+ " spec_details_df = generate_spec_details_dataframe(spec)\n",
1346
+ " spec_conditions_df = generate_spec_conditions_dataframe(spec_condition)\n",
1347
+ " steps = await query_steps(part_number, spec_id)\n",
1348
+ " measurement_data = fetch_measurement_data_for_spec(\n",
1349
+ " steps, spec_id, spec_condition, spec_unit\n",
1350
+ " )\n",
1351
+ " spec_compliance_df = generate_spec_compliance_dataframe(\n",
1352
+ " spec, len(spec_conditions_df), measurement_data\n",
1353
+ " )\n",
1354
+ " spec_df = concatenate_dataframes([spec_details_df, spec_conditions_df])\n",
1355
+ "\n",
1356
+ " if category not in spec_data:\n",
1357
+ " spec_data[category] = spec_df\n",
1358
+ " spec_compliance_data[category] = spec_compliance_df\n",
1359
+ " else:\n",
1360
+ " spec_data[category] = concatenate_dataframes([spec_data[category], spec_df], False, 0)\n",
1361
+ " spec_compliance_data[category] = concatenate_dataframes(\n",
1362
+ " [spec_compliance_data[category], spec_compliance_df], False, 0\n",
1363
+ " )\n",
1364
+ "\n",
1365
+ " for category in spec_data:\n",
1366
+ " spec_data[category] = concatenate_dataframes(\n",
1367
+ " [spec_data[category], spec_compliance_data[category]]\n",
1368
+ " )\n",
1369
+ "\n",
1370
+ " return spec_data\n",
1371
+ "\n",
1372
+ "def __get_batches(list, batch_size):\n",
1373
+ " for i in range(0, len(list), batch_size):\n",
1374
+ " yield list[i:i+batch_size]\n",
1375
+ "\n",
1376
+ "def __format_compliance(value: Decimal) -> Decimal:\n",
1377
+ " return '{:.4f}'.format(value)\n",
1378
+ "\n",
1379
+ "def __delete_spec_custom_property(spec, property_to_be_deleted) -> None:\n",
1380
+ " if property_to_be_deleted in spec[Spec.PROPERTIES]:\n",
1381
+ " del spec[Spec.PROPERTIES][property_to_be_deleted]\n",
1382
+ "\n",
1383
+ "def add_compliance_to_spec_custom_properties(specs: List, dataframe_dict: Dict) -> None:\n",
1384
+ " specs_to_be_updated = []\n",
1385
+ " for category, dataframe in dataframe_dict.items():\n",
1386
+ " specIds_df = dataframe[DataframeHeaders.SPEC_DETAILS][DataframeHeaders.SPEC_ID]\n",
1387
+ " for spec in specs:\n",
1388
+ " spec_compliance_metrices_df = dataframe[specIds_df == spec[Spec.SPECID]][DataframeHeaders.COMPLIANCE]\n",
1389
+ " \n",
1390
+ " if spec_compliance_metrices_df.empty or spec_compliance_metrices_df.iloc[0].empty or (not spec_compliance_metrices_df.iloc[0][ComplianceParameters.HEALTH]):\n",
1391
+ " __delete_spec_custom_property(spec, SpecComplianceProperties.MIN)\n",
1392
+ " __delete_spec_custom_property(spec, SpecComplianceProperties.MAX)\n",
1393
+ " __delete_spec_custom_property(spec, SpecComplianceProperties.MEAN)\n",
1394
+ " __delete_spec_custom_property(spec, SpecComplianceProperties.MEDIAN)\n",
1395
+ " __delete_spec_custom_property(spec, SpecComplianceProperties.HEALTH)\n",
1396
+ " __delete_spec_custom_property(spec, SpecComplianceProperties.STANDARD_DEVIATION)\n",
1397
+ " __delete_spec_custom_property(spec, SpecComplianceProperties.CPK)\n",
1398
+ " __delete_spec_custom_property(spec, SpecComplianceProperties.CP)\n",
1399
+ "\n",
1400
+ " else:\n",
1401
+ " spec[Spec.PROPERTIES][SpecComplianceProperties.MIN] = str(__format_compliance(spec_compliance_metrices_df.iloc[0][ComplianceParameters.MIN]))\n",
1402
+ " spec[Spec.PROPERTIES][SpecComplianceProperties.MAX] = str(__format_compliance(spec_compliance_metrices_df.iloc[0][ComplianceParameters.MAX]))\n",
1403
+ " spec[Spec.PROPERTIES][SpecComplianceProperties.MEAN] = str(__format_compliance(spec_compliance_metrices_df.iloc[0][ComplianceParameters.MEAN]))\n",
1404
+ " spec[Spec.PROPERTIES][SpecComplianceProperties.MEDIAN] = str(__format_compliance(spec_compliance_metrices_df.iloc[0][ComplianceParameters.MEDIAN]))\n",
1405
+ " spec[Spec.PROPERTIES][SpecComplianceProperties.HEALTH] = str(spec_compliance_metrices_df.iloc[0][ComplianceParameters.HEALTH])\n",
1406
+ " \n",
1407
+ " spec[Spec.PROPERTIES][SpecComplianceProperties.STANDARD_DEVIATION] = str(__format_compliance(spec_compliance_metrices_df.iloc[0][ComplianceParameters.STANDARD_DEVIATION]))\n",
1408
+ " spec[Spec.PROPERTIES][SpecComplianceProperties.CPK] = str(__format_compliance(spec_compliance_metrices_df.iloc[0][ComplianceParameters.CPK]))\n",
1409
+ " spec[Spec.PROPERTIES][SpecComplianceProperties.CP] = str(__format_compliance(spec_compliance_metrices_df.iloc[0][ComplianceParameters.CP]))\n",
1410
+ " \n",
1411
+ " specs_to_be_updated.append(spec)\n",
1412
+ "\n",
1413
+ " for specs_batch in __get_batches(specs_to_be_updated, MAX_SPECS_PER_UPDATE_REQUEST):\n",
1414
+ " update_specs_response = update_specs(specs_batch)\n",
1415
+ " if (update_specs_response.status_code != 200) or (\"failedSpecs\" in update_specs_response.json()):\n",
1416
+ " sb.glue(\"Error updating spec custom properties with the compliance metrices\", json.dumps(update_specs_response.json()))\n",
1417
+ " break\n",
1418
+ "\n",
1419
+ "def upload_file(file_name: str, workspace_id: str) -> Response:\n",
1420
+ " \"\"\"Upload file to SLE\n",
1421
+ "\n",
1422
+ " Args:\n",
1423
+ " file_name (str): File name\n",
1424
+ " part_number (str): Part number of the product\n",
1425
+ "\n",
1426
+ " Returns:\n",
1427
+ " Response: Upload file response\n",
1428
+ " \"\"\"\n",
1429
+ " upload_file_url = f\"{ApiUrls.UPLOAD_FILE_URL}?workspace={workspace_id}\"\n",
1430
+ " file = {\"file\": (f\"{part_number}-{file_name}\", open(file_name, \"rb\"))}\n",
1431
+ " response = upload_file_request(upload_file_url, file)\n",
1432
+ "\n",
1433
+ " return response\n",
1434
+ "\n",
1435
+ "\n",
1436
+ "def add_file_id_to_product(product_information: Any, file_id: str) -> Response:\n",
1437
+ " \"\"\"Add file to the product using update product API\n",
1438
+ "\n",
1439
+ " Args:\n",
1440
+ " product_information (Any): Product information received for get product API\n",
1441
+ " file_id (str): File ID\n",
1442
+ "\n",
1443
+ " Returns:\n",
1444
+ " Response: Update product response\n",
1445
+ " \"\"\"\n",
1446
+ " product_information[ApiResponse.FILE_IDS].append(file_id)\n",
1447
+ " product_information.pop(ApiResponse.UPDATED_AT)\n",
1448
+ " body = {ApiBody.PRODUCTS: [product_information], ApiBody.REPLACE: ApiBody.FALSE}\n",
1449
+ " response = create_post_request(ApiUrls.UPDATE_PRODUCT_URL, body)\n",
1450
+ "\n",
1451
+ " return response\n",
1452
+ "\n",
1453
+ "\n",
1454
+ "def write_dataframe_to_excel(dataframe_dict: Dict, file_name: str) -> None:\n",
1455
+ " \"\"\"Write each dataframe to a new sheet in excel\n",
1456
+ "\n",
1457
+ " Args:\n",
1458
+ " dataframe_dict (Dict): Dictionary of dataframes\n",
1459
+ " file_name (str): File name\n",
1460
+ " \"\"\"\n",
1461
+ " with pd.ExcelWriter(file_name, engine=\"openpyxl\") as writer:\n",
1462
+ " for category, dataframe in dataframe_dict.items():\n",
1463
+ " dataframe.reset_index(drop=True, inplace=True)\n",
1464
+ " dataframe.to_excel(writer, sheet_name=f\"Spec_{category}\", index=True)"
1465
+ ]
1466
+ },
1467
+ {
1468
+ "cell_type": "markdown",
1469
+ "metadata": {},
1470
+ "source": [
1471
+ "### Fetch parametric specs, measurement data, calculate Compliance and generate dataframe"
1472
+ ]
1473
+ },
1474
+ {
1475
+ "cell_type": "code",
1476
+ "execution_count": null,
1477
+ "metadata": {},
1478
+ "outputs": [],
1479
+ "source": [
1480
+ "parametric_specs = query_parametric_specs(product_id)\n",
1481
+ "spec_dataframe = await generate_spec_dataframe(parametric_specs, part_number)"
1482
+ ]
1483
+ },
1484
+ {
1485
+ "cell_type": "markdown",
1486
+ "metadata": {},
1487
+ "source": [
1488
+ "### Write compliance metrices to custom properties of specs"
1489
+ ]
1490
+ },
1491
+ {
1492
+ "cell_type": "code",
1493
+ "execution_count": null,
1494
+ "metadata": {},
1495
+ "outputs": [],
1496
+ "source": [
1497
+ "add_compliance_to_spec_custom_properties(parametric_specs, spec_dataframe)"
1498
+ ]
1499
+ },
1500
+ {
1501
+ "cell_type": "markdown",
1502
+ "metadata": {},
1503
+ "source": [
1504
+ "### Write dataframe to excel, upload and add the excel report to the product"
1505
+ ]
1506
+ },
1507
+ {
1508
+ "cell_type": "code",
1509
+ "execution_count": null,
1510
+ "metadata": {},
1511
+ "outputs": [],
1512
+ "source": [
1513
+ "pd.set_option(\"display.float_format\", \"{:.15f}\".format)\n",
1514
+ "write_dataframe_to_excel(spec_dataframe, excel_file_name)\n",
1515
+ "upload_file_response = upload_file(file_name=excel_file_name, workspace_id=parametric_specs[0]['workspace']).json()\n",
1516
+ "uploaded_file_id = upload_file_response[ApiResponse.URI].split(\"/\")[-1]\n",
1517
+ "product_response = add_file_id_to_product(\n",
1518
+ " product_information=product_information, file_id=uploaded_file_id\n",
1519
+ ").json()\n",
1520
+ "\n",
1521
+ "try:\n",
1522
+ " if product_response[ApiBody.PRODUCTS]:\n",
1523
+ " sb.glue(\n",
1524
+ " \"Compliance excel report generated and uploaded to the product\",\n",
1525
+ " f'<a href=\"../../testinsights/products/product/{product_id}/files\">Product Files tab</a>',\n",
1526
+ " )\n",
1527
+ "except KeyError:\n",
1528
+ " sb.glue(\"Error\", json.dumps(product_response))"
1529
+ ]
1530
+ }
1531
+ ],
1532
+ "metadata": {
1533
+ "kernelspec": {
1534
+ "display_name": "Python 3 (ipykernel)",
1535
+ "language": "python",
1536
+ "name": "python3"
1537
+ },
1538
+ "language_info": {
1539
+ "codemirror_mode": {
1540
+ "name": "ipython",
1541
+ "version": 3
1542
+ },
1543
+ "file_extension": ".py",
1544
+ "mimetype": "text/x-python",
1545
+ "name": "python",
1546
+ "nbconvert_exporter": "python",
1547
+ "pygments_lexer": "ipython3",
1548
+ "version": "3.11.6"
1549
+ }
1550
+ },
1551
+ "nbformat": 4,
1552
+ "nbformat_minor": 4
1553
+ }