sift-stack-py 0.3.2__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 (291) hide show
  1. google/__init__.py +1 -0
  2. google/api/__init__.py +0 -0
  3. google/api/annotations_pb2.py +27 -0
  4. google/api/annotations_pb2.pyi +29 -0
  5. google/api/annotations_pb2_grpc.py +4 -0
  6. google/api/annotations_pb2_grpc.pyi +30 -0
  7. google/api/field_behavior_pb2.py +30 -0
  8. google/api/field_behavior_pb2.pyi +175 -0
  9. google/api/field_behavior_pb2_grpc.py +4 -0
  10. google/api/field_behavior_pb2_grpc.pyi +30 -0
  11. google/api/http_pb2.py +31 -0
  12. google/api/http_pb2.pyi +433 -0
  13. google/api/http_pb2_grpc.py +4 -0
  14. google/api/http_pb2_grpc.pyi +30 -0
  15. protoc_gen_openapiv2/__init__.py +0 -0
  16. protoc_gen_openapiv2/options/__init__.py +0 -0
  17. protoc_gen_openapiv2/options/annotations_pb2.py +27 -0
  18. protoc_gen_openapiv2/options/annotations_pb2.pyi +48 -0
  19. protoc_gen_openapiv2/options/annotations_pb2_grpc.py +4 -0
  20. protoc_gen_openapiv2/options/annotations_pb2_grpc.pyi +17 -0
  21. protoc_gen_openapiv2/options/openapiv2_pb2.py +132 -0
  22. protoc_gen_openapiv2/options/openapiv2_pb2.pyi +1533 -0
  23. protoc_gen_openapiv2/options/openapiv2_pb2_grpc.py +4 -0
  24. protoc_gen_openapiv2/options/openapiv2_pb2_grpc.pyi +17 -0
  25. sift/__init__.py +0 -0
  26. sift/annotation_logs/__init__.py +0 -0
  27. sift/annotation_logs/v1/__init__.py +0 -0
  28. sift/annotation_logs/v1/annotation_logs_pb2.py +115 -0
  29. sift/annotation_logs/v1/annotation_logs_pb2.pyi +370 -0
  30. sift/annotation_logs/v1/annotation_logs_pb2_grpc.py +135 -0
  31. sift/annotation_logs/v1/annotation_logs_pb2_grpc.pyi +84 -0
  32. sift/annotations/__init__.py +0 -0
  33. sift/annotations/v1/__init__.py +0 -0
  34. sift/annotations/v1/annotations_pb2.py +180 -0
  35. sift/annotations/v1/annotations_pb2.pyi +539 -0
  36. sift/annotations/v1/annotations_pb2_grpc.py +237 -0
  37. sift/annotations/v1/annotations_pb2_grpc.pyi +144 -0
  38. sift/assets/__init__.py +0 -0
  39. sift/assets/v1/__init__.py +0 -0
  40. sift/assets/v1/assets_pb2.py +90 -0
  41. sift/assets/v1/assets_pb2.pyi +235 -0
  42. sift/assets/v1/assets_pb2_grpc.py +168 -0
  43. sift/assets/v1/assets_pb2_grpc.pyi +101 -0
  44. sift/calculated_channels/__init__.py +0 -0
  45. sift/calculated_channels/v1/__init__.py +0 -0
  46. sift/calculated_channels/v1/calculated_channels_pb2.py +99 -0
  47. sift/calculated_channels/v1/calculated_channels_pb2.pyi +280 -0
  48. sift/calculated_channels/v1/calculated_channels_pb2_grpc.py +101 -0
  49. sift/calculated_channels/v1/calculated_channels_pb2_grpc.pyi +64 -0
  50. sift/campaigns/__init__.py +0 -0
  51. sift/campaigns/v1/__init__.py +0 -0
  52. sift/campaigns/v1/campaigns_pb2.py +144 -0
  53. sift/campaigns/v1/campaigns_pb2.pyi +383 -0
  54. sift/campaigns/v1/campaigns_pb2_grpc.py +169 -0
  55. sift/campaigns/v1/campaigns_pb2_grpc.pyi +104 -0
  56. sift/channel_schemas/__init__.py +0 -0
  57. sift/channel_schemas/v1/__init__.py +0 -0
  58. sift/channel_schemas/v1/channel_schemas_pb2.py +69 -0
  59. sift/channel_schemas/v1/channel_schemas_pb2.pyi +117 -0
  60. sift/channel_schemas/v1/channel_schemas_pb2_grpc.py +101 -0
  61. sift/channel_schemas/v1/channel_schemas_pb2_grpc.pyi +64 -0
  62. sift/channels/__init__.py +0 -0
  63. sift/channels/v2/__init__.py +0 -0
  64. sift/channels/v2/channels_pb2.py +88 -0
  65. sift/channels/v2/channels_pb2.pyi +183 -0
  66. sift/channels/v2/channels_pb2_grpc.py +101 -0
  67. sift/channels/v2/channels_pb2_grpc.pyi +64 -0
  68. sift/common/__init__.py +0 -0
  69. sift/common/type/__init__.py +0 -0
  70. sift/common/type/v1/__init__.py +0 -0
  71. sift/common/type/v1/channel_bit_field_element_pb2.py +34 -0
  72. sift/common/type/v1/channel_bit_field_element_pb2.pyi +33 -0
  73. sift/common/type/v1/channel_bit_field_element_pb2_grpc.py +4 -0
  74. sift/common/type/v1/channel_bit_field_element_pb2_grpc.pyi +17 -0
  75. sift/common/type/v1/channel_data_type_pb2.py +29 -0
  76. sift/common/type/v1/channel_data_type_pb2.pyi +50 -0
  77. sift/common/type/v1/channel_data_type_pb2_grpc.py +4 -0
  78. sift/common/type/v1/channel_data_type_pb2_grpc.pyi +17 -0
  79. sift/common/type/v1/channel_enum_type_pb2.py +32 -0
  80. sift/common/type/v1/channel_enum_type_pb2.pyi +29 -0
  81. sift/common/type/v1/channel_enum_type_pb2_grpc.py +4 -0
  82. sift/common/type/v1/channel_enum_type_pb2_grpc.pyi +17 -0
  83. sift/common/type/v1/organization_pb2.py +27 -0
  84. sift/common/type/v1/organization_pb2.pyi +29 -0
  85. sift/common/type/v1/organization_pb2_grpc.py +4 -0
  86. sift/common/type/v1/organization_pb2_grpc.pyi +17 -0
  87. sift/common/type/v1/resource_identifier_pb2.py +46 -0
  88. sift/common/type/v1/resource_identifier_pb2.pyi +145 -0
  89. sift/common/type/v1/resource_identifier_pb2_grpc.py +4 -0
  90. sift/common/type/v1/resource_identifier_pb2_grpc.pyi +17 -0
  91. sift/common/type/v1/user_pb2.py +33 -0
  92. sift/common/type/v1/user_pb2.pyi +36 -0
  93. sift/common/type/v1/user_pb2_grpc.py +4 -0
  94. sift/common/type/v1/user_pb2_grpc.pyi +17 -0
  95. sift/data/__init__.py +0 -0
  96. sift/data/v1/__init__.py +0 -0
  97. sift/data/v1/data_pb2.py +212 -0
  98. sift/data/v1/data_pb2.pyi +745 -0
  99. sift/data/v1/data_pb2_grpc.py +67 -0
  100. sift/data/v1/data_pb2_grpc.pyi +44 -0
  101. sift/ingest/__init__.py +0 -0
  102. sift/ingest/v1/__init__.py +0 -0
  103. sift/ingest/v1/ingest_pb2.py +35 -0
  104. sift/ingest/v1/ingest_pb2.pyi +118 -0
  105. sift/ingest/v1/ingest_pb2_grpc.py +66 -0
  106. sift/ingest/v1/ingest_pb2_grpc.pyi +41 -0
  107. sift/ingestion_configs/__init__.py +0 -0
  108. sift/ingestion_configs/v1/__init__.py +0 -0
  109. sift/ingestion_configs/v1/ingestion_configs_pb2.py +115 -0
  110. sift/ingestion_configs/v1/ingestion_configs_pb2.pyi +332 -0
  111. sift/ingestion_configs/v1/ingestion_configs_pb2_grpc.py +203 -0
  112. sift/ingestion_configs/v1/ingestion_configs_pb2_grpc.pyi +124 -0
  113. sift/notifications/__init__.py +0 -0
  114. sift/notifications/v1/__init__.py +0 -0
  115. sift/notifications/v1/notifications_pb2.py +64 -0
  116. sift/notifications/v1/notifications_pb2.pyi +225 -0
  117. sift/notifications/v1/notifications_pb2_grpc.py +101 -0
  118. sift/notifications/v1/notifications_pb2_grpc.pyi +64 -0
  119. sift/ping/__init__.py +0 -0
  120. sift/ping/v1/__init__.py +0 -0
  121. sift/ping/v1/ping_pb2.py +38 -0
  122. sift/ping/v1/ping_pb2.pyi +36 -0
  123. sift/ping/v1/ping_pb2_grpc.py +66 -0
  124. sift/ping/v1/ping_pb2_grpc.pyi +41 -0
  125. sift/remote_files/__init__.py +0 -0
  126. sift/remote_files/v1/__init__.py +0 -0
  127. sift/remote_files/v1/remote_files_pb2.py +174 -0
  128. sift/remote_files/v1/remote_files_pb2.pyi +472 -0
  129. sift/remote_files/v1/remote_files_pb2_grpc.py +271 -0
  130. sift/remote_files/v1/remote_files_pb2_grpc.pyi +164 -0
  131. sift/report_templates/__init__.py +0 -0
  132. sift/report_templates/v1/__init__.py +0 -0
  133. sift/report_templates/v1/report_templates_pb2.py +146 -0
  134. sift/report_templates/v1/report_templates_pb2.pyi +381 -0
  135. sift/report_templates/v1/report_templates_pb2_grpc.py +169 -0
  136. sift/report_templates/v1/report_templates_pb2_grpc.pyi +104 -0
  137. sift/reports/__init__.py +0 -0
  138. sift/reports/v1/__init__.py +0 -0
  139. sift/reports/v1/reports_pb2.py +193 -0
  140. sift/reports/v1/reports_pb2.pyi +562 -0
  141. sift/reports/v1/reports_pb2_grpc.py +205 -0
  142. sift/reports/v1/reports_pb2_grpc.pyi +136 -0
  143. sift/rule_evaluation/__init__.py +0 -0
  144. sift/rule_evaluation/v1/__init__.py +0 -0
  145. sift/rule_evaluation/v1/rule_evaluation_pb2.py +89 -0
  146. sift/rule_evaluation/v1/rule_evaluation_pb2.pyi +263 -0
  147. sift/rule_evaluation/v1/rule_evaluation_pb2_grpc.py +101 -0
  148. sift/rule_evaluation/v1/rule_evaluation_pb2_grpc.pyi +64 -0
  149. sift/rules/__init__.py +0 -0
  150. sift/rules/v1/__init__.py +0 -0
  151. sift/rules/v1/rules_pb2.py +420 -0
  152. sift/rules/v1/rules_pb2.pyi +1355 -0
  153. sift/rules/v1/rules_pb2_grpc.py +577 -0
  154. sift/rules/v1/rules_pb2_grpc.pyi +351 -0
  155. sift/runs/__init__.py +0 -0
  156. sift/runs/v2/__init__.py +0 -0
  157. sift/runs/v2/runs_pb2.py +150 -0
  158. sift/runs/v2/runs_pb2.pyi +413 -0
  159. sift/runs/v2/runs_pb2_grpc.py +271 -0
  160. sift/runs/v2/runs_pb2_grpc.pyi +164 -0
  161. sift/saved_searches/__init__.py +0 -0
  162. sift/saved_searches/v1/__init__.py +0 -0
  163. sift/saved_searches/v1/saved_searches_pb2.py +144 -0
  164. sift/saved_searches/v1/saved_searches_pb2.pyi +385 -0
  165. sift/saved_searches/v1/saved_searches_pb2_grpc.py +237 -0
  166. sift/saved_searches/v1/saved_searches_pb2_grpc.pyi +144 -0
  167. sift/tags/__init__.py +0 -0
  168. sift/tags/v1/__init__.py +0 -0
  169. sift/tags/v1/tags_pb2.py +49 -0
  170. sift/tags/v1/tags_pb2.pyi +71 -0
  171. sift/tags/v1/tags_pb2_grpc.py +4 -0
  172. sift/tags/v1/tags_pb2_grpc.pyi +17 -0
  173. sift/users/__init__.py +0 -0
  174. sift/users/v2/__init__.py +0 -0
  175. sift/users/v2/users_pb2.py +61 -0
  176. sift/users/v2/users_pb2.pyi +142 -0
  177. sift/users/v2/users_pb2_grpc.py +135 -0
  178. sift/users/v2/users_pb2_grpc.pyi +84 -0
  179. sift/views/__init__.py +0 -0
  180. sift/views/v1/__init__.py +0 -0
  181. sift/views/v1/views_pb2.py +130 -0
  182. sift/views/v1/views_pb2.pyi +466 -0
  183. sift/views/v1/views_pb2_grpc.py +305 -0
  184. sift/views/v1/views_pb2_grpc.pyi +184 -0
  185. sift_grafana/py.typed +0 -0
  186. sift_grafana/sift_query_model.py +64 -0
  187. sift_py/__init__.py +923 -0
  188. sift_py/_internal/__init__.py +5 -0
  189. sift_py/_internal/cel.py +18 -0
  190. sift_py/_internal/channel.py +42 -0
  191. sift_py/_internal/convert/__init__.py +3 -0
  192. sift_py/_internal/convert/json.py +24 -0
  193. sift_py/_internal/convert/protobuf.py +34 -0
  194. sift_py/_internal/convert/timestamp.py +9 -0
  195. sift_py/_internal/test_util/__init__.py +0 -0
  196. sift_py/_internal/test_util/channel.py +136 -0
  197. sift_py/_internal/test_util/fn.py +14 -0
  198. sift_py/_internal/test_util/server_interceptor.py +62 -0
  199. sift_py/_internal/time.py +48 -0
  200. sift_py/_internal/user.py +39 -0
  201. sift_py/data/__init__.py +171 -0
  202. sift_py/data/_channel.py +38 -0
  203. sift_py/data/_deserialize.py +208 -0
  204. sift_py/data/_deserialize_test.py +134 -0
  205. sift_py/data/_service_test.py +276 -0
  206. sift_py/data/_validate.py +10 -0
  207. sift_py/data/error.py +5 -0
  208. sift_py/data/query.py +299 -0
  209. sift_py/data/service.py +497 -0
  210. sift_py/data_import/__init__.py +130 -0
  211. sift_py/data_import/_config.py +167 -0
  212. sift_py/data_import/_config_test.py +166 -0
  213. sift_py/data_import/_csv_test.py +395 -0
  214. sift_py/data_import/_status_test.py +176 -0
  215. sift_py/data_import/_tdms_test.py +238 -0
  216. sift_py/data_import/ch10.py +157 -0
  217. sift_py/data_import/config.py +19 -0
  218. sift_py/data_import/csv.py +259 -0
  219. sift_py/data_import/status.py +113 -0
  220. sift_py/data_import/tdms.py +206 -0
  221. sift_py/data_import/tempfile.py +30 -0
  222. sift_py/data_import/time_format.py +39 -0
  223. sift_py/error.py +11 -0
  224. sift_py/file_attachment/__init__.py +88 -0
  225. sift_py/file_attachment/_internal/__init__.py +0 -0
  226. sift_py/file_attachment/_internal/download.py +13 -0
  227. sift_py/file_attachment/_internal/upload.py +100 -0
  228. sift_py/file_attachment/_service_test.py +161 -0
  229. sift_py/file_attachment/entity.py +30 -0
  230. sift_py/file_attachment/metadata.py +107 -0
  231. sift_py/file_attachment/service.py +142 -0
  232. sift_py/grpc/__init__.py +15 -0
  233. sift_py/grpc/_async_interceptors/__init__.py +0 -0
  234. sift_py/grpc/_async_interceptors/base.py +72 -0
  235. sift_py/grpc/_async_interceptors/metadata.py +36 -0
  236. sift_py/grpc/_interceptors/__init__.py +0 -0
  237. sift_py/grpc/_interceptors/base.py +61 -0
  238. sift_py/grpc/_interceptors/context.py +25 -0
  239. sift_py/grpc/_interceptors/metadata.py +33 -0
  240. sift_py/grpc/_retry.py +70 -0
  241. sift_py/grpc/keepalive.py +34 -0
  242. sift_py/grpc/transport.py +250 -0
  243. sift_py/grpc/transport_test.py +170 -0
  244. sift_py/ingestion/__init__.py +6 -0
  245. sift_py/ingestion/_internal/__init__.py +6 -0
  246. sift_py/ingestion/_internal/channel.py +12 -0
  247. sift_py/ingestion/_internal/error.py +10 -0
  248. sift_py/ingestion/_internal/ingest.py +350 -0
  249. sift_py/ingestion/_internal/ingest_test.py +357 -0
  250. sift_py/ingestion/_internal/ingestion_config.py +130 -0
  251. sift_py/ingestion/_internal/run.py +46 -0
  252. sift_py/ingestion/_service_test.py +478 -0
  253. sift_py/ingestion/buffer.py +189 -0
  254. sift_py/ingestion/channel.py +422 -0
  255. sift_py/ingestion/config/__init__.py +3 -0
  256. sift_py/ingestion/config/telemetry.py +281 -0
  257. sift_py/ingestion/config/telemetry_test.py +405 -0
  258. sift_py/ingestion/config/yaml/__init__.py +0 -0
  259. sift_py/ingestion/config/yaml/error.py +44 -0
  260. sift_py/ingestion/config/yaml/load.py +126 -0
  261. sift_py/ingestion/config/yaml/spec.py +58 -0
  262. sift_py/ingestion/config/yaml/test_load.py +25 -0
  263. sift_py/ingestion/flow.py +73 -0
  264. sift_py/ingestion/manager.py +99 -0
  265. sift_py/ingestion/rule/__init__.py +4 -0
  266. sift_py/ingestion/rule/config.py +11 -0
  267. sift_py/ingestion/service.py +237 -0
  268. sift_py/py.typed +0 -0
  269. sift_py/report_templates/__init__.py +0 -0
  270. sift_py/report_templates/_config_test.py +34 -0
  271. sift_py/report_templates/_service_test.py +94 -0
  272. sift_py/report_templates/config.py +36 -0
  273. sift_py/report_templates/service.py +171 -0
  274. sift_py/rest.py +29 -0
  275. sift_py/rule/__init__.py +0 -0
  276. sift_py/rule/_config_test.py +109 -0
  277. sift_py/rule/_service_test.py +168 -0
  278. sift_py/rule/config.py +229 -0
  279. sift_py/rule/service.py +484 -0
  280. sift_py/yaml/__init__.py +0 -0
  281. sift_py/yaml/_channel_test.py +169 -0
  282. sift_py/yaml/_rule_test.py +207 -0
  283. sift_py/yaml/channel.py +224 -0
  284. sift_py/yaml/report_templates.py +73 -0
  285. sift_py/yaml/rule.py +321 -0
  286. sift_py/yaml/utils.py +15 -0
  287. sift_stack_py-0.3.2.dist-info/LICENSE +7 -0
  288. sift_stack_py-0.3.2.dist-info/METADATA +109 -0
  289. sift_stack_py-0.3.2.dist-info/RECORD +291 -0
  290. sift_stack_py-0.3.2.dist-info/WHEEL +5 -0
  291. sift_stack_py-0.3.2.dist-info/top_level.txt +5 -0
