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.
- google/__init__.py +1 -0
- google/api/__init__.py +0 -0
- google/api/annotations_pb2.py +27 -0
- google/api/annotations_pb2.pyi +29 -0
- google/api/annotations_pb2_grpc.py +4 -0
- google/api/annotations_pb2_grpc.pyi +30 -0
- google/api/field_behavior_pb2.py +30 -0
- google/api/field_behavior_pb2.pyi +175 -0
- google/api/field_behavior_pb2_grpc.py +4 -0
- google/api/field_behavior_pb2_grpc.pyi +30 -0
- google/api/http_pb2.py +31 -0
- google/api/http_pb2.pyi +433 -0
- google/api/http_pb2_grpc.py +4 -0
- google/api/http_pb2_grpc.pyi +30 -0
- protoc_gen_openapiv2/__init__.py +0 -0
- protoc_gen_openapiv2/options/__init__.py +0 -0
- protoc_gen_openapiv2/options/annotations_pb2.py +27 -0
- protoc_gen_openapiv2/options/annotations_pb2.pyi +48 -0
- protoc_gen_openapiv2/options/annotations_pb2_grpc.py +4 -0
- protoc_gen_openapiv2/options/annotations_pb2_grpc.pyi +17 -0
- protoc_gen_openapiv2/options/openapiv2_pb2.py +132 -0
- protoc_gen_openapiv2/options/openapiv2_pb2.pyi +1533 -0
- protoc_gen_openapiv2/options/openapiv2_pb2_grpc.py +4 -0
- protoc_gen_openapiv2/options/openapiv2_pb2_grpc.pyi +17 -0
- sift/__init__.py +0 -0
- sift/annotation_logs/__init__.py +0 -0
- sift/annotation_logs/v1/__init__.py +0 -0
- sift/annotation_logs/v1/annotation_logs_pb2.py +115 -0
- sift/annotation_logs/v1/annotation_logs_pb2.pyi +370 -0
- sift/annotation_logs/v1/annotation_logs_pb2_grpc.py +135 -0
- sift/annotation_logs/v1/annotation_logs_pb2_grpc.pyi +84 -0
- sift/annotations/__init__.py +0 -0
- sift/annotations/v1/__init__.py +0 -0
- sift/annotations/v1/annotations_pb2.py +180 -0
- sift/annotations/v1/annotations_pb2.pyi +539 -0
- sift/annotations/v1/annotations_pb2_grpc.py +237 -0
- sift/annotations/v1/annotations_pb2_grpc.pyi +144 -0
- sift/assets/__init__.py +0 -0
- sift/assets/v1/__init__.py +0 -0
- sift/assets/v1/assets_pb2.py +90 -0
- sift/assets/v1/assets_pb2.pyi +235 -0
- sift/assets/v1/assets_pb2_grpc.py +168 -0
- sift/assets/v1/assets_pb2_grpc.pyi +101 -0
- sift/calculated_channels/__init__.py +0 -0
- sift/calculated_channels/v1/__init__.py +0 -0
- sift/calculated_channels/v1/calculated_channels_pb2.py +99 -0
- sift/calculated_channels/v1/calculated_channels_pb2.pyi +280 -0
- sift/calculated_channels/v1/calculated_channels_pb2_grpc.py +101 -0
- sift/calculated_channels/v1/calculated_channels_pb2_grpc.pyi +64 -0
- sift/campaigns/__init__.py +0 -0
- sift/campaigns/v1/__init__.py +0 -0
- sift/campaigns/v1/campaigns_pb2.py +144 -0
- sift/campaigns/v1/campaigns_pb2.pyi +383 -0
- sift/campaigns/v1/campaigns_pb2_grpc.py +169 -0
- sift/campaigns/v1/campaigns_pb2_grpc.pyi +104 -0
- sift/channel_schemas/__init__.py +0 -0
- sift/channel_schemas/v1/__init__.py +0 -0
- sift/channel_schemas/v1/channel_schemas_pb2.py +69 -0
- sift/channel_schemas/v1/channel_schemas_pb2.pyi +117 -0
- sift/channel_schemas/v1/channel_schemas_pb2_grpc.py +101 -0
- sift/channel_schemas/v1/channel_schemas_pb2_grpc.pyi +64 -0
- sift/channels/__init__.py +0 -0
- sift/channels/v2/__init__.py +0 -0
- sift/channels/v2/channels_pb2.py +88 -0
- sift/channels/v2/channels_pb2.pyi +183 -0
- sift/channels/v2/channels_pb2_grpc.py +101 -0
- sift/channels/v2/channels_pb2_grpc.pyi +64 -0
- sift/common/__init__.py +0 -0
- sift/common/type/__init__.py +0 -0
- sift/common/type/v1/__init__.py +0 -0
- sift/common/type/v1/channel_bit_field_element_pb2.py +34 -0
- sift/common/type/v1/channel_bit_field_element_pb2.pyi +33 -0
- sift/common/type/v1/channel_bit_field_element_pb2_grpc.py +4 -0
- sift/common/type/v1/channel_bit_field_element_pb2_grpc.pyi +17 -0
- sift/common/type/v1/channel_data_type_pb2.py +29 -0
- sift/common/type/v1/channel_data_type_pb2.pyi +50 -0
- sift/common/type/v1/channel_data_type_pb2_grpc.py +4 -0
- sift/common/type/v1/channel_data_type_pb2_grpc.pyi +17 -0
- sift/common/type/v1/channel_enum_type_pb2.py +32 -0
- sift/common/type/v1/channel_enum_type_pb2.pyi +29 -0
- sift/common/type/v1/channel_enum_type_pb2_grpc.py +4 -0
- sift/common/type/v1/channel_enum_type_pb2_grpc.pyi +17 -0
- sift/common/type/v1/organization_pb2.py +27 -0
- sift/common/type/v1/organization_pb2.pyi +29 -0
- sift/common/type/v1/organization_pb2_grpc.py +4 -0
- sift/common/type/v1/organization_pb2_grpc.pyi +17 -0
- sift/common/type/v1/resource_identifier_pb2.py +46 -0
- sift/common/type/v1/resource_identifier_pb2.pyi +145 -0
- sift/common/type/v1/resource_identifier_pb2_grpc.py +4 -0
- sift/common/type/v1/resource_identifier_pb2_grpc.pyi +17 -0
- sift/common/type/v1/user_pb2.py +33 -0
- sift/common/type/v1/user_pb2.pyi +36 -0
- sift/common/type/v1/user_pb2_grpc.py +4 -0
- sift/common/type/v1/user_pb2_grpc.pyi +17 -0
- sift/data/__init__.py +0 -0
- sift/data/v1/__init__.py +0 -0
- sift/data/v1/data_pb2.py +212 -0
- sift/data/v1/data_pb2.pyi +745 -0
- sift/data/v1/data_pb2_grpc.py +67 -0
- sift/data/v1/data_pb2_grpc.pyi +44 -0
- sift/ingest/__init__.py +0 -0
- sift/ingest/v1/__init__.py +0 -0
- sift/ingest/v1/ingest_pb2.py +35 -0
- sift/ingest/v1/ingest_pb2.pyi +118 -0
- sift/ingest/v1/ingest_pb2_grpc.py +66 -0
- sift/ingest/v1/ingest_pb2_grpc.pyi +41 -0
- sift/ingestion_configs/__init__.py +0 -0
- sift/ingestion_configs/v1/__init__.py +0 -0
- sift/ingestion_configs/v1/ingestion_configs_pb2.py +115 -0
- sift/ingestion_configs/v1/ingestion_configs_pb2.pyi +332 -0
- sift/ingestion_configs/v1/ingestion_configs_pb2_grpc.py +203 -0
- sift/ingestion_configs/v1/ingestion_configs_pb2_grpc.pyi +124 -0
- sift/notifications/__init__.py +0 -0
- sift/notifications/v1/__init__.py +0 -0
- sift/notifications/v1/notifications_pb2.py +64 -0
- sift/notifications/v1/notifications_pb2.pyi +225 -0
- sift/notifications/v1/notifications_pb2_grpc.py +101 -0
- sift/notifications/v1/notifications_pb2_grpc.pyi +64 -0
- sift/ping/__init__.py +0 -0
- sift/ping/v1/__init__.py +0 -0
- sift/ping/v1/ping_pb2.py +38 -0
- sift/ping/v1/ping_pb2.pyi +36 -0
- sift/ping/v1/ping_pb2_grpc.py +66 -0
- sift/ping/v1/ping_pb2_grpc.pyi +41 -0
- sift/remote_files/__init__.py +0 -0
- sift/remote_files/v1/__init__.py +0 -0
- sift/remote_files/v1/remote_files_pb2.py +174 -0
- sift/remote_files/v1/remote_files_pb2.pyi +472 -0
- sift/remote_files/v1/remote_files_pb2_grpc.py +271 -0
- sift/remote_files/v1/remote_files_pb2_grpc.pyi +164 -0
- sift/report_templates/__init__.py +0 -0
- sift/report_templates/v1/__init__.py +0 -0
- sift/report_templates/v1/report_templates_pb2.py +146 -0
- sift/report_templates/v1/report_templates_pb2.pyi +381 -0
- sift/report_templates/v1/report_templates_pb2_grpc.py +169 -0
- sift/report_templates/v1/report_templates_pb2_grpc.pyi +104 -0
- sift/reports/__init__.py +0 -0
- sift/reports/v1/__init__.py +0 -0
- sift/reports/v1/reports_pb2.py +193 -0
- sift/reports/v1/reports_pb2.pyi +562 -0
- sift/reports/v1/reports_pb2_grpc.py +205 -0
- sift/reports/v1/reports_pb2_grpc.pyi +136 -0
- sift/rule_evaluation/__init__.py +0 -0
- sift/rule_evaluation/v1/__init__.py +0 -0
- sift/rule_evaluation/v1/rule_evaluation_pb2.py +89 -0
- sift/rule_evaluation/v1/rule_evaluation_pb2.pyi +263 -0
- sift/rule_evaluation/v1/rule_evaluation_pb2_grpc.py +101 -0
- sift/rule_evaluation/v1/rule_evaluation_pb2_grpc.pyi +64 -0
- sift/rules/__init__.py +0 -0
- sift/rules/v1/__init__.py +0 -0
- sift/rules/v1/rules_pb2.py +420 -0
- sift/rules/v1/rules_pb2.pyi +1355 -0
- sift/rules/v1/rules_pb2_grpc.py +577 -0
- sift/rules/v1/rules_pb2_grpc.pyi +351 -0
- sift/runs/__init__.py +0 -0
- sift/runs/v2/__init__.py +0 -0
- sift/runs/v2/runs_pb2.py +150 -0
- sift/runs/v2/runs_pb2.pyi +413 -0
- sift/runs/v2/runs_pb2_grpc.py +271 -0
- sift/runs/v2/runs_pb2_grpc.pyi +164 -0
- sift/saved_searches/__init__.py +0 -0
- sift/saved_searches/v1/__init__.py +0 -0
- sift/saved_searches/v1/saved_searches_pb2.py +144 -0
- sift/saved_searches/v1/saved_searches_pb2.pyi +385 -0
- sift/saved_searches/v1/saved_searches_pb2_grpc.py +237 -0
- sift/saved_searches/v1/saved_searches_pb2_grpc.pyi +144 -0
- sift/tags/__init__.py +0 -0
- sift/tags/v1/__init__.py +0 -0
- sift/tags/v1/tags_pb2.py +49 -0
- sift/tags/v1/tags_pb2.pyi +71 -0
- sift/tags/v1/tags_pb2_grpc.py +4 -0
- sift/tags/v1/tags_pb2_grpc.pyi +17 -0
- sift/users/__init__.py +0 -0
- sift/users/v2/__init__.py +0 -0
- sift/users/v2/users_pb2.py +61 -0
- sift/users/v2/users_pb2.pyi +142 -0
- sift/users/v2/users_pb2_grpc.py +135 -0
- sift/users/v2/users_pb2_grpc.pyi +84 -0
- sift/views/__init__.py +0 -0
- sift/views/v1/__init__.py +0 -0
- sift/views/v1/views_pb2.py +130 -0
- sift/views/v1/views_pb2.pyi +466 -0
- sift/views/v1/views_pb2_grpc.py +305 -0
- sift/views/v1/views_pb2_grpc.pyi +184 -0
- sift_grafana/py.typed +0 -0
- sift_grafana/sift_query_model.py +64 -0
- sift_py/__init__.py +923 -0
- sift_py/_internal/__init__.py +5 -0
- sift_py/_internal/cel.py +18 -0
- sift_py/_internal/channel.py +42 -0
- sift_py/_internal/convert/__init__.py +3 -0
- sift_py/_internal/convert/json.py +24 -0
- sift_py/_internal/convert/protobuf.py +34 -0
- sift_py/_internal/convert/timestamp.py +9 -0
- sift_py/_internal/test_util/__init__.py +0 -0
- sift_py/_internal/test_util/channel.py +136 -0
- sift_py/_internal/test_util/fn.py +14 -0
- sift_py/_internal/test_util/server_interceptor.py +62 -0
- sift_py/_internal/time.py +48 -0
- sift_py/_internal/user.py +39 -0
- sift_py/data/__init__.py +171 -0
- sift_py/data/_channel.py +38 -0
- sift_py/data/_deserialize.py +208 -0
- sift_py/data/_deserialize_test.py +134 -0
- sift_py/data/_service_test.py +276 -0
- sift_py/data/_validate.py +10 -0
- sift_py/data/error.py +5 -0
- sift_py/data/query.py +299 -0
- sift_py/data/service.py +497 -0
- sift_py/data_import/__init__.py +130 -0
- sift_py/data_import/_config.py +167 -0
- sift_py/data_import/_config_test.py +166 -0
- sift_py/data_import/_csv_test.py +395 -0
- sift_py/data_import/_status_test.py +176 -0
- sift_py/data_import/_tdms_test.py +238 -0
- sift_py/data_import/ch10.py +157 -0
- sift_py/data_import/config.py +19 -0
- sift_py/data_import/csv.py +259 -0
- sift_py/data_import/status.py +113 -0
- sift_py/data_import/tdms.py +206 -0
- sift_py/data_import/tempfile.py +30 -0
- sift_py/data_import/time_format.py +39 -0
- sift_py/error.py +11 -0
- sift_py/file_attachment/__init__.py +88 -0
- sift_py/file_attachment/_internal/__init__.py +0 -0
- sift_py/file_attachment/_internal/download.py +13 -0
- sift_py/file_attachment/_internal/upload.py +100 -0
- sift_py/file_attachment/_service_test.py +161 -0
- sift_py/file_attachment/entity.py +30 -0
- sift_py/file_attachment/metadata.py +107 -0
- sift_py/file_attachment/service.py +142 -0
- sift_py/grpc/__init__.py +15 -0
- sift_py/grpc/_async_interceptors/__init__.py +0 -0
- sift_py/grpc/_async_interceptors/base.py +72 -0
- sift_py/grpc/_async_interceptors/metadata.py +36 -0
- sift_py/grpc/_interceptors/__init__.py +0 -0
- sift_py/grpc/_interceptors/base.py +61 -0
- sift_py/grpc/_interceptors/context.py +25 -0
- sift_py/grpc/_interceptors/metadata.py +33 -0
- sift_py/grpc/_retry.py +70 -0
- sift_py/grpc/keepalive.py +34 -0
- sift_py/grpc/transport.py +250 -0
- sift_py/grpc/transport_test.py +170 -0
- sift_py/ingestion/__init__.py +6 -0
- sift_py/ingestion/_internal/__init__.py +6 -0
- sift_py/ingestion/_internal/channel.py +12 -0
- sift_py/ingestion/_internal/error.py +10 -0
- sift_py/ingestion/_internal/ingest.py +350 -0
- sift_py/ingestion/_internal/ingest_test.py +357 -0
- sift_py/ingestion/_internal/ingestion_config.py +130 -0
- sift_py/ingestion/_internal/run.py +46 -0
- sift_py/ingestion/_service_test.py +478 -0
- sift_py/ingestion/buffer.py +189 -0
- sift_py/ingestion/channel.py +422 -0
- sift_py/ingestion/config/__init__.py +3 -0
- sift_py/ingestion/config/telemetry.py +281 -0
- sift_py/ingestion/config/telemetry_test.py +405 -0
- sift_py/ingestion/config/yaml/__init__.py +0 -0
- sift_py/ingestion/config/yaml/error.py +44 -0
- sift_py/ingestion/config/yaml/load.py +126 -0
- sift_py/ingestion/config/yaml/spec.py +58 -0
- sift_py/ingestion/config/yaml/test_load.py +25 -0
- sift_py/ingestion/flow.py +73 -0
- sift_py/ingestion/manager.py +99 -0
- sift_py/ingestion/rule/__init__.py +4 -0
- sift_py/ingestion/rule/config.py +11 -0
- sift_py/ingestion/service.py +237 -0
- sift_py/py.typed +0 -0
- sift_py/report_templates/__init__.py +0 -0
- sift_py/report_templates/_config_test.py +34 -0
- sift_py/report_templates/_service_test.py +94 -0
- sift_py/report_templates/config.py +36 -0
- sift_py/report_templates/service.py +171 -0
- sift_py/rest.py +29 -0
- sift_py/rule/__init__.py +0 -0
- sift_py/rule/_config_test.py +109 -0
- sift_py/rule/_service_test.py +168 -0
- sift_py/rule/config.py +229 -0
- sift_py/rule/service.py +484 -0
- sift_py/yaml/__init__.py +0 -0
- sift_py/yaml/_channel_test.py +169 -0
- sift_py/yaml/_rule_test.py +207 -0
- sift_py/yaml/channel.py +224 -0
- sift_py/yaml/report_templates.py +73 -0
- sift_py/yaml/rule.py +321 -0
- sift_py/yaml/utils.py +15 -0
- sift_stack_py-0.3.2.dist-info/LICENSE +7 -0
- sift_stack_py-0.3.2.dist-info/METADATA +109 -0
- sift_stack_py-0.3.2.dist-info/RECORD +291 -0
- sift_stack_py-0.3.2.dist-info/WHEEL +5 -0
- 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
|
+
)
|
sift_py/grpc/__init__.py
ADDED
|
@@ -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)
|