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,113 @@
|
|
|
1
|
+
import time
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from enum import Enum
|
|
4
|
+
from typing import Optional, Union
|
|
5
|
+
from urllib.parse import urljoin
|
|
6
|
+
|
|
7
|
+
import requests
|
|
8
|
+
from pydantic import BaseModel, ConfigDict, field_validator
|
|
9
|
+
from pydantic.alias_generators import to_camel
|
|
10
|
+
from pydantic_core import PydanticCustomError
|
|
11
|
+
from typing_extensions import Self
|
|
12
|
+
|
|
13
|
+
from sift_py.rest import SiftRestConfig, compute_uri
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class DataImportStatusType(Enum):
|
|
17
|
+
"""Status of the data import."""
|
|
18
|
+
|
|
19
|
+
SUCCEEDED = "DATA_IMPORT_STATUS_SUCCEEDED"
|
|
20
|
+
PENDING = "DATA_IMPORT_STATUS_PENDING"
|
|
21
|
+
IN_PROGRESS = "DATA_IMPORT_STATUS_IN_PROGRESS"
|
|
22
|
+
FAILED = "DATA_IMPORT_STATUS_FAILED"
|
|
23
|
+
|
|
24
|
+
@classmethod
|
|
25
|
+
def from_str(cls, val: str) -> Optional[Self]:
|
|
26
|
+
try:
|
|
27
|
+
return cls(val)
|
|
28
|
+
except ValueError:
|
|
29
|
+
return None
|
|
30
|
+
|
|
31
|
+
def as_human_str(self) -> str:
|
|
32
|
+
return self.value
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class DataImport(BaseModel):
|
|
36
|
+
"""Metadata regarding the data import."""
|
|
37
|
+
|
|
38
|
+
model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
|
|
39
|
+
|
|
40
|
+
data_import_id: str
|
|
41
|
+
created_date: datetime
|
|
42
|
+
modified_date: datetime
|
|
43
|
+
source_url: str = ""
|
|
44
|
+
status: Union[str, DataImportStatusType]
|
|
45
|
+
error_message: str = ""
|
|
46
|
+
csv_config: dict
|
|
47
|
+
|
|
48
|
+
@field_validator("status", mode="before")
|
|
49
|
+
@classmethod
|
|
50
|
+
def convert_status(cls, raw: Union[str, DataImportStatusType]) -> DataImportStatusType:
|
|
51
|
+
if isinstance(raw, DataImportStatusType):
|
|
52
|
+
return raw
|
|
53
|
+
elif isinstance(raw, str):
|
|
54
|
+
value = DataImportStatusType.from_str(raw)
|
|
55
|
+
if value is not None:
|
|
56
|
+
return value
|
|
57
|
+
|
|
58
|
+
raise PydanticCustomError(
|
|
59
|
+
"invalid_data_import_error", f"Invalid data import status: {raw}."
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class DataImportService:
|
|
64
|
+
"""
|
|
65
|
+
Service used to retrive information about a particular data import.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
STATUS_PATH = "/api/v1/data-imports"
|
|
69
|
+
_data_import_id: str
|
|
70
|
+
|
|
71
|
+
def __init__(self, restconf: SiftRestConfig, data_import_id: str):
|
|
72
|
+
base_uri = compute_uri(restconf)
|
|
73
|
+
self._data_import_id = data_import_id
|
|
74
|
+
self._status_uri = urljoin(base_uri, self.STATUS_PATH)
|
|
75
|
+
self._apikey = restconf["apikey"]
|
|
76
|
+
|
|
77
|
+
def get_data_import(self) -> DataImport:
|
|
78
|
+
"""
|
|
79
|
+
Returns information about the data import.
|
|
80
|
+
"""
|
|
81
|
+
response = requests.get(
|
|
82
|
+
url=f"{self._status_uri}/{self._data_import_id}",
|
|
83
|
+
headers={"Authorization": f"Bearer {self._apikey}"},
|
|
84
|
+
)
|
|
85
|
+
response.raise_for_status()
|
|
86
|
+
|
|
87
|
+
data = response.json().get("dataImport")
|
|
88
|
+
data_import = DataImport(**data)
|
|
89
|
+
return data_import
|
|
90
|
+
|
|
91
|
+
def wait_until_complete(self) -> DataImport:
|
|
92
|
+
"""
|
|
93
|
+
Blocks until the data import is completed. Check the status to determine
|
|
94
|
+
if the import was successful or not.
|
|
95
|
+
"""
|
|
96
|
+
polling_interval = 1
|
|
97
|
+
while True:
|
|
98
|
+
data_import = self.get_data_import()
|
|
99
|
+
status: DataImportStatusType = data_import.status # type: ignore
|
|
100
|
+
if status in [
|
|
101
|
+
DataImportStatusType.SUCCEEDED,
|
|
102
|
+
DataImportStatusType.FAILED,
|
|
103
|
+
]:
|
|
104
|
+
return data_import
|
|
105
|
+
elif status in [
|
|
106
|
+
DataImportStatusType.PENDING,
|
|
107
|
+
DataImportStatusType.IN_PROGRESS,
|
|
108
|
+
]:
|
|
109
|
+
pass
|
|
110
|
+
else:
|
|
111
|
+
raise Exception(f"Unknown status: {status}")
|
|
112
|
+
time.sleep(polling_interval)
|
|
113
|
+
polling_interval = min(polling_interval * 2, 60)
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import Dict, List, Optional, Union
|
|
3
|
+
|
|
4
|
+
try:
|
|
5
|
+
from nptdms import ( # type: ignore
|
|
6
|
+
ChannelObject,
|
|
7
|
+
RootObject,
|
|
8
|
+
TdmsChannel,
|
|
9
|
+
TdmsFile,
|
|
10
|
+
TdmsWriter,
|
|
11
|
+
types,
|
|
12
|
+
)
|
|
13
|
+
except ImportError as e:
|
|
14
|
+
raise RuntimeError(
|
|
15
|
+
"The npTDMS package is required to use the TDMS upload service. "
|
|
16
|
+
"Please include this dependency in your project by specifying `sift-stack-py[tdms]`."
|
|
17
|
+
) from e
|
|
18
|
+
|
|
19
|
+
from sift_py.data_import._config import DataColumn, TimeColumn
|
|
20
|
+
from sift_py.data_import.config import CsvConfig
|
|
21
|
+
from sift_py.data_import.csv import CsvUploadService
|
|
22
|
+
from sift_py.data_import.status import DataImportService
|
|
23
|
+
from sift_py.data_import.tempfile import NamedTemporaryFile
|
|
24
|
+
from sift_py.data_import.time_format import TimeFormatType
|
|
25
|
+
from sift_py.ingestion.channel import ChannelDataType
|
|
26
|
+
from sift_py.rest import SiftRestConfig
|
|
27
|
+
|
|
28
|
+
TDMS_TO_SIFT_TYPES = {
|
|
29
|
+
types.Boolean: ChannelDataType.BOOL,
|
|
30
|
+
types.Int8: ChannelDataType.INT_32,
|
|
31
|
+
types.Int16: ChannelDataType.INT_32,
|
|
32
|
+
types.Int32: ChannelDataType.INT_32,
|
|
33
|
+
types.Int64: ChannelDataType.INT_64,
|
|
34
|
+
types.Uint8: ChannelDataType.UINT_32,
|
|
35
|
+
types.Uint16: ChannelDataType.UINT_32,
|
|
36
|
+
types.Uint32: ChannelDataType.UINT_32,
|
|
37
|
+
types.Uint64: ChannelDataType.UINT_64,
|
|
38
|
+
types.SingleFloat: ChannelDataType.FLOAT,
|
|
39
|
+
types.DoubleFloat: ChannelDataType.DOUBLE,
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class TdmsUploadService:
|
|
44
|
+
"""
|
|
45
|
+
Service to upload TDMS files.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
_csv_upload_service: CsvUploadService
|
|
49
|
+
|
|
50
|
+
def __init__(self, rest_conf: SiftRestConfig):
|
|
51
|
+
self._csv_upload_service = CsvUploadService(rest_conf)
|
|
52
|
+
|
|
53
|
+
def upload(
|
|
54
|
+
self,
|
|
55
|
+
path: Union[str, Path],
|
|
56
|
+
asset_name: str,
|
|
57
|
+
group_into_components: bool = False,
|
|
58
|
+
ignore_errors: bool = False,
|
|
59
|
+
run_name: Optional[str] = None,
|
|
60
|
+
run_id: Optional[str] = None,
|
|
61
|
+
) -> DataImportService:
|
|
62
|
+
"""
|
|
63
|
+
Uploads the TDMS file pointed to by `path` to the specified asset.
|
|
64
|
+
|
|
65
|
+
Set `group_into_components` to True if you want to upload the TDMS groups as
|
|
66
|
+
a Sift Component.
|
|
67
|
+
|
|
68
|
+
If `ignore_errors` is True will skip channels without timing information.
|
|
69
|
+
|
|
70
|
+
Override `run_name` to specify the name of the run to create for this data. Default is None.
|
|
71
|
+
Override `run_id` to specify the id of the run to add this data to. Default is None.
|
|
72
|
+
"""
|
|
73
|
+
posix_path = Path(path) if isinstance(path, str) else path
|
|
74
|
+
|
|
75
|
+
if not posix_path.is_file():
|
|
76
|
+
raise Exception(f"Provided path, '{path}', does not point to a regular file.")
|
|
77
|
+
|
|
78
|
+
with NamedTemporaryFile(mode="w", suffix=".csv") as temp_file:
|
|
79
|
+
valid_channels = self._convert_to_csv(path, temp_file.name, ignore_errors)
|
|
80
|
+
if not valid_channels:
|
|
81
|
+
raise Exception(f"No valid channels remaining in {path}")
|
|
82
|
+
|
|
83
|
+
csv_config = self._create_csv_config(
|
|
84
|
+
valid_channels, asset_name, group_into_components, run_name, run_id
|
|
85
|
+
)
|
|
86
|
+
return self._csv_upload_service.upload(temp_file.name, csv_config)
|
|
87
|
+
|
|
88
|
+
def _convert_to_csv(
|
|
89
|
+
self, src_path: Union[str, Path], dst_path: Union[str, Path], ignore_errors: bool
|
|
90
|
+
) -> List[TdmsChannel]:
|
|
91
|
+
"""Converts the TDMS file to a temporary CSV on disk that we will upload.
|
|
92
|
+
|
|
93
|
+
Returns the valid channels after parsing the TDMS file. Valid channels contain
|
|
94
|
+
timing information.
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
def contains_timing(channel: TdmsChannel) -> bool:
|
|
98
|
+
"""Returns true if the TDMS Channel contains timing information."""
|
|
99
|
+
return all(
|
|
100
|
+
[
|
|
101
|
+
"wf_increment" in channel.properties,
|
|
102
|
+
"wf_start_time" in channel.properties,
|
|
103
|
+
"wf_start_offset" in channel.properties,
|
|
104
|
+
]
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
def normalize_name(channel_name: str) -> str:
|
|
108
|
+
"""Normalize channel names by invalid characters."""
|
|
109
|
+
return " ".join(channel_name.replace("/", " ").split())
|
|
110
|
+
|
|
111
|
+
src_file = TdmsFile(src_path)
|
|
112
|
+
|
|
113
|
+
original_groups = src_file.groups()
|
|
114
|
+
valid_channels: List[ChannelObject] = []
|
|
115
|
+
for group in original_groups:
|
|
116
|
+
for channel in group.channels():
|
|
117
|
+
if contains_timing(channel):
|
|
118
|
+
new_channel = ChannelObject(
|
|
119
|
+
group=normalize_name(channel.group_name),
|
|
120
|
+
channel=normalize_name(channel.name),
|
|
121
|
+
data=channel.data,
|
|
122
|
+
properties=channel.properties,
|
|
123
|
+
)
|
|
124
|
+
valid_channels.append(new_channel)
|
|
125
|
+
else:
|
|
126
|
+
if ignore_errors:
|
|
127
|
+
print(
|
|
128
|
+
f"{group.name}:{channel.name} does not contain timing information. Skipping."
|
|
129
|
+
)
|
|
130
|
+
else:
|
|
131
|
+
raise Exception(
|
|
132
|
+
f"{group.name}:{channel.name} does not contain timing information. "
|
|
133
|
+
"Set `ignore_errors` to True to skip channels without timing information."
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
# Write out the new TDMS file with invalid channels removed, then convert to csv.
|
|
137
|
+
with NamedTemporaryFile(mode="w") as f:
|
|
138
|
+
with TdmsWriter(f.name) as tdms_writer:
|
|
139
|
+
root_object = RootObject(src_file.properties)
|
|
140
|
+
tdms_writer.write_segment([root_object] + original_groups + valid_channels)
|
|
141
|
+
|
|
142
|
+
filtered_tdms_file = TdmsFile(f.name)
|
|
143
|
+
df = filtered_tdms_file.as_dataframe(time_index=True, absolute_time=True)
|
|
144
|
+
df.to_csv(dst_path, encoding="utf-8")
|
|
145
|
+
|
|
146
|
+
return [channel for group in filtered_tdms_file.groups() for channel in group.channels()]
|
|
147
|
+
|
|
148
|
+
def _create_csv_config(
|
|
149
|
+
self,
|
|
150
|
+
channels: List[TdmsChannel],
|
|
151
|
+
asset_name: str,
|
|
152
|
+
group_into_components: bool,
|
|
153
|
+
run_name: Optional[str] = None,
|
|
154
|
+
run_id: Optional[str] = None,
|
|
155
|
+
) -> CsvConfig:
|
|
156
|
+
"""Construct a CsvConfig based on metadata within the TDMS file."""
|
|
157
|
+
data_config: Dict[int, DataColumn] = {}
|
|
158
|
+
# Data columns start in column 2 (1-indexed)
|
|
159
|
+
first_data_column = 2
|
|
160
|
+
for i, channel in enumerate(channels):
|
|
161
|
+
try:
|
|
162
|
+
data_type = TDMS_TO_SIFT_TYPES[channel.data_type].as_human_str(api_format=True)
|
|
163
|
+
except KeyError:
|
|
164
|
+
data_type = None
|
|
165
|
+
|
|
166
|
+
if data_type is None:
|
|
167
|
+
raise Exception(f"{channel.name} data type not supported: {channel.data_type}")
|
|
168
|
+
|
|
169
|
+
extra_info = ""
|
|
170
|
+
for k, v in channel.properties.items():
|
|
171
|
+
# Skip these since the csv config has dedicated fields for them.
|
|
172
|
+
if k in ["description", "unit_string"]:
|
|
173
|
+
continue
|
|
174
|
+
# Must convert datetime to a string
|
|
175
|
+
elif k == "wf_start_time":
|
|
176
|
+
v = str(v)
|
|
177
|
+
extra_info += f"{k}: {v}\n"
|
|
178
|
+
|
|
179
|
+
channel_config = DataColumn(
|
|
180
|
+
name=channel.name,
|
|
181
|
+
data_type=data_type,
|
|
182
|
+
description=f"{channel.properties.get('description')}\n{extra_info}",
|
|
183
|
+
units=channel.properties.get("unit_string") or "",
|
|
184
|
+
)
|
|
185
|
+
if group_into_components and channel.group_name:
|
|
186
|
+
channel_config.component = channel.group_name
|
|
187
|
+
|
|
188
|
+
data_config[first_data_column + i] = channel_config
|
|
189
|
+
|
|
190
|
+
config_info = {
|
|
191
|
+
"asset_name": asset_name,
|
|
192
|
+
"first_data_row": first_data_column,
|
|
193
|
+
"time_column": TimeColumn(
|
|
194
|
+
format=TimeFormatType.ABSOLUTE_DATETIME,
|
|
195
|
+
column_number=1,
|
|
196
|
+
),
|
|
197
|
+
"data_columns": data_config,
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if run_name is not None:
|
|
201
|
+
config_info["run_name"] = run_name
|
|
202
|
+
|
|
203
|
+
if run_id is not None:
|
|
204
|
+
config_info["run_id"] = run_id
|
|
205
|
+
|
|
206
|
+
return CsvConfig(config_info)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import tempfile
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class NamedTemporaryFile:
|
|
7
|
+
"""
|
|
8
|
+
Created a named temporary file.
|
|
9
|
+
|
|
10
|
+
Works on both Windows and Unix systems.
|
|
11
|
+
|
|
12
|
+
See https://stackoverflow.com/questions/23212435/permission-denied-to-write-to-my-temporary-file
|
|
13
|
+
for more information on Windows compatibility.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def __init__(self, mode, suffix=""):
|
|
17
|
+
self.temp_dir = tempfile.mkdtemp()
|
|
18
|
+
self.name = Path(self.temp_dir) / f"tempfile{suffix}"
|
|
19
|
+
self.file = open(self.name, mode)
|
|
20
|
+
|
|
21
|
+
def __enter__(self):
|
|
22
|
+
return self.file
|
|
23
|
+
|
|
24
|
+
def __exit__(self, exc_type, exc_value, traceback):
|
|
25
|
+
self.file.close()
|
|
26
|
+
try:
|
|
27
|
+
os.remove(self.name)
|
|
28
|
+
os.rmdir(self.temp_dir)
|
|
29
|
+
except FileNotFoundError:
|
|
30
|
+
pass
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
from typing_extensions import Self
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class TimeFormatType(Enum):
|
|
8
|
+
ABSOLUTE_RFC3339 = "TIME_FORMAT_ABSOLUTE_RFC3339"
|
|
9
|
+
ABSOLUTE_DATETIME = "TIME_FORMAT_ABSOLUTE_DATETIME"
|
|
10
|
+
ABSOLUTE_UNIX_SECONDS = "TIME_FORMAT_ABSOLUTE_UNIX_SECONDS"
|
|
11
|
+
ABSOLUTE_UNIX_MILLISECONDS = "TIME_FORMAT_ABSOLUTE_UNIX_MILLISECONDS"
|
|
12
|
+
ABSOLUTE_UNIX_MICROSECONDS = "TIME_FORMAT_ABSOLUTE_UNIX_MICROSECONDS"
|
|
13
|
+
ABSOLUTE_UNIX_NANOSECONDS = "TIME_FORMAT_ABSOLUTE_UNIX_NANOSECONDS"
|
|
14
|
+
RELATIVE_NANOSECONDS = "TIME_FORMAT_RELATIVE_NANOSECONDS"
|
|
15
|
+
RELATIVE_MICROSECONDS = "TIME_FORMAT_RELATIVE_MICROSECONDS"
|
|
16
|
+
RELATIVE_MILLISECONDS = "TIME_FORMAT_RELATIVE_MILLISECONDS"
|
|
17
|
+
RELATIVE_SECONDS = "TIME_FORMAT_RELATIVE_SECONDS"
|
|
18
|
+
RELATIVE_MINUTES = "TIME_FORMAT_RELATIVE_MINUTES"
|
|
19
|
+
RELATIVE_HOURS = "TIME_FORMAT_RELATIVE_HOURS"
|
|
20
|
+
|
|
21
|
+
@classmethod
|
|
22
|
+
def from_str(cls, val: str) -> Optional[Self]:
|
|
23
|
+
try:
|
|
24
|
+
return cls(val)
|
|
25
|
+
except ValueError:
|
|
26
|
+
return None
|
|
27
|
+
|
|
28
|
+
def as_human_str(self) -> str:
|
|
29
|
+
return self.value
|
|
30
|
+
|
|
31
|
+
def is_relative(self) -> bool:
|
|
32
|
+
return self in [
|
|
33
|
+
self.RELATIVE_NANOSECONDS,
|
|
34
|
+
self.RELATIVE_MICROSECONDS,
|
|
35
|
+
self.RELATIVE_MILLISECONDS,
|
|
36
|
+
self.RELATIVE_SECONDS,
|
|
37
|
+
self.RELATIVE_MINUTES,
|
|
38
|
+
self.RELATIVE_HOURS,
|
|
39
|
+
]
|
sift_py/error.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
class SiftError(Exception):
|
|
2
|
+
"""
|
|
3
|
+
These exceptions are raised when something totally unexpected occurs and is
|
|
4
|
+
meant to indicate that the error is likely not caused by the user, but rather,
|
|
5
|
+
the library itself. These errors should be reported to Sift.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
msg: str
|
|
9
|
+
|
|
10
|
+
def __init__(self, msg: str):
|
|
11
|
+
super().__init__(f"{msg}\nPlease notify Sift.")
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module contains services to facilitate uploading and downloading file attachments.
|
|
3
|
+
It also provides utilities to easily query all file attachments for a given entity
|
|
4
|
+
which could be a run, annotation, or annotation logs. File attachment deletion is also supported.
|
|
5
|
+
|
|
6
|
+
Once files have been attached, they should be viewable on the Sift application, attached to their
|
|
7
|
+
respective entities. Below are various examples on how to leverage the `sift_py.file_attachment.service.FileAttachmentService`.
|
|
8
|
+
|
|
9
|
+
## Initializing the file attachment service
|
|
10
|
+
|
|
11
|
+
Unlike other services throughout `sift_py`, the `sift_py.file_attachment.service.FileAttachmentService` does rely on both
|
|
12
|
+
REST and gRPC APIs, so with that in mind we can initialize our service like so:
|
|
13
|
+
|
|
14
|
+
```python
|
|
15
|
+
from sift_py.grpc.transport import SiftChannelConfig, use_sift_channel
|
|
16
|
+
from sift_py.file_attachment.service import FileAttachmentService
|
|
17
|
+
from sift_py.file_attachment.entity import Entity, EntityType
|
|
18
|
+
from sift_py.file_attachment.metadata import ImageMetadata
|
|
19
|
+
from sift_py.rest import SiftRestConfig
|
|
20
|
+
|
|
21
|
+
from sift.remote_files.v1.remote_files_pb2 import GetRemoteFileRequest
|
|
22
|
+
from sift.remote_files.v1.remote_files_pb2_grpc import RemoteFileServiceStub
|
|
23
|
+
|
|
24
|
+
rest_config: SiftRestConfig = {
|
|
25
|
+
# Be sure to exclude the "https://" or "http://" scheme out of the uri
|
|
26
|
+
"uri": rest_base_uri,
|
|
27
|
+
"apikey": apikey,
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
sift_channel_config = SiftChannelConfig(uri=grpc_base_uri, apikey=apikey)
|
|
31
|
+
|
|
32
|
+
with use_sift_channel(sift_channel_config) as channel:
|
|
33
|
+
file_attachment_service = FileAttachmentService(channel, rest_config)
|
|
34
|
+
...
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
With the service initialized we can now interact with the file attachments API.
|
|
38
|
+
|
|
39
|
+
## Various Examples
|
|
40
|
+
|
|
41
|
+
For demonstrative purposes we will upload an `mp4` file and attach to a run of `run_id`.
|
|
42
|
+
Once it is uploaded we will query all file attachments for a particular run and re-download
|
|
43
|
+
what we just uploaded.
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
|
|
47
|
+
from sift_py.grpc.transport import SiftChannelConfig, use_sift_channel
|
|
48
|
+
from sift_py.file_attachment.service import FileAttachmentService
|
|
49
|
+
from sift_py.file_attachment.entity import Entity, EntityType
|
|
50
|
+
from sift_py.file_attachment.metadata import VideoMetadata
|
|
51
|
+
from sift_py.rest import SiftRestConfig
|
|
52
|
+
|
|
53
|
+
from sift.remote_files.v1.remote_files_pb2 import GetRemoteFileRequest
|
|
54
|
+
from sift.remote_files.v1.remote_files_pb2_grpc import RemoteFileServiceStub
|
|
55
|
+
|
|
56
|
+
...
|
|
57
|
+
|
|
58
|
+
with use_sift_channel(sift_channel_config) as channel:
|
|
59
|
+
file_attachment_service = FileAttachmentService(channel, rest_config)
|
|
60
|
+
|
|
61
|
+
run = entity=Entity(
|
|
62
|
+
entity_id=run_id, # some arbitrary run ID that refers to an existing run
|
|
63
|
+
entity_type=EntityType.RUN,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
# uploading the file attachment and attaching it to a run of `run_id`.
|
|
67
|
+
remote_file = file_attachment_service.upload_attachment(
|
|
68
|
+
path="path/to/foo.mp4",
|
|
69
|
+
entity=run,
|
|
70
|
+
# Metatadata.. optional but recommended for optimal viewing in the application
|
|
71
|
+
metadata=VideoMetadata(height=2160, width=3840, duration_seconds=5.5, timestamp=datetime(2024, 10, 19, 2, 22, 22),
|
|
72
|
+
description="thrusters getting too hot" ,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
# retrieving all of the file attachments for our run
|
|
76
|
+
all_file_attachments = file_attachment_service.retrieve_attachments(run)
|
|
77
|
+
|
|
78
|
+
# downloading our file_attachment and saving it to our current working dir
|
|
79
|
+
file_attachment_service.download_attachment(remote_file)
|
|
80
|
+
|
|
81
|
+
# downloading our file_attachment and saving it somewhere else with a different name
|
|
82
|
+
file_attachment_service.download_attachment(remote_file, "somewhere/else/foo.mp4")
|
|
83
|
+
|
|
84
|
+
# deleting out file attachment from Sift
|
|
85
|
+
file_attachment_service.delete_file_attachments(remote_file_1, remote_file_2, remote_file_etc)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
"""
|
|
File without changes
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import io
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
import requests
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def download_remote_file(url: str, out: Path):
|
|
8
|
+
with requests.get(url, stream=True) as req:
|
|
9
|
+
req.raise_for_status()
|
|
10
|
+
with open(out, "wb") as output_file:
|
|
11
|
+
for chunk in req.iter_content(chunk_size=io.DEFAULT_BUFFER_SIZE):
|
|
12
|
+
if chunk:
|
|
13
|
+
output_file.write(chunk)
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import mimetypes
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Any, Dict, Optional, Tuple, Union
|
|
4
|
+
from urllib.parse import urljoin
|
|
5
|
+
|
|
6
|
+
import requests
|
|
7
|
+
from requests_toolbelt import MultipartEncoder
|
|
8
|
+
|
|
9
|
+
from sift_py._internal.convert.json import to_json
|
|
10
|
+
from sift_py.file_attachment.entity import Entity
|
|
11
|
+
from sift_py.file_attachment.metadata import Metadata
|
|
12
|
+
from sift_py.rest import SiftRestConfig, compute_uri
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class UploadService:
|
|
16
|
+
UPLOAD_PATH = "/api/v0/remote-files/upload"
|
|
17
|
+
UPLOAD_BULK_PATH = "/api/v0/remote-files/upload:bulk"
|
|
18
|
+
|
|
19
|
+
_upload_uri: str
|
|
20
|
+
_upload_bulk_uri: str
|
|
21
|
+
_apikey: str
|
|
22
|
+
|
|
23
|
+
def __init__(self, restconf: SiftRestConfig):
|
|
24
|
+
base_uri = compute_uri(restconf)
|
|
25
|
+
self._upload_uri = urljoin(base_uri, self.UPLOAD_PATH)
|
|
26
|
+
self._upload_bulk_uri = urljoin(base_uri, self.UPLOAD_BULK_PATH)
|
|
27
|
+
self._apikey = restconf["apikey"]
|
|
28
|
+
|
|
29
|
+
def upload_attachment(
|
|
30
|
+
self,
|
|
31
|
+
path: Union[str, Path],
|
|
32
|
+
entity: Entity,
|
|
33
|
+
metadata: Optional[Metadata] = None,
|
|
34
|
+
description: Optional[str] = None,
|
|
35
|
+
organization_id: Optional[str] = None,
|
|
36
|
+
) -> str:
|
|
37
|
+
posix_path = Path(path) if isinstance(path, str) else path
|
|
38
|
+
|
|
39
|
+
if not posix_path.is_file():
|
|
40
|
+
raise Exception(f"Provided path, '{path}', does not point to a regular file.")
|
|
41
|
+
|
|
42
|
+
file_name, mimetype, content_encoding = self.__class__._mime_and_content_type_from_path(
|
|
43
|
+
posix_path
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
if not mimetype:
|
|
47
|
+
raise Exception(f"The MIME-type of '{posix_path}' could not be computed.")
|
|
48
|
+
|
|
49
|
+
with open(path, "rb") as file:
|
|
50
|
+
form_fields: Dict[str, Any] = {
|
|
51
|
+
"entityId": entity.entity_id,
|
|
52
|
+
"entityType": entity.entity_type.value,
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if content_encoding:
|
|
56
|
+
form_fields["file"] = (
|
|
57
|
+
file_name,
|
|
58
|
+
file,
|
|
59
|
+
mimetype,
|
|
60
|
+
{
|
|
61
|
+
"Content-Encoding": content_encoding,
|
|
62
|
+
},
|
|
63
|
+
)
|
|
64
|
+
else:
|
|
65
|
+
form_fields["file"] = (file_name, file, mimetype)
|
|
66
|
+
|
|
67
|
+
if metadata:
|
|
68
|
+
form_fields["metadata"] = to_json(metadata)
|
|
69
|
+
|
|
70
|
+
if organization_id:
|
|
71
|
+
form_fields["organizationId"] = organization_id
|
|
72
|
+
|
|
73
|
+
if description:
|
|
74
|
+
form_fields["description"] = description
|
|
75
|
+
|
|
76
|
+
form_data = MultipartEncoder(fields=form_fields)
|
|
77
|
+
|
|
78
|
+
# https://github.com/requests/toolbelt/issues/312
|
|
79
|
+
# Issue above is reason for the type ignoring
|
|
80
|
+
response = requests.post(
|
|
81
|
+
url=self._upload_uri,
|
|
82
|
+
data=form_data, # type: ignore
|
|
83
|
+
headers={
|
|
84
|
+
"Authorization": f"Bearer {self._apikey}",
|
|
85
|
+
"Content-Type": form_data.content_type,
|
|
86
|
+
},
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
if response.status_code != 200:
|
|
90
|
+
raise Exception(
|
|
91
|
+
f"Request failed with status code {response.status_code} ({response.reason})."
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
return response.json().get("remoteFile").get("remoteFileId")
|
|
95
|
+
|
|
96
|
+
@staticmethod
|
|
97
|
+
def _mime_and_content_type_from_path(path: Path) -> Tuple[str, Optional[str], Optional[str]]:
|
|
98
|
+
file_name = path.name
|
|
99
|
+
mime, encoding = mimetypes.guess_type(path)
|
|
100
|
+
return file_name, mime, encoding
|