@@ -0,0 +1,395 @@
1
+ import json
2
+
3
+ import pandas as pd
4
+ import pytest
5
+ from pytest_mock import MockFixture
6
+
7
+ from sift_py.data_import.config import CsvConfig
8
+ from sift_py.data_import.csv import CsvUploadService
9
+ from sift_py.rest import SiftRestConfig
10
+
11
+
12
+ class MockResponse:
13
+ status_code: int
14
+ text: str
15
+
16
+ def __init__(self, status_code: int, text: str):
17
+ self.status_code = status_code
18
+ self.text = text
19
+
20
+ def json(self) -> dict:
21
+ return json.loads(self.text)
22
+
23
+
24
+ csv_config = CsvConfig(
25
+ {
26
+ "asset_name": "test_asset",
27
+ "first_data_row": 2,
28
+ "time_column": {
29
+ "format": "TIME_FORMAT_ABSOLUTE_DATETIME",
30
+ "column_number": 1,
31
+ },
32
+ "data_columns": {
33
+ 2: {
34
+ "name": "channel_1",
35
+ "data_type": "CHANNEL_DATA_TYPE_DOUBLE",
36
+ }
37
+ },
38
+ }
39
+ )
40
+
41
+ rest_config: SiftRestConfig = {
42
+ "uri": "some_uri.com",
43
+ "apikey": "123123123",
44
+ }
45
+
46
+
47
+ def test_csv_upload_service_upload_validate_path(mocker: MockFixture):
48
+ mock_path_is_file = mocker.patch("sift_py.data_import.csv.Path.is_file")
49
+ mock_path_is_file.return_value = False
50
+
51
+ with pytest.raises(Exception, match="does not point to a regular file"):
52
+ svc = CsvUploadService(rest_config)
53
+ svc.upload(
54
+ path="some_csv.csv",
55
+ csv_config=csv_config,
56
+ )
57
+
58
+
59
+ def test_csv_upload_service_upload_validate_mime_type(mocker: MockFixture):
60
+ mock_path_is_file = mocker.patch("sift_py.data_import.csv.Path.is_file")
61
+ mock_path_is_file.return_value = True
62
+
63
+ with pytest.raises(Exception, match="MIME"):
64
+ svc = CsvUploadService(rest_config)
65
+ svc.upload(
66
+ path="some_csv.asdfghjkl",
67
+ csv_config=csv_config,
68
+ )
69
+
70
+ with pytest.raises(Exception, match="Must be"):
71
+ svc = CsvUploadService(rest_config)
72
+ svc.upload(
73
+ path="some_file.pdf",
74
+ csv_config=csv_config,
75
+ )
76
+
77
+
78
+ def test_csv_upload_service_invalid_config_response(mocker: MockFixture):
79
+ mock_path_is_file = mocker.patch("sift_py.data_import.csv.Path.is_file")
80
+ mock_path_is_file.return_value = True
81
+
82
+ mock_requests_post = mocker.patch("sift_py.data_import.csv.requests.post")
83
+ mock_requests_post.return_value = MockResponse(status_code=400, text="Invalid request")
84
+ with pytest.raises(Exception, match="Config file upload request failed"):
85
+ svc = CsvUploadService(rest_config)
86
+
87
+ svc.upload(
88
+ path="some_csv.csv",
89
+ csv_config=csv_config,
90
+ )
91
+
92
+
93
+ def test_csv_upload_service_invalid_data_response(mocker: MockFixture):
94
+ mock_path_is_file = mocker.patch("sift_py.data_import.csv.Path.is_file")
95
+ mock_path_is_file.return_value = True
96
+
97
+ mocker.patch(
98
+ "sift_py.data_import.csv.open",
99
+ mocker.mock_open(),
100
+ )
101
+
102
+ mock_requests_post = mocker.patch("sift_py.data_import.csv.requests.post")
103
+ mock_requests_post.return_value = MockResponse(status_code=200, text="asdgasdg")
104
+
105
+ with pytest.raises(Exception, match="Invalid response"):
106
+ svc = CsvUploadService(rest_config)
107
+
108
+ svc.upload(
109
+ path="some_csv.csv",
110
+ csv_config=csv_config,
111
+ )
112
+
113
+ mock_requests_post = mocker.patch("sift_py.data_import.csv.requests.post")
114
+ mock_requests_post.return_value = MockResponse(status_code=200, text="{}")
115
+
116
+ with pytest.raises(Exception, match="Response missing required keys"):
117
+ svc = CsvUploadService(rest_config)
118
+
119
+ svc.upload(
120
+ path="some_csv.csv",
121
+ csv_config=csv_config,
122
+ )
123
+
124
+ mock_requests_post.side_effect = [
125
+ MockResponse(
126
+ status_code=200,
127
+ text=json.dumps({"uploadUrl": "some_url.com", "dataImportId": "123-123-123"}),
128
+ ),
129
+ MockResponse(status_code=400, text="Invalid request"),
130
+ ]
131
+
132
+ with pytest.raises(Exception, match="Data file upload request failed"):
133
+ svc = CsvUploadService(rest_config)
134
+
135
+ svc.upload(
136
+ path="some_csv.csv",
137
+ csv_config=csv_config,
138
+ )
139
+
140
+
141
+ def test_csv_upload_service_success(mocker: MockFixture):
142
+ mock_path_is_file = mocker.patch("sift_py.data_import.csv.Path.is_file")
143
+ mock_path_is_file.return_value = True
144
+
145
+ mock_requests_post = mocker.patch("sift_py.data_import.csv.requests.post")
146
+ mock_requests_post.side_effect = [
147
+ MockResponse(
148
+ status_code=200,
149
+ text=json.dumps({"uploadUrl": "some_url.com", "dataImportId": "123-123-123"}),
150
+ ),
151
+ MockResponse(status_code=200, text=""),
152
+ ]
153
+
154
+ mocker.patch(
155
+ "sift_py.data_import.csv.open",
156
+ mocker.mock_open(),
157
+ )
158
+ svc = CsvUploadService(
159
+ {
160
+ "uri": "some_uri.com",
161
+ "apikey": "123123123",
162
+ },
163
+ )
164
+
165
+ svc.upload(
166
+ path="some_csv.csv",
167
+ csv_config=csv_config,
168
+ )
169
+
170
+
171
+ def test_csv_upload_service_upload_validate_url(mocker: MockFixture):
172
+ with pytest.raises(Exception, match="Invalid URL scheme:"):
173
+ svc = CsvUploadService(rest_config)
174
+
175
+ svc.upload_from_url(
176
+ url="asdf://some_url.com/file.csv",
177
+ csv_config=csv_config,
178
+ )
179
+
180
+
181
+ def test_csv_upload_service_upload_from_url_invalid_config(mocker: MockFixture):
182
+ mock_requests_post = mocker.patch("sift_py.data_import.csv.requests.post")
183
+ mock_requests_post.return_value = MockResponse(status_code=400, text="Invalid request")
184
+ with pytest.raises(Exception, match="URL upload request failed"):
185
+ svc = CsvUploadService(rest_config)
186
+
187
+ svc.upload_from_url(
188
+ url="http://some_url.com/file.csv",
189
+ csv_config=csv_config,
190
+ )
191
+
192
+
193
+ def test_csv_upload_service_upload_from_url_success(mocker: MockFixture):
194
+ mock_requests_post = mocker.patch("sift_py.data_import.csv.requests.post")
195
+ mock_requests_post.return_value = MockResponse(
196
+ status_code=200,
197
+ text=json.dumps({"uploadUrl": "some_url.com", "dataImportId": "123-123-123"}),
198
+ )
199
+ svc = CsvUploadService(
200
+ {
201
+ "uri": "some_uri.com",
202
+ "apikey": "123123123",
203
+ },
204
+ )
205
+
206
+ svc.upload_from_url(
207
+ url="http://some_url.com/file.csv",
208
+ csv_config=csv_config,
209
+ )
210
+
211
+
212
+ def test_simple_upload_invalid_csv(mocker: MockFixture):
213
+ mock_path_is_file = mocker.patch("sift_py.data_import.csv.Path.is_file")
214
+ mock_path_is_file.return_value = True
215
+
216
+ mock_read_csv = mocker.patch("sift_py.data_import.csv.pd.read_csv")
217
+ mock_read_csv.return_value = pd.DataFrame(
218
+ {
219
+ "time": [1, 2, 3],
220
+ "channel_1": [1, 1.0, True],
221
+ }
222
+ )
223
+ with pytest.raises(Exception, match="Unable to upload.*"):
224
+ svc = CsvUploadService(rest_config)
225
+ svc.simple_upload("test_asset", "sample.csv")
226
+
227
+ mock_read_csv = mocker.patch("sift_py.data_import.csv.pd.read_csv")
228
+ mock_read_csv.return_value = pd.DataFrame(
229
+ {
230
+ "time": [1, 2, 3],
231
+ "channel_1": [complex(1), complex(1), complex(1)],
232
+ }
233
+ )
234
+ with pytest.raises(Exception, match="Unable to upload.*"):
235
+ svc = CsvUploadService(rest_config)
236
+ svc.simple_upload("test_asset", "sample.csv")
237
+
238
+ mock_read_csv = mocker.patch("sift_py.data_import.csv.pd.read_csv")
239
+ mock_read_csv.return_value = pd.DataFrame(
240
+ {
241
+ "time": [1, 2, 3],
242
+ "channel_bool": [True, True, False],
243
+ "channel_int": [-1, 2, 0],
244
+ "channel_double": [1.0, 2.0, -3.3],
245
+ "channel_string": ["a", "b", "c"],
246
+ }
247
+ )
248
+ mock_requests_post = mocker.patch("sift_py.data_import.csv.requests.post")
249
+ mock_requests_post.side_effect = [
250
+ MockResponse(
251
+ status_code=200,
252
+ text=json.dumps({"uploadUrl": "some_url.com", "dataImportId": "123-123-123"}),
253
+ ),
254
+ MockResponse(status_code=200, text=""),
255
+ ]
256
+ mocker.patch(
257
+ "sift_py.data_import.csv.open",
258
+ mocker.mock_open(),
259
+ )
260
+ svc = CsvUploadService(rest_config)
261
+ svc.simple_upload("test_asset", "sample.csv")
262
+
263
+
264
+ def test_simple_upload_metadata_csv(mocker: MockFixture):
265
+ mock_path_is_file = mocker.patch("sift_py.data_import.csv.Path.is_file")
266
+ mock_path_is_file.return_value = True
267
+
268
+ def mock_read_csv(*_, **kwargs):
269
+ if "skiprows" in kwargs:
270
+ return pd.DataFrame(
271
+ {
272
+ "time": [1, 2, 3],
273
+ "channel_int": [-1, 2, 1],
274
+ }
275
+ )
276
+ else:
277
+ return pd.DataFrame(
278
+ {
279
+ "time": ["s", "a description", 1, 2, 3],
280
+ "channel_int": ["degC", "another description", -1, 2, 1],
281
+ }
282
+ )
283
+
284
+ mocker.patch("sift_py.data_import.csv.pd.read_csv", mock_read_csv)
285
+
286
+ mock_requests_post = mocker.patch("sift_py.data_import.csv.requests.post")
287
+ mock_requests_post.side_effect = [
288
+ MockResponse(
289
+ status_code=200,
290
+ text=json.dumps({"uploadUrl": "some_url.com", "dataImportId": "123-123-123"}),
291
+ ),
292
+ MockResponse(status_code=200, text=""),
293
+ ]
294
+ mocker.patch(
295
+ "sift_py.data_import.csv.open",
296
+ mocker.mock_open(),
297
+ )
298
+ svc = CsvUploadService(rest_config)
299
+
300
+ svc.simple_upload("test_asset", "sample.csv", units_row=2, descriptions_row=3)
301
+
302
+ expected_csv_config = CsvConfig(
303
+ {
304
+ "asset_name": "test_asset",
305
+ "run_name": "",
306
+ "run_id": "",
307
+ "first_data_row": 2,
308
+ "time_column": {
309
+ "format": "TIME_FORMAT_ABSOLUTE_DATETIME",
310
+ "column_number": 1,
311
+ },
312
+ "data_columns": {
313
+ "2": {
314
+ "name": "channel_int",
315
+ "data_type": "CHANNEL_DATA_TYPE_INT_64",
316
+ "component": "",
317
+ "units": "degC",
318
+ "description": "another description",
319
+ "enum_types": [],
320
+ "bit_field_elements": [],
321
+ }
322
+ },
323
+ }
324
+ )
325
+
326
+ mock_requests_post.assert_any_call(
327
+ url="https://some_uri.com/api/v1/data-imports:upload",
328
+ headers={
329
+ "Authorization": "Bearer 123123123",
330
+ "Content-Encoding": "application/octet-stream",
331
+ },
332
+ data=json.dumps({"csv_config": expected_csv_config.to_dict()}),
333
+ )
334
+
335
+
336
+ def test_simple_upload_uint64_csv(mocker: MockFixture):
337
+ mock_path_is_file = mocker.patch("sift_py.data_import.csv.Path.is_file")
338
+ mock_path_is_file.return_value = True
339
+
340
+ mock_read_csv = mocker.patch("sift_py.data_import.csv.pd.read_csv")
341
+ mock_read_csv.return_value = pd.DataFrame(
342
+ {
343
+ "time": [1, 2, 3],
344
+ "channel_uint64": [1, 2, 2**63],
345
+ }
346
+ )
347
+
348
+ mock_requests_post = mocker.patch("sift_py.data_import.csv.requests.post")
349
+ mock_requests_post.side_effect = [
350
+ MockResponse(
351
+ status_code=200,
352
+ text=json.dumps({"uploadUrl": "some_url.com", "dataImportId": "123-123-123"}),
353
+ ),
354
+ MockResponse(status_code=200, text=""),
355
+ ]
356
+ mocker.patch(
357
+ "sift_py.data_import.csv.open",
358
+ mocker.mock_open(),
359
+ )
360
+ svc = CsvUploadService(rest_config)
361
+
362
+ svc.simple_upload("test_asset", "sample.csv")
363
+
364
+ expected_csv_config = CsvConfig(
365
+ {
366
+ "asset_name": "test_asset",
367
+ "run_name": "",
368
+ "run_id": "",
369
+ "first_data_row": 2,
370
+ "time_column": {
371
+ "format": "TIME_FORMAT_ABSOLUTE_DATETIME",
372
+ "column_number": 1,
373
+ },
374
+ "data_columns": {
375
+ "2": {
376
+ "name": "channel_uint64",
377
+ "data_type": "CHANNEL_DATA_TYPE_UINT_64",
378
+ "component": "",
379
+ "units": "",
380
+ "description": "",
381
+ "enum_types": [],
382
+ "bit_field_elements": [],
383
+ }
384
+ },
385
+ }
386
+ )
387
+
388
+ mock_requests_post.assert_any_call(
389
+ url="https://some_uri.com/api/v1/data-imports:upload",
390
+ headers={
391
+ "Authorization": "Bearer 123123123",
392
+ "Content-Encoding": "application/octet-stream",
393
+ },
394
+ data=json.dumps({"csv_config": expected_csv_config.to_dict()}),
395
+ )
@@ -0,0 +1,176 @@
1
+ import json
2
+ from copy import deepcopy
3
+
4
+ import pytest
5
+ from pytest_mock import MockFixture
6
+
7
+ from sift_py.data_import.status import DataImportService, DataImportStatusType
8
+ from sift_py.rest import SiftRestConfig
9
+
10
+ rest_config: SiftRestConfig = {
11
+ "uri": "some_uri.com",
12
+ "apikey": "123123123",
13
+ }
14
+
15
+
16
+ @pytest.fixture
17
+ def data_import_data():
18
+ return {
19
+ "dataImport": {
20
+ "dataImportId": "random-data-import-id",
21
+ "createdDate": "2024-10-07T18:37:00.146649Z",
22
+ "modifiedDate": "2024-10-07T18:37:00.146649Z",
23
+ "sourceUrl": "",
24
+ "status": "",
25
+ "errorMessage": "",
26
+ "csvConfig": {},
27
+ }
28
+ }
29
+
30
+
31
+ class MockResponse:
32
+ status_code: int
33
+ text: str
34
+
35
+ def __init__(self, status_code: int, text: str):
36
+ self.status_code = status_code
37
+ self.text = text
38
+
39
+ def json(self):
40
+ return json.loads(self.text)
41
+
42
+ def raise_for_status(self):
43
+ if self.status_code != 200:
44
+ raise Exception("Invalid status")
45
+
46
+
47
+ def test_get_status(mocker: MockFixture, data_import_data: dict):
48
+ mock_requests_post = mocker.patch("sift_py.data_import.status.requests.get")
49
+ data_import_data["dataImport"]["status"] = "DATA_IMPORT_STATUS_SUCCEEDED"
50
+ mock_requests_post.return_value = MockResponse(
51
+ status_code=200, text=json.dumps(data_import_data)
52
+ )
53
+ service = DataImportService(rest_config, "123-123-123")
54
+ assert service.get_data_import().status == DataImportStatusType.SUCCEEDED
55
+
56
+ data_import_data["dataImport"]["status"] = "DATA_IMPORT_STATUS_PENDING"
57
+ mock_requests_post.return_value = MockResponse(
58
+ status_code=200, text=json.dumps(data_import_data)
59
+ )
60
+ service = DataImportService(rest_config, "123-123-123")
61
+ assert service.get_data_import().status == DataImportStatusType.PENDING
62
+
63
+ data_import_data["dataImport"]["status"] = "DATA_IMPORT_STATUS_IN_PROGRESS"
64
+ mock_requests_post.return_value = MockResponse(
65
+ status_code=200, text=json.dumps(data_import_data)
66
+ )
67
+ service = DataImportService(rest_config, "123-123-123")
68
+ assert service.get_data_import().status == DataImportStatusType.IN_PROGRESS
69
+
70
+ data_import_data["dataImport"]["status"] = "DATA_IMPORT_STATUS_FAILED"
71
+ mock_requests_post.return_value = MockResponse(
72
+ status_code=200, text=json.dumps(data_import_data)
73
+ )
74
+ service = DataImportService(rest_config, "123-123-123")
75
+ assert service.get_data_import().status == DataImportStatusType.FAILED
76
+
77
+ data_import_data["dataImport"]["status"] = "INVALID_STATUS"
78
+ with pytest.raises(Exception, match="Invalid data import status"):
79
+ mock_requests_post.return_value = MockResponse(
80
+ status_code=200, text=json.dumps(data_import_data)
81
+ )
82
+ service = DataImportService(rest_config, "123-123-123")
83
+ service.get_data_import()
84
+
85
+
86
+ def test_wait_success(mocker: MockFixture, data_import_data: dict):
87
+ mock_time_sleep = mocker.patch("sift_py.data_import.status.time.sleep")
88
+ mock_requests_get = mocker.patch("sift_py.data_import.status.requests.get")
89
+
90
+ succeeded = deepcopy(data_import_data)
91
+ succeeded["dataImport"]["status"] = "DATA_IMPORT_STATUS_SUCCEEDED"
92
+
93
+ pending = deepcopy(data_import_data)
94
+ pending["dataImport"]["status"] = "DATA_IMPORT_STATUS_PENDING"
95
+
96
+ in_progress = deepcopy(data_import_data)
97
+ in_progress["dataImport"]["status"] = "DATA_IMPORT_STATUS_IN_PROGRESS"
98
+
99
+ mock_requests_get.side_effect = [
100
+ MockResponse(
101
+ status_code=200,
102
+ text=json.dumps(pending),
103
+ ),
104
+ MockResponse(
105
+ status_code=200,
106
+ text=json.dumps(in_progress),
107
+ ),
108
+ MockResponse(
109
+ status_code=200,
110
+ text=json.dumps(succeeded),
111
+ ),
112
+ ]
113
+
114
+ service = DataImportService(rest_config, "123-123-123")
115
+ assert service.wait_until_complete().status == DataImportStatusType.SUCCEEDED
116
+ mock_time_sleep.assert_any_call(1)
117
+ mock_time_sleep.assert_any_call(2)
118
+
119
+
120
+ def test_wait_failure(mocker: MockFixture, data_import_data: dict):
121
+ mock_requests_get = mocker.patch("sift_py.data_import.status.requests.get")
122
+
123
+ failed = deepcopy(data_import_data)
124
+ failed["dataImport"]["status"] = "DATA_IMPORT_STATUS_FAILED"
125
+
126
+ pending = deepcopy(data_import_data)
127
+ pending["dataImport"]["status"] = "DATA_IMPORT_STATUS_PENDING"
128
+
129
+ in_progress = deepcopy(data_import_data)
130
+ in_progress["dataImport"]["status"] = "DATA_IMPORT_STATUS_IN_PROGRESS"
131
+
132
+ mock_requests_get.side_effect = [
133
+ MockResponse(
134
+ status_code=200,
135
+ text=json.dumps(pending),
136
+ ),
137
+ MockResponse(
138
+ status_code=200,
139
+ text=json.dumps(in_progress),
140
+ ),
141
+ MockResponse(
142
+ status_code=200,
143
+ text=json.dumps(failed),
144
+ ),
145
+ ]
146
+
147
+ service = DataImportService(rest_config, "123-123-123")
148
+ assert service.wait_until_complete().status == DataImportStatusType.FAILED
149
+
150
+
151
+ def test_wait_max_polling_interval(mocker: MockFixture, data_import_data: dict):
152
+ mock_time_sleep = mocker.patch("sift_py.data_import.status.time.sleep")
153
+ mock_requests_get = mocker.patch("sift_py.data_import.status.requests.get")
154
+
155
+ succeeded = deepcopy(data_import_data)
156
+ succeeded["dataImport"]["status"] = "DATA_IMPORT_STATUS_SUCCEEDED"
157
+
158
+ in_progress = deepcopy(data_import_data)
159
+ in_progress["dataImport"]["status"] = "DATA_IMPORT_STATUS_IN_PROGRESS"
160
+
161
+ mock_requests_get.side_effect = [
162
+ MockResponse(
163
+ status_code=200,
164
+ text=json.dumps(in_progress),
165
+ )
166
+ for _ in range(60)
167
+ ] + [
168
+ MockResponse(
169
+ status_code=200,
170
+ text=json.dumps(succeeded),
171
+ )
172
+ ]
173
+
174
+ service = DataImportService(rest_config, "123-123-123")
175
+ assert service.wait_until_complete().status == DataImportStatusType.SUCCEEDED
176
+ mock_time_sleep.assert_called_with(60)