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,161 @@
1
+ import json
2
+
3
+ import pytest
4
+ from pytest_mock import MockFixture
5
+ from sift.remote_files.v1.remote_files_pb2 import GetRemoteFileResponse, RemoteFile
6
+
7
+ from sift_py._internal.test_util.channel import MockChannel
8
+ from sift_py.file_attachment.entity import Entity, EntityType
9
+ from sift_py.file_attachment.metadata import ImageMetadata
10
+ from sift_py.file_attachment.service import FileAttachmentService
11
+
12
+
13
+ class MockResponse:
14
+ status_code: int
15
+ text: str
16
+
17
+ def __init__(self, status_code: int, text: str):
18
+ self.status_code = status_code
19
+ self.text = text
20
+
21
+ def json(self):
22
+ return json.loads(self.text)
23
+
24
+
25
+ class MockMultipartEncoder:
26
+ @property
27
+ def content_type(self):
28
+ return "multipart/form-data"
29
+
30
+
31
+ def test_file_attachments_service_upload_validate_uri():
32
+ mock_channel = MockChannel()
33
+
34
+ svc = FileAttachmentService(
35
+ mock_channel,
36
+ {
37
+ "uri": "https://some_uri.com",
38
+ "apikey": "123123123",
39
+ },
40
+ )
41
+
42
+ assert svc is not None
43
+
44
+ svc = FileAttachmentService(
45
+ mock_channel,
46
+ {
47
+ "uri": "some_uri.com",
48
+ "apikey": "123123123",
49
+ },
50
+ )
51
+
52
+ assert svc is not None
53
+
54
+
55
+ def test_file_attachments_service_upload_validate_path(mocker: MockFixture):
56
+ mock_channel = MockChannel()
57
+
58
+ mock_path_is_file = mocker.patch("sift_py.file_attachment._internal.upload.Path.is_file")
59
+ mock_path_is_file.return_value = False
60
+
61
+ with pytest.raises(Exception, match="does not point to a regular file"):
62
+ svc = FileAttachmentService(
63
+ mock_channel,
64
+ {
65
+ "uri": "some_uri.com",
66
+ "apikey": "123123123",
67
+ },
68
+ )
69
+
70
+ svc.upload_attachment(
71
+ path="some_image.png.gz",
72
+ entity=Entity(
73
+ entity_id="123-123-123",
74
+ entity_type=EntityType.ANNOTATION_LOG,
75
+ ),
76
+ metadata=ImageMetadata(
77
+ width=16,
78
+ height=9,
79
+ ),
80
+ )
81
+
82
+
83
+ def test_file_attachments_service_upload_validate_mimetype(mocker: MockFixture):
84
+ mock_channel = MockChannel()
85
+
86
+ mock_path_is_file = mocker.patch("sift_py.file_attachment._internal.upload.Path.is_file")
87
+ mock_path_is_file.return_value = True
88
+
89
+ with pytest.raises(Exception, match="MIME"):
90
+ svc = FileAttachmentService(
91
+ mock_channel,
92
+ {
93
+ "uri": "some_uri.com",
94
+ "apikey": "123123123",
95
+ },
96
+ )
97
+
98
+ svc.upload_attachment(
99
+ path="some_image.asdlkjfh",
100
+ entity=Entity(
101
+ entity_id="123-123-123",
102
+ entity_type=EntityType.ANNOTATION_LOG,
103
+ ),
104
+ metadata=ImageMetadata(
105
+ width=16,
106
+ height=9,
107
+ ),
108
+ )
109
+
110
+
111
+ def test_file_attachments_service_upload_returns_remote_file(mocker: MockFixture):
112
+ mock_channel = MockChannel()
113
+
114
+ mock_path_is_file = mocker.patch("sift_py.file_attachment._internal.upload.Path.is_file")
115
+ mock_path_is_file.return_value = True
116
+
117
+ mocker.patch(
118
+ "sift_py.file_attachment._internal.upload.open",
119
+ mocker.mock_open(read_data=b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR"),
120
+ )
121
+
122
+ mock_multipart_encoder = mocker.patch(
123
+ "sift_py.file_attachment._internal.upload.MultipartEncoder"
124
+ )
125
+ mock_multipart_encoder.return_value = MockMultipartEncoder()
126
+
127
+ mock_requests_post = mocker.patch("sift_py.file_attachment._internal.upload.requests.post")
128
+ mock_requests_post.return_value = MockResponse(
129
+ status_code=200, text=json.dumps({"remoteFile": {"remoteFileId": "abc"}})
130
+ )
131
+
132
+ svc = FileAttachmentService(
133
+ mock_channel,
134
+ {
135
+ "uri": "some_uri.com",
136
+ "apikey": "123123123",
137
+ },
138
+ )
139
+
140
+ mock_get_remote_file = mocker.patch.object(
141
+ svc._remote_file_service_stub,
142
+ "GetRemoteFile",
143
+ return_value=GetRemoteFileResponse(remote_file=RemoteFile(remote_file_id="abc")),
144
+ )
145
+
146
+ remote_file = svc.upload_attachment(
147
+ path="some_image.png.gz",
148
+ entity=Entity(
149
+ entity_id="123-123-123",
150
+ entity_type=EntityType.ANNOTATION_LOG,
151
+ ),
152
+ metadata=ImageMetadata(
153
+ width=16,
154
+ height=9,
155
+ ),
156
+ )
157
+ mock_get_remote_file.assert_called_once()
158
+ mock_multipart_encoder.assert_called_once()
159
+ mock_requests_post.assert_called_once()
160
+
161
+ assert remote_file.remote_file_id == "abc"
@@ -0,0 +1,30 @@
1
+ """
2
+ Entities represent things that files can be attached to.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from enum import Enum
8
+
9
+
10
+ class Entity:
11
+ """
12
+ An abstract entity that represents the thing that we want to attach files to.
13
+ """
14
+
15
+ entity_id: str
16
+ entity_type: EntityType
17
+
18
+ def __init__(self, entity_id: str, entity_type: EntityType):
19
+ self.entity_id = entity_id
20
+ self.entity_type = entity_type
21
+
22
+
23
+ class EntityType(Enum):
24
+ """
25
+ Represents the types of entities that supports file attachments.
26
+ """
27
+
28
+ RUN = "runs"
29
+ ANNOTATION = "annotations"
30
+ ANNOTATION_LOG = "annotation_logs"
@@ -0,0 +1,107 @@
1
+ """
2
+ Module containing optional metadata types to provide to Sift when uploading a file attachment.
3
+ Though optional, providing this information could help improve quality of renders on the Sift app.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from datetime import datetime
9
+ from typing import Any, Optional, Type
10
+
11
+ from google.protobuf.timestamp_pb2 import Timestamp
12
+ from sift.remote_files.v1.remote_files_pb2 import (
13
+ ImageMetadata as ImageMetadataPb,
14
+ )
15
+ from sift.remote_files.v1.remote_files_pb2 import (
16
+ VideoMetadata as VideoMetadataPb,
17
+ )
18
+ from typing_extensions import Self
19
+
20
+ from sift_py._internal.convert.json import AsJson
21
+ from sift_py._internal.convert.protobuf import AsProtobuf
22
+
23
+
24
+ class Metadata(AsJson): ...
25
+
26
+
27
+ class VideoMetadata(AsProtobuf, Metadata):
28
+ """
29
+ Metadata for video media-types i.e. any mimetypes of the following pattern: `video/*`.
30
+ """
31
+
32
+ width: int
33
+ height: int
34
+ duration_seconds: float
35
+ timestamp: Optional[datetime]
36
+
37
+ def __init__(
38
+ self, width: int, height: int, duration_seconds: float, timestamp: Optional[datetime] = None
39
+ ):
40
+ self.width = width
41
+ self.height = height
42
+ self.duration_seconds = duration_seconds
43
+ self.timestamp = timestamp
44
+
45
+ def as_pb(self, klass: Type[VideoMetadataPb]) -> VideoMetadataPb:
46
+ if self.timestamp is not None:
47
+ timestamp_pb = Timestamp()
48
+ timestamp_pb.FromDatetime(self.timestamp)
49
+ else:
50
+ timestamp_pb = None
51
+
52
+ return klass(
53
+ width=self.width,
54
+ height=self.height,
55
+ duration_seconds=self.duration_seconds,
56
+ timestamp=timestamp_pb,
57
+ )
58
+
59
+ @classmethod
60
+ def from_pb(cls, message: VideoMetadataPb) -> Self:
61
+ return cls(
62
+ width=message.width,
63
+ height=message.height,
64
+ duration_seconds=message.duration_seconds,
65
+ timestamp=message.timestamp.ToDateTime(), # type: ignore
66
+ )
67
+
68
+ def as_json(self) -> Any:
69
+ timestamp = None if self.timestamp is None else self.timestamp.isoformat()
70
+ return {
71
+ "height": self.height,
72
+ "width": self.width,
73
+ "duration_seconds": self.duration_seconds,
74
+ "timestamp": timestamp,
75
+ }
76
+
77
+
78
+ class ImageMetadata(AsProtobuf, Metadata):
79
+ """
80
+ Metadata for image media-types i.e. any mimetypes of the following pattern: `image/*`.
81
+ """
82
+
83
+ width: int
84
+ height: int
85
+
86
+ def __init__(self, width: int, height: int):
87
+ self.width = width
88
+ self.height = height
89
+
90
+ def as_pb(self, klass: Type[ImageMetadataPb]) -> ImageMetadataPb:
91
+ return klass(
92
+ width=self.width,
93
+ height=self.height,
94
+ )
95
+
96
+ @classmethod
97
+ def from_pb(cls, message: ImageMetadataPb) -> Self:
98
+ return cls(
99
+ width=message.width,
100
+ height=message.height,
101
+ )
102
+
103
+ def as_json(self) -> Any:
104
+ return {
105
+ "height": self.height,
106
+ "width": self.width,
107
+ }
@@ -0,0 +1,142 @@
1
+ from pathlib import Path
2
+ from typing import List, Optional, Union, cast
3
+
4
+ from sift.remote_files.v1.remote_files_pb2 import (
5
+ BatchDeleteRemoteFilesRequest,
6
+ GetRemoteFileDownloadUrlRequest,
7
+ GetRemoteFileDownloadUrlResponse,
8
+ GetRemoteFileRequest,
9
+ GetRemoteFileResponse,
10
+ ListRemoteFilesRequest,
11
+ ListRemoteFilesResponse,
12
+ RemoteFile,
13
+ )
14
+ from sift.remote_files.v1.remote_files_pb2_grpc import RemoteFileServiceStub
15
+
16
+ from sift_py.file_attachment._internal.download import download_remote_file
17
+ from sift_py.file_attachment._internal.upload import UploadService
18
+ from sift_py.file_attachment.entity import Entity
19
+ from sift_py.file_attachment.metadata import Metadata
20
+ from sift_py.grpc.transport import SiftChannel
21
+ from sift_py.rest import SiftRestConfig
22
+
23
+
24
+ class FileAttachmentService:
25
+ """
26
+ Service used to retrieve, upload, download, and delete file attachments. Seee `sift_py.file_attachment`
27
+ for more information and examples on how to use this service.
28
+ """
29
+
30
+ _remote_file_service_stub: RemoteFileServiceStub
31
+ _upload_service: UploadService
32
+
33
+ def __init__(self, channel: SiftChannel, restconf: SiftRestConfig):
34
+ self._remote_file_service_stub = RemoteFileServiceStub(channel)
35
+ self._upload_service = UploadService(restconf)
36
+
37
+ def retrieve_attachments(self, entity: Entity) -> List[RemoteFile]:
38
+ """
39
+ Retrieves all file attachments for the provided `entity`.
40
+ """
41
+
42
+ filter = f'entity_id=="{entity.entity_id}" && entity_type=="{entity.entity_type.value}"'
43
+ page_size = 1_000
44
+ next_page_token = ""
45
+
46
+ remote_files: List[RemoteFile] = []
47
+
48
+ while True:
49
+ req = ListRemoteFilesRequest(
50
+ filter=filter,
51
+ page_size=page_size,
52
+ page_token=next_page_token,
53
+ )
54
+ res = cast(ListRemoteFilesResponse, self._remote_file_service_stub.ListRemoteFiles(req))
55
+ remote_files.extend(res.remote_files)
56
+ next_page_token = res.next_page_token
57
+
58
+ if not next_page_token:
59
+ break
60
+
61
+ return remote_files
62
+
63
+ def upload_attachment(
64
+ self,
65
+ path: Union[str, Path],
66
+ entity: Entity,
67
+ metadata: Optional[Metadata],
68
+ description: Optional[str] = None,
69
+ organization_id: Optional[str] = None,
70
+ ) -> RemoteFile:
71
+ """
72
+ Uploads a file pointed to by `path` and attaches it to the provided `entity`.
73
+
74
+ - `path`: A path to the file to upload to Sift as a file attachment.
75
+ - `entity`: The entity to attach the file to.
76
+ - `metadata`: Optional metadata to include with the specific file.
77
+ - `description`: An optional description to provide for the file attachment.
78
+ - `organization_id`: Only required if your user belongs to multiple organizations.
79
+ """
80
+ remote_file_id = self._upload_service.upload_attachment(
81
+ path,
82
+ entity,
83
+ metadata,
84
+ description,
85
+ organization_id,
86
+ )
87
+ req = GetRemoteFileRequest(remote_file_id=remote_file_id)
88
+ res = cast(GetRemoteFileResponse, self._remote_file_service_stub.GetRemoteFile(req))
89
+ return res.remote_file
90
+
91
+ def download_attachment(
92
+ self,
93
+ file: Union[RemoteFile, str],
94
+ out: Optional[Union[str, Path]] = None,
95
+ ) -> Path:
96
+ """
97
+ Downloads a file attachment and saves it locally.
98
+
99
+ - `remote_file`: Could either be an instance of `RemoteFile` or the ID of the remote file to download.
100
+ - `out`: If unspecified, then the file will be downloaded to the current working directory with the original name.
101
+ """
102
+
103
+ if isinstance(file, RemoteFile):
104
+ remote_file = file
105
+ else:
106
+ req = GetRemoteFileRequest(remote_file_id=file)
107
+ res = cast(GetRemoteFileResponse, self._remote_file_service_stub.GetRemoteFile(req))
108
+ remote_file = res.remote_file
109
+
110
+ output_file_path = (
111
+ Path(out) if isinstance(out, str) else Path(remote_file.file_name).resolve()
112
+ )
113
+
114
+ download_url_req = GetRemoteFileDownloadUrlRequest(
115
+ remote_file_id=remote_file.remote_file_id
116
+ )
117
+ download_url_res = cast(
118
+ GetRemoteFileDownloadUrlResponse,
119
+ self._remote_file_service_stub.GetRemoteFileDownloadUrl(download_url_req),
120
+ )
121
+ url = download_url_res.download_url
122
+
123
+ download_remote_file(url, output_file_path)
124
+
125
+ return output_file_path
126
+
127
+ def delete_file_attachments(self, *to_delete: Union[str, RemoteFile]):
128
+ """
129
+ Deletes remote files given a set of arguments that could either be instances of `RemoteFile` or the ID
130
+ of remote files to delete
131
+ """
132
+ remote_file_ids = [
133
+ remote_file.remote_file_id if isinstance(remote_file, RemoteFile) else remote_file
134
+ for remote_file in to_delete
135
+ ]
136
+
137
+ batch_size = 1_000
138
+ for i in range(0, len(remote_file_ids), batch_size):
139
+ batch = remote_file_ids[i : i + batch_size]
140
+ self._remote_file_service_stub.BatchDeleteRemoteFiles(
141
+ BatchDeleteRemoteFilesRequest(remote_file_ids=batch)
142
+ )
@@ -0,0 +1,15 @@
1
+ """
2
+ This module is primarily concerned with configuring and initializing gRPC connections to the Sift API.
3
+
4
+ Example of establishing a connection to Sift's gRPC APi:
5
+
6
+ ```python
7
+ from sift_py.grpc.transport import SiftChannelConfig, use_sift_channel
8
+
9
+ # Be sure not to include the url scheme i.e. 'https://' in the uri.
10
+ sift_channel_config = SiftChannelConfig(uri=SIFT_BASE_URI, apikey=SIFT_API_KEY)
11
+
12
+ with use_sift_channel(sift_channel_config) as channel:
13
+ # Connect to Sift
14
+ ```
15
+ """
File without changes
@@ -0,0 +1,72 @@
1
+ from abc import abstractmethod
2
+ from typing import Any, AsyncIterable, Callable, Iterable, TypeVar, Union
3
+
4
+ from grpc import aio as grpc_aio
5
+
6
+ CallType = TypeVar("CallType", bound=grpc_aio.Call)
7
+ Continuation = Callable[[grpc_aio.ClientCallDetails, Any], CallType]
8
+
9
+
10
+ class ClientAsyncInterceptor(
11
+ grpc_aio.UnaryUnaryClientInterceptor,
12
+ grpc_aio.UnaryStreamClientInterceptor,
13
+ grpc_aio.StreamUnaryClientInterceptor,
14
+ grpc_aio.StreamStreamClientInterceptor,
15
+ ):
16
+ @abstractmethod
17
+ async def intercept(
18
+ self,
19
+ method: Callable,
20
+ request_or_iterator: Any,
21
+ client_call_details: grpc_aio.ClientCallDetails,
22
+ ) -> Any:
23
+ pass
24
+
25
+ async def intercept_unary_unary(
26
+ self,
27
+ continuation: Continuation[grpc_aio.UnaryUnaryCall],
28
+ client_call_details: grpc_aio.ClientCallDetails,
29
+ request: Any,
30
+ ):
31
+ return await self.intercept(_async_swap_args(continuation), request, client_call_details)
32
+
33
+ async def intercept_unary_stream(
34
+ self,
35
+ continuation: Continuation[grpc_aio.UnaryStreamCall],
36
+ client_call_details: grpc_aio.ClientCallDetails,
37
+ request: Any,
38
+ ):
39
+ return await self.intercept(_async_swap_args(continuation), request, client_call_details)
40
+
41
+ async def intercept_stream_unary(
42
+ self,
43
+ continuation: Continuation[grpc_aio.StreamUnaryCall],
44
+ client_call_details: grpc_aio.ClientCallDetails,
45
+ request_iterator: Union[Iterable[Any], AsyncIterable[Any]],
46
+ ):
47
+ return await self.intercept(
48
+ _async_swap_args(continuation), request_iterator, client_call_details
49
+ )
50
+
51
+ async def intercept_stream_stream(
52
+ self,
53
+ continuation: Continuation[grpc_aio.StreamStreamCall],
54
+ client_call_details: grpc_aio.ClientCallDetails,
55
+ request_iterator: Union[Iterable[Any], AsyncIterable[Any]],
56
+ ):
57
+ return await self.intercept(
58
+ _async_swap_args(continuation), request_iterator, client_call_details
59
+ )
60
+
61
+
62
+ def _async_swap_args(fn: Callable[[Any, Any], Any]) -> Callable[[Any, Any], Any]:
63
+ """
64
+ Continuations are typed in such a way that details are the first argument, and the request second.
65
+ Code generated from protobuf however takes in the request first, then the details. Weird grpc library
66
+ quirk. This utility just flips the arguments.
67
+ """
68
+
69
+ async def new_fn(x, y):
70
+ return await fn(y, x)
71
+
72
+ return new_fn
@@ -0,0 +1,36 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Callable, List, Tuple, cast
4
+
5
+ from grpc import aio as grpc_aio
6
+
7
+ from sift_py.grpc._async_interceptors.base import ClientAsyncInterceptor
8
+
9
+ Metadata = List[Tuple[str, str]]
10
+
11
+
12
+ class MetadataAsyncInterceptor(ClientAsyncInterceptor):
13
+ metadata: Metadata
14
+
15
+ """
16
+ Interceptor to add metadata to all async unary and streaming RPCs
17
+ """
18
+
19
+ def __init__(self, metadata: Metadata):
20
+ self.metadata = metadata
21
+
22
+ async def intercept(
23
+ self,
24
+ method: Callable,
25
+ request_or_iterator: Any,
26
+ client_call_details: grpc_aio.ClientCallDetails,
27
+ ):
28
+ call_details = cast(grpc_aio.ClientCallDetails, client_call_details)
29
+ new_details = grpc_aio.ClientCallDetails(
30
+ call_details.method,
31
+ call_details.timeout,
32
+ self.metadata,
33
+ call_details.credentials,
34
+ call_details.wait_for_ready,
35
+ )
36
+ return await method(request_or_iterator, new_details)
File without changes
@@ -0,0 +1,61 @@
1
+ from abc import abstractmethod
2
+ from typing import Any, Callable, Iterator
3
+
4
+ import grpc
5
+
6
+ Continuation = Callable[[grpc.ClientCallDetails, Any], Any]
7
+
8
+
9
+ class ClientInterceptor(
10
+ grpc.StreamStreamClientInterceptor,
11
+ grpc.StreamUnaryClientInterceptor,
12
+ grpc.UnaryStreamClientInterceptor,
13
+ grpc.UnaryUnaryClientInterceptor,
14
+ ):
15
+ @abstractmethod
16
+ def intercept(
17
+ self,
18
+ method: Continuation,
19
+ request_or_iterator: Any,
20
+ client_call_details: grpc.ClientCallDetails,
21
+ ):
22
+ pass
23
+
24
+ def intercept_unary_unary(
25
+ self,
26
+ continuation: Continuation,
27
+ client_call_details: grpc.ClientCallDetails,
28
+ request: Any,
29
+ ):
30
+ return self.intercept(_swap_args(continuation), request, client_call_details)
31
+
32
+ def intercept_stream_unary(
33
+ self,
34
+ continuation: Continuation,
35
+ client_call_details: grpc.ClientCallDetails,
36
+ request_iterator: Iterator[Any],
37
+ ):
38
+ return self.intercept(_swap_args(continuation), request_iterator, client_call_details)
39
+
40
+ def intercept_unary_stream(
41
+ self,
42
+ continuation: Continuation,
43
+ client_call_details: grpc.ClientCallDetails,
44
+ request: Any,
45
+ ):
46
+ return self.intercept(_swap_args(continuation), request, client_call_details)
47
+
48
+ def intercept_stream_stream(
49
+ self,
50
+ continuation: Continuation,
51
+ client_call_details: grpc.ClientCallDetails,
52
+ request_iterator: Iterator[Any],
53
+ ):
54
+ return self.intercept(_swap_args(continuation), request_iterator, client_call_details)
55
+
56
+
57
+ def _swap_args(fn: Callable[[Any, Any], Any]) -> Callable[[Any, Any], Any]:
58
+ def new_fn(x, y):
59
+ return fn(y, x)
60
+
61
+ return new_fn
@@ -0,0 +1,25 @@
1
+ from typing import Optional, Sequence, Tuple, Union
2
+
3
+ import grpc
4
+
5
+
6
+ class ClientCallDetails(grpc.ClientCallDetails):
7
+ method: str
8
+ timeout: Optional[float]
9
+ metadata: Optional[Sequence[Tuple[str, Union[str, bytes]]]]
10
+ credentials: Optional[grpc.CallCredentials]
11
+ wait_for_ready: Optional[bool]
12
+
13
+ def __init__(
14
+ self,
15
+ method: str,
16
+ timeout: Optional[float],
17
+ metadata: Optional[Sequence[Tuple[str, Union[str, bytes]]]],
18
+ credentials: Optional[grpc.CallCredentials],
19
+ wait_for_ready: Optional[bool],
20
+ ):
21
+ self.method = method
22
+ self.timeout = timeout
23
+ self.metadata = metadata
24
+ self.credentials = credentials
25
+ self.wait_for_ready = wait_for_ready
@@ -0,0 +1,33 @@
1
+ from typing import Any, List, Tuple, cast
2
+
3
+ import grpc
4
+
5
+ from sift_py.grpc._interceptors.base import ClientInterceptor, Continuation
6
+ from sift_py.grpc._interceptors.context import ClientCallDetails
7
+
8
+ Metadata = List[Tuple[str, str]]
9
+
10
+
11
+ class MetadataInterceptor(ClientInterceptor):
12
+ metadata: Metadata
13
+
14
+ def __init__(self, metadata: Metadata):
15
+ self.metadata = metadata
16
+
17
+ def intercept(
18
+ self,
19
+ method: Continuation,
20
+ request_or_iterator: Any,
21
+ client_call_details: grpc.ClientCallDetails,
22
+ ):
23
+ details = cast(ClientCallDetails, client_call_details)
24
+
25
+ new_details = ClientCallDetails(
26
+ method=details.method,
27
+ timeout=details.timeout,
28
+ credentials=details.credentials,
29
+ wait_for_ready=details.wait_for_ready,
30
+ metadata=self.metadata,
31
+ )
32
+
33
+ return method(request_or_iterator, new_details)