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
sift_py/grpc/_retry.py ADDED
@@ -0,0 +1,70 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from typing import Dict, List, TypedDict
5
+
6
+ from grpc import StatusCode
7
+ from typing_extensions import Self
8
+
9
+
10
+ class RetryPolicy:
11
+ """
12
+ Retry policy meant to be used for `sift_py.grpc.transport.SiftChannel`. Users may have the ability to configure their own
13
+ custom retry policy in the future, but for now this is primarily intended for internal use.
14
+
15
+ - [Retry policy schema](https://github.com/grpc/grpc-proto/blob/ec30f589e2519d595688b9a42f88a91bdd6b733f/grpc/service_config/service_config.proto#L136)
16
+ - [Enable gRPC retry option](https://github.com/grpc/grpc/blob/9a5fdfc3d3a7fc575a394360be4532ee09a85620/include/grpc/impl/channel_arg_names.h#L311)
17
+ - [Service config option](https://github.com/grpc/grpc/blob/9a5fdfc3d3a7fc575a394360be4532ee09a85620/include/grpc/impl/channel_arg_names.h#L207)
18
+ """
19
+
20
+ config: RetryConfig
21
+
22
+ DEFAULT_POLICY: RetryConfig = {
23
+ "methodConfig": [
24
+ {
25
+ # We can configure this on a per-service and RPC basis but for now we'll
26
+ # apply this across all services and RPCs.
27
+ "name": [{}],
28
+ "retryPolicy": {
29
+ # gRPC does not allow more than 5 attempts
30
+ "maxAttempts": 5,
31
+ "initialBackoff": "0.05s",
32
+ "maxBackoff": "5s",
33
+ "backoffMultiplier": 4,
34
+ "retryableStatusCodes": [
35
+ StatusCode.UNKNOWN.name,
36
+ StatusCode.UNAVAILABLE.name,
37
+ StatusCode.ABORTED.name,
38
+ StatusCode.DEADLINE_EXCEEDED.name,
39
+ ],
40
+ },
41
+ }
42
+ ]
43
+ }
44
+
45
+ def __init__(self, config: RetryConfig):
46
+ self.config = config
47
+
48
+ def as_json(self) -> str:
49
+ return json.dumps(self.config)
50
+
51
+ @classmethod
52
+ def default(cls) -> Self:
53
+ return cls(config=cls.DEFAULT_POLICY)
54
+
55
+
56
+ class RetryConfig(TypedDict):
57
+ methodConfig: List[MethodConfigDict]
58
+
59
+
60
+ class MethodConfigDict(TypedDict):
61
+ name: List[Dict[str, str]]
62
+ retryPolicy: RetryConfigDict
63
+
64
+
65
+ class RetryConfigDict(TypedDict):
66
+ maxAttempts: int
67
+ initialBackoff: str
68
+ maxBackoff: str
69
+ backoffMultiplier: int
70
+ retryableStatusCodes: List[str]
@@ -0,0 +1,34 @@
1
+ from typing import TypedDict
2
+
3
+ DEFAULT_KEEPALIVE_TIME_MS = 20_000
4
+ """Interval with which to send keepalive pings"""
5
+
6
+ DEFAULT_KEEPALIVE_TIMEOUT_MS = 20_000
7
+ """Timeout while waiting for server to acknowledge keepalive ping"""
8
+
9
+ DEFAULT_KEEPALIVE_PERMIT_WITHOUT_CALLS = 1
10
+ """Allows connection without any active RPCs"""
11
+
12
+ DEFAULT_MAX_PINGS_WITHOUT_DATA = 0
13
+ """Disabled"""
14
+
15
+
16
+ # https://github.com/grpc/grpc/blob/master/doc/keepalive.md
17
+ class KeepaliveConfig(TypedDict):
18
+ """
19
+ Make make this public in the future to allow folks to configure their own keepalive settings
20
+ if there is demand for it.
21
+ """
22
+
23
+ keepalive_time_ms: int
24
+ keepalive_timeout_ms: int
25
+ keepalive_permit_without_calls: int
26
+ max_pings_without_data: int
27
+
28
+
29
+ DEFAULT_KEEPALIVE_CONFIG: KeepaliveConfig = {
30
+ "keepalive_time_ms": DEFAULT_KEEPALIVE_TIME_MS,
31
+ "keepalive_timeout_ms": DEFAULT_KEEPALIVE_TIMEOUT_MS,
32
+ "keepalive_permit_without_calls": DEFAULT_KEEPALIVE_PERMIT_WITHOUT_CALLS,
33
+ "max_pings_without_data": DEFAULT_MAX_PINGS_WITHOUT_DATA,
34
+ }
@@ -0,0 +1,250 @@
1
+ """
2
+ This module is concerned with creating a gRPC transport channel specifically for
3
+ interacting with Sift's gRPC API. the `use_sift_channel` method creates said channel
4
+ and should generally be used within a with-block for correct resource management.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from importlib.metadata import PackageNotFoundError, version
10
+ from typing import Any, Dict, List, Optional, Tuple, TypedDict, Union, cast
11
+ from urllib.parse import ParseResult, urlparse
12
+
13
+ import grpc
14
+ import grpc.aio as grpc_aio
15
+ from typing_extensions import NotRequired, TypeAlias
16
+
17
+ from sift_py.grpc._async_interceptors.base import ClientAsyncInterceptor
18
+ from sift_py.grpc._async_interceptors.metadata import MetadataAsyncInterceptor
19
+ from sift_py.grpc._interceptors.base import ClientInterceptor
20
+ from sift_py.grpc._interceptors.metadata import Metadata, MetadataInterceptor
21
+ from sift_py.grpc._retry import RetryPolicy
22
+ from sift_py.grpc.keepalive import DEFAULT_KEEPALIVE_CONFIG, KeepaliveConfig
23
+
24
+ SiftChannel: TypeAlias = grpc.Channel
25
+ SiftAsyncChannel: TypeAlias = grpc_aio.Channel
26
+
27
+
28
+ def get_ssl_credentials(cert_via_openssl: bool) -> grpc.ChannelCredentials:
29
+ """
30
+ Returns SSL credentials for use with gRPC.
31
+ Workaround for this issue: https://github.com/grpc/grpc/issues/29682
32
+ """
33
+ if not cert_via_openssl:
34
+ return grpc.ssl_channel_credentials()
35
+
36
+ try:
37
+ import ssl
38
+
39
+ from OpenSSL import crypto
40
+
41
+ ssl_context = ssl.create_default_context()
42
+ certs_der = ssl_context.get_ca_certs(binary_form=True)
43
+ certs_x509 = [crypto.load_certificate(crypto.FILETYPE_ASN1, x) for x in certs_der]
44
+ certs_pem = [crypto.dump_certificate(crypto.FILETYPE_PEM, x) for x in certs_x509]
45
+ certs_bytes = b"".join(certs_pem)
46
+
47
+ return grpc.ssl_channel_credentials(certs_bytes)
48
+ except ImportError as e:
49
+ raise Exception(
50
+ "Missing required dependencies for cert_via_openssl. Run `pip install sift-stack-py[openssl]` to install the required dependencies."
51
+ ) from e
52
+
53
+
54
+ def use_sift_channel(
55
+ config: SiftChannelConfig, metadata: Optional[Dict[str, Any]] = None
56
+ ) -> SiftChannel:
57
+ """
58
+ Returns an intercepted channel that is meant to be used across all services that
59
+ make RPCs to Sift's API. It is highly encouraged to use this within a with-block
60
+ for correct resource clean-up.
61
+
62
+ Should an RPC fail for a reason that isn't explicitly controlled by Sift, `SiftChannel`
63
+ will automatically leverage gRPC's retry mechanism to try and recover until the max-attempts
64
+ are exceeded, after which the underlying exception will be raised.
65
+ """
66
+ use_ssl = config.get("use_ssl", True)
67
+ cert_via_openssl = config.get("cert_via_openssl", False)
68
+
69
+ if not use_ssl:
70
+ return _use_insecure_sift_channel(config, metadata)
71
+
72
+ credentials = get_ssl_credentials(cert_via_openssl)
73
+ options = _compute_channel_options(config)
74
+ api_uri = _clean_uri(config["uri"], use_ssl)
75
+ channel = grpc.secure_channel(api_uri, credentials, options)
76
+ interceptors = _compute_sift_interceptors(config, metadata)
77
+ return grpc.intercept_channel(channel, *interceptors)
78
+
79
+
80
+ def use_sift_async_channel(
81
+ config: SiftChannelConfig, metadata: Optional[Dict[str, Any]] = None
82
+ ) -> SiftAsyncChannel:
83
+ """
84
+ Like `use_sift_channel` but returns a channel meant to be used within the context
85
+ of an async runtime when asynchonous I/O is required.
86
+ """
87
+ use_ssl = config.get("use_ssl", True)
88
+ cert_via_openssl = config.get("cert_via_openssl", False)
89
+
90
+ if not use_ssl:
91
+ return _use_insecure_sift_async_channel(config, metadata)
92
+
93
+ return grpc_aio.secure_channel(
94
+ target=_clean_uri(config["uri"], use_ssl),
95
+ credentials=get_ssl_credentials(cert_via_openssl),
96
+ options=_compute_channel_options(config),
97
+ interceptors=_compute_sift_async_interceptors(config, metadata),
98
+ )
99
+
100
+
101
+ def _use_insecure_sift_channel(
102
+ config: SiftChannelConfig, metadata: Optional[Dict[str, Any]] = None
103
+ ) -> SiftChannel:
104
+ """
105
+ FOR DEVELOPMENT PURPOSES ONLY
106
+ """
107
+ options = _compute_channel_options(config)
108
+ api_uri = _clean_uri(config["uri"], False)
109
+ channel = grpc.insecure_channel(api_uri, options)
110
+ interceptors = _compute_sift_interceptors(config, metadata)
111
+ return grpc.intercept_channel(channel, *interceptors)
112
+
113
+
114
+ def _use_insecure_sift_async_channel(
115
+ config: SiftChannelConfig, metadata: Optional[Dict[str, Any]] = None
116
+ ) -> SiftAsyncChannel:
117
+ """
118
+ FOR DEVELOPMENT PURPOSES ONLY
119
+ """
120
+ return grpc_aio.insecure_channel(
121
+ target=config["uri"],
122
+ options=_compute_channel_options(config),
123
+ interceptors=_compute_sift_async_interceptors(config, metadata),
124
+ )
125
+
126
+
127
+ def _compute_sift_interceptors(
128
+ config: SiftChannelConfig, metadata: Optional[Dict[str, Any]] = None
129
+ ) -> List[ClientInterceptor]:
130
+ """
131
+ Initialized all interceptors here.
132
+ """
133
+ return [
134
+ _metadata_interceptor(config, metadata),
135
+ ]
136
+
137
+
138
+ def _compute_sift_async_interceptors(
139
+ config: SiftChannelConfig, metadata: Optional[Dict[str, Any]] = None
140
+ ) -> List[grpc_aio.ClientInterceptor]:
141
+ return [
142
+ _metadata_async_interceptor(config, metadata),
143
+ ]
144
+
145
+
146
+ def _compute_channel_options(opts: SiftChannelConfig) -> List[Tuple[str, Any]]:
147
+ """
148
+ Initialize all [channel options](https://github.com/grpc/grpc/blob/v1.64.x/include/grpc/impl/channel_arg_names.h) here.
149
+ """
150
+
151
+ options = [
152
+ ("grpc.enable_retries", 1),
153
+ ("grpc.service_config", RetryPolicy.default().as_json()),
154
+ # Primary cannot be overriden:
155
+ # https://github.com/grpc/grpc/blob/0498194240f55d7f4b12633ad01339fb690621bf/src/core/ext/filters/http/client/http_client_filter.cc#L97
156
+ ("grpc.secondary_user_agent", _compute_user_agent()),
157
+ ]
158
+
159
+ enable_keepalive = opts.get("enable_keepalive", True)
160
+ if isinstance(enable_keepalive, dict):
161
+ config = cast(KeepaliveConfig, enable_keepalive)
162
+ options.extend(_compute_keep_alive_channel_opts(config))
163
+ elif enable_keepalive:
164
+ options.extend(_compute_keep_alive_channel_opts(DEFAULT_KEEPALIVE_CONFIG))
165
+
166
+ return options
167
+
168
+
169
+ def _metadata_interceptor(
170
+ config: SiftChannelConfig, metadata: Optional[Dict[str, Any]] = None
171
+ ) -> ClientInterceptor:
172
+ """
173
+ Any new metadata goes here.
174
+ """
175
+ apikey = config["apikey"]
176
+ md: Metadata = [("authorization", f"Bearer {apikey}")]
177
+
178
+ if metadata:
179
+ for key, val in metadata.items():
180
+ md.append((key, val))
181
+
182
+ return MetadataInterceptor(md)
183
+
184
+
185
+ def _metadata_async_interceptor(
186
+ config: SiftChannelConfig, metadata: Optional[Dict[str, Any]] = None
187
+ ) -> ClientAsyncInterceptor:
188
+ """
189
+ Any new metadata goes here for unary-unary calls.
190
+ """
191
+ apikey = config["apikey"]
192
+ md: Metadata = [("authorization", f"Bearer {apikey}")]
193
+
194
+ if metadata:
195
+ for key, val in metadata.items():
196
+ md.append((key, val))
197
+
198
+ return MetadataAsyncInterceptor(md)
199
+
200
+
201
+ def _clean_uri(uri: str, use_ssl: bool) -> str:
202
+ """
203
+ This will automatically transform the URI to an acceptable form regardless of whether or not
204
+ users included the scheme in the URL or included trailing slashes.
205
+ """
206
+
207
+ if "http://" in uri or "https://" in uri:
208
+ parsed: ParseResult = urlparse(uri)
209
+ return parsed.netloc
210
+
211
+ full_uri = f"https://{uri}" if use_ssl else f"http://{uri}"
212
+ parsed_res: ParseResult = urlparse(full_uri)
213
+ return parsed_res.netloc
214
+
215
+
216
+ def _compute_user_agent() -> str:
217
+ try:
218
+ return f"sift_stack_py/{version('sift_stack_py')}"
219
+ except PackageNotFoundError:
220
+ return "sift-stack-py"
221
+
222
+
223
+ def _compute_keep_alive_channel_opts(config: KeepaliveConfig) -> List[Tuple[str, int]]:
224
+ return [
225
+ ("grpc.keepalive_time_ms", config["keepalive_time_ms"]),
226
+ ("grpc.keepalive_timeout_ms", config["keepalive_timeout_ms"]),
227
+ ("grpc.http2.max_pings_without_data", config["max_pings_without_data"]),
228
+ ("grpc.keepalive_permit_without_calls", config["keepalive_permit_without_calls"]),
229
+ ]
230
+
231
+
232
+ class SiftChannelConfig(TypedDict):
233
+ """
234
+ Config class used to instantiate a `SiftChannel` via `use_sift_channel`.
235
+ - `uri`: The URI of Sift's gRPC API. The scheme portion of the URI i.e. `https://` should be ommitted.
236
+ - `apikey`: User-generated API key generated via the Sift application.
237
+ - `enable_keepalive`: Enabled by default, but can be disabled by passing in `False`. HTTP/2 keep-alive prevents connections from
238
+ being terminated during idle periods. A custom `sift_py.grpc.keepalive.KeepaliveConfig` may also be provided.
239
+ - `use_ssl`: INTERNAL USE. Meant to be used for local development.
240
+ - `cert_via_openssl`: Enable this if you want to use OpenSSL to load the certificates.
241
+ Run `pip install sift-stack-py[openssl]` to install the dependencies required to use this option.
242
+ This works around this issue with grpc loading SSL certificates: https://github.com/grpc/grpc/issues/29682.
243
+ Default is False.
244
+ """
245
+
246
+ uri: str
247
+ apikey: str
248
+ enable_keepalive: NotRequired[Union[bool, KeepaliveConfig]]
249
+ use_ssl: NotRequired[bool]
250
+ cert_via_openssl: NotRequired[bool]
@@ -0,0 +1,170 @@
1
+ # ruff: noqa: N802
2
+
3
+ import re
4
+ from concurrent import futures
5
+ from contextlib import contextmanager
6
+ from typing import Any, Callable, Iterator, cast
7
+
8
+ import grpc
9
+ import pytest
10
+ from pytest_mock import MockFixture, MockType
11
+ from sift.data.v1.data_pb2 import GetDataRequest, GetDataResponse
12
+ from sift.data.v1.data_pb2_grpc import (
13
+ DataServiceServicer,
14
+ DataServiceStub,
15
+ add_DataServiceServicer_to_server,
16
+ )
17
+
18
+ from sift_py._internal.test_util.server_interceptor import ServerInterceptor
19
+ from sift_py.grpc.transport import SiftChannelConfig, use_sift_channel
20
+
21
+
22
+ class DataService(DataServiceServicer):
23
+ def GetData(self, request: GetDataRequest, context: grpc.ServicerContext):
24
+ return GetDataResponse(next_page_token="next-page-token")
25
+
26
+
27
+ class AuthInterceptor(ServerInterceptor):
28
+ AUTH_REGEX = re.compile(r"^Bearer (.+)$")
29
+
30
+ def intercept(
31
+ self,
32
+ method: Callable,
33
+ request_or_iterator: Any,
34
+ context: grpc.ServicerContext,
35
+ method_name: str,
36
+ ) -> Any:
37
+ authenticated = False
38
+ for metadata in context.invocation_metadata():
39
+ if metadata.key == "authorization":
40
+ auth = self.__class__.AUTH_REGEX.match(metadata.value)
41
+
42
+ if auth is not None and len(auth.group(1)) > 0:
43
+ authenticated = True
44
+
45
+ break
46
+
47
+ if authenticated:
48
+ return method(request_or_iterator, context)
49
+ else:
50
+ context.set_code(grpc.StatusCode.UNAUTHENTICATED)
51
+ context.set_details("Invalid or missing API key")
52
+ raise
53
+
54
+
55
+ class ForceFailInterceptor(ServerInterceptor):
56
+ """
57
+ Force RPC to fail a few times before letting it pass.
58
+
59
+ `failed_attempts`: Count of how many times failed
60
+ `expected_num_fails`: How many times you want call to fail
61
+ """
62
+
63
+ failed_attempts: int
64
+ expected_num_fails: int
65
+
66
+ def __init__(self, expected_num_fails: int):
67
+ self.expected_num_fails = expected_num_fails
68
+ self.failed_attempts = 0
69
+ super().__init__()
70
+
71
+ def intercept(
72
+ self,
73
+ method: Callable,
74
+ request_or_iterator: Any,
75
+ context: grpc.ServicerContext,
76
+ method_name: str,
77
+ ) -> Any:
78
+ if self.failed_attempts < self.expected_num_fails:
79
+ self.failed_attempts += 1
80
+ context.set_code(grpc.StatusCode.UNKNOWN)
81
+ context.set_details("something unknown happened")
82
+ raise
83
+
84
+ return method(request_or_iterator, context)
85
+
86
+
87
+ def test_sift_channel(mocker: MockFixture):
88
+ @contextmanager
89
+ def test_server_spy(*interceptors: ServerInterceptor) -> Iterator[MockType]:
90
+ server = grpc.server(
91
+ thread_pool=futures.ThreadPoolExecutor(max_workers=1), interceptors=list(interceptors)
92
+ )
93
+
94
+ data_service = DataService()
95
+ spy = mocker.spy(data_service, "GetData")
96
+
97
+ add_DataServiceServicer_to_server(data_service, server)
98
+ server.add_insecure_port("[::]:50052")
99
+ server.start()
100
+ try:
101
+ yield spy
102
+ finally:
103
+ server.stop(None)
104
+ server.wait_for_termination()
105
+
106
+ with test_server_spy(AuthInterceptor()) as get_data_spy:
107
+ sift_channel_config_a: SiftChannelConfig = {
108
+ "uri": "localhost:50052",
109
+ "apikey": "",
110
+ "use_ssl": False,
111
+ }
112
+
113
+ with use_sift_channel(sift_channel_config_a) as channel:
114
+ with pytest.raises(grpc.RpcError, match="UNAUTHENTICATED"):
115
+ stub = DataServiceStub(channel)
116
+ _ = cast(GetDataResponse, stub.GetData(GetDataRequest()))
117
+
118
+ get_data_spy.assert_not_called()
119
+
120
+ sift_channel_config_b: SiftChannelConfig = {
121
+ "uri": "localhost:50052",
122
+ "apikey": "some-token",
123
+ "use_ssl": False,
124
+ }
125
+
126
+ with use_sift_channel(sift_channel_config_b) as channel:
127
+ stub = DataServiceStub(channel)
128
+ res = cast(GetDataResponse, stub.GetData(GetDataRequest()))
129
+ assert res.next_page_token == "next-page-token"
130
+ get_data_spy.assert_called_once()
131
+
132
+ force_fail_interceptor = ForceFailInterceptor(4)
133
+ with test_server_spy(AuthInterceptor(), force_fail_interceptor) as get_data_spy:
134
+ sift_channel_config_c: SiftChannelConfig = {
135
+ "uri": "localhost:50052",
136
+ "apikey": "some-token",
137
+ "use_ssl": False,
138
+ }
139
+
140
+ with use_sift_channel(sift_channel_config_c) as channel:
141
+ stub = DataServiceStub(channel)
142
+ # This will attempt 5 times: fail 4 times, succeed on 5th
143
+ res = cast(GetDataResponse, stub.GetData(GetDataRequest()))
144
+ assert res.next_page_token == "next-page-token"
145
+ get_data_spy.assert_called_once()
146
+
147
+ # fail 4 times, pass the 5th attempt
148
+ assert force_fail_interceptor.failed_attempts == 4
149
+
150
+ # Now we're going to fail beyond the max retry attempts
151
+
152
+ force_fail_interceptor_max = ForceFailInterceptor(7)
153
+ with test_server_spy(AuthInterceptor(), force_fail_interceptor_max) as get_data_spy:
154
+ sift_channel_config_d: SiftChannelConfig = {
155
+ "uri": "localhost:50052",
156
+ "apikey": "some-token",
157
+ "use_ssl": False,
158
+ }
159
+
160
+ with use_sift_channel(sift_channel_config_d) as channel:
161
+ stub = DataServiceStub(channel)
162
+
163
+ # This will go beyond the max number of attempts
164
+ with pytest.raises(Exception):
165
+ res = cast(GetDataResponse, stub.GetData(GetDataRequest()))
166
+
167
+ get_data_spy.assert_not_called()
168
+
169
+ # All attempts failed
170
+ assert force_fail_interceptor_max.failed_attempts == 5
@@ -0,0 +1,6 @@
1
+ """
2
+ Module containing components necessary to leverage Sift's data ingestion and rule evaluation API.
3
+ The fundamental components of this module are the following:
4
+ - `sift_py.ingestion.config.telemetry.TelemetryConfig`
5
+ - `sift_py.ingestion.service.IngestionService`
6
+ """
@@ -0,0 +1,6 @@
1
+ """
2
+ INTERNAL MODULE
3
+
4
+ This module contains implementation details that aren't meant to be used directly.
5
+ APIs in this module are garaunteed to not be stable so proceed at your own risk.
6
+ """
@@ -0,0 +1,12 @@
1
+ from sift.rules.v1.rules_pb2 import ChannelReference
2
+
3
+
4
+ def channel_reference_from_fqn(fqn: str) -> ChannelReference:
5
+ parts = fqn.split(".")
6
+
7
+ if len(parts) == 1:
8
+ return ChannelReference(name=parts[0])
9
+
10
+ component_parts = parts[: len(parts) - 1]
11
+
12
+ return ChannelReference(name=parts[-1], component=".".join(component_parts))
@@ -0,0 +1,10 @@
1
+ class IngestionValidationError(Exception):
2
+ """
3
+ Errors that can occur while initializing the ingestion service
4
+ or when creating ingestion requests.
5
+ """
6
+
7
+ message: str
8
+
9
+ def __init__(self, message: str):
10
+ super().__init__(message)