clarifai 11.5.3rc2__py3-none-any.whl → 11.5.5__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.
- clarifai/__init__.py +1 -1
- clarifai/client/model.py +4 -1
- clarifai/runners/models/model_builder.py +4 -4
- clarifai/runners/models/model_class.py +90 -22
- clarifai/runners/models/model_runner.py +50 -8
- clarifai/runners/models/model_servicer.py +26 -3
- clarifai/runners/utils/code_script.py +5 -1
- clarifai/runners/utils/data_types/data_types.py +5 -22
- clarifai/runners/utils/model_utils.py +49 -0
- clarifai/runners/utils/url_fetcher.py +55 -10
- {clarifai-11.5.3rc2.dist-info → clarifai-11.5.5.dist-info}/METADATA +2 -2
- clarifai-11.5.5.dist-info/RECORD +126 -0
- clarifai/__pycache__/__init__.cpython-312.pyc +0 -0
- clarifai/__pycache__/__init__.cpython-38.pyc +0 -0
- clarifai/__pycache__/__init__.cpython-39.pyc +0 -0
- clarifai/__pycache__/errors.cpython-312.pyc +0 -0
- clarifai/__pycache__/errors.cpython-38.pyc +0 -0
- clarifai/__pycache__/errors.cpython-39.pyc +0 -0
- clarifai/__pycache__/versions.cpython-312.pyc +0 -0
- clarifai/__pycache__/versions.cpython-38.pyc +0 -0
- clarifai/__pycache__/versions.cpython-39.pyc +0 -0
- clarifai/client/__pycache__/__init__.cpython-312.pyc +0 -0
- clarifai/client/__pycache__/__init__.cpython-38.pyc +0 -0
- clarifai/client/__pycache__/__init__.cpython-39.pyc +0 -0
- clarifai/client/__pycache__/app.cpython-312.pyc +0 -0
- clarifai/client/__pycache__/app.cpython-38.pyc +0 -0
- clarifai/client/__pycache__/app.cpython-39.pyc +0 -0
- clarifai/client/__pycache__/base.cpython-312.pyc +0 -0
- clarifai/client/__pycache__/base.cpython-38.pyc +0 -0
- clarifai/client/__pycache__/base.cpython-39.pyc +0 -0
- clarifai/client/__pycache__/compute_cluster.cpython-312.pyc +0 -0
- clarifai/client/__pycache__/compute_cluster.cpython-38.pyc +0 -0
- clarifai/client/__pycache__/compute_cluster.cpython-39.pyc +0 -0
- clarifai/client/__pycache__/dataset.cpython-312.pyc +0 -0
- clarifai/client/__pycache__/dataset.cpython-38.pyc +0 -0
- clarifai/client/__pycache__/dataset.cpython-39.pyc +0 -0
- clarifai/client/__pycache__/deployment.cpython-312.pyc +0 -0
- clarifai/client/__pycache__/deployment.cpython-38.pyc +0 -0
- clarifai/client/__pycache__/deployment.cpython-39.pyc +0 -0
- clarifai/client/__pycache__/input.cpython-312.pyc +0 -0
- clarifai/client/__pycache__/input.cpython-38.pyc +0 -0
- clarifai/client/__pycache__/input.cpython-39.pyc +0 -0
- clarifai/client/__pycache__/lister.cpython-312.pyc +0 -0
- clarifai/client/__pycache__/lister.cpython-38.pyc +0 -0
- clarifai/client/__pycache__/lister.cpython-39.pyc +0 -0
- clarifai/client/__pycache__/model.cpython-312.pyc +0 -0
- clarifai/client/__pycache__/model.cpython-38.pyc +0 -0
- clarifai/client/__pycache__/model.cpython-39.pyc +0 -0
- clarifai/client/__pycache__/model_client.cpython-39.pyc +0 -0
- clarifai/client/__pycache__/module.cpython-312.pyc +0 -0
- clarifai/client/__pycache__/module.cpython-38.pyc +0 -0
- clarifai/client/__pycache__/module.cpython-39.pyc +0 -0
- clarifai/client/__pycache__/nodepool.cpython-312.pyc +0 -0
- clarifai/client/__pycache__/nodepool.cpython-38.pyc +0 -0
- clarifai/client/__pycache__/nodepool.cpython-39.pyc +0 -0
- clarifai/client/__pycache__/runner.cpython-39.pyc +0 -0
- clarifai/client/__pycache__/search.cpython-312.pyc +0 -0
- clarifai/client/__pycache__/search.cpython-38.pyc +0 -0
- clarifai/client/__pycache__/search.cpython-39.pyc +0 -0
- clarifai/client/__pycache__/user.cpython-312.pyc +0 -0
- clarifai/client/__pycache__/user.cpython-38.pyc +0 -0
- clarifai/client/__pycache__/user.cpython-39.pyc +0 -0
- clarifai/client/__pycache__/workflow.cpython-312.pyc +0 -0
- clarifai/client/__pycache__/workflow.cpython-38.pyc +0 -0
- clarifai/client/__pycache__/workflow.cpython-39.pyc +0 -0
- clarifai/client/auth/__pycache__/__init__.cpython-312.pyc +0 -0
- clarifai/client/auth/__pycache__/__init__.cpython-38.pyc +0 -0
- clarifai/client/auth/__pycache__/__init__.cpython-39.pyc +0 -0
- clarifai/client/auth/__pycache__/helper.cpython-312.pyc +0 -0
- clarifai/client/auth/__pycache__/helper.cpython-38.pyc +0 -0
- clarifai/client/auth/__pycache__/helper.cpython-39.pyc +0 -0
- clarifai/client/auth/__pycache__/register.cpython-312.pyc +0 -0
- clarifai/client/auth/__pycache__/register.cpython-38.pyc +0 -0
- clarifai/client/auth/__pycache__/register.cpython-39.pyc +0 -0
- clarifai/client/auth/__pycache__/stub.cpython-312.pyc +0 -0
- clarifai/client/auth/__pycache__/stub.cpython-38.pyc +0 -0
- clarifai/client/auth/__pycache__/stub.cpython-39.pyc +0 -0
- clarifai/constants/__pycache__/base.cpython-312.pyc +0 -0
- clarifai/constants/__pycache__/base.cpython-38.pyc +0 -0
- clarifai/constants/__pycache__/base.cpython-39.pyc +0 -0
- clarifai/constants/__pycache__/dataset.cpython-312.pyc +0 -0
- clarifai/constants/__pycache__/dataset.cpython-38.pyc +0 -0
- clarifai/constants/__pycache__/dataset.cpython-39.pyc +0 -0
- clarifai/constants/__pycache__/input.cpython-312.pyc +0 -0
- clarifai/constants/__pycache__/input.cpython-38.pyc +0 -0
- clarifai/constants/__pycache__/input.cpython-39.pyc +0 -0
- clarifai/constants/__pycache__/model.cpython-312.pyc +0 -0
- clarifai/constants/__pycache__/model.cpython-38.pyc +0 -0
- clarifai/constants/__pycache__/model.cpython-39.pyc +0 -0
- clarifai/constants/__pycache__/search.cpython-312.pyc +0 -0
- clarifai/constants/__pycache__/search.cpython-38.pyc +0 -0
- clarifai/constants/__pycache__/search.cpython-39.pyc +0 -0
- clarifai/constants/__pycache__/workflow.cpython-312.pyc +0 -0
- clarifai/constants/__pycache__/workflow.cpython-38.pyc +0 -0
- clarifai/constants/__pycache__/workflow.cpython-39.pyc +0 -0
- clarifai/datasets/__pycache__/__init__.cpython-312.pyc +0 -0
- clarifai/datasets/__pycache__/__init__.cpython-38.pyc +0 -0
- clarifai/datasets/__pycache__/__init__.cpython-39.pyc +0 -0
- clarifai/datasets/export/__pycache__/__init__.cpython-312.pyc +0 -0
- clarifai/datasets/export/__pycache__/__init__.cpython-38.pyc +0 -0
- clarifai/datasets/export/__pycache__/__init__.cpython-39.pyc +0 -0
- clarifai/datasets/export/__pycache__/inputs_annotations.cpython-312.pyc +0 -0
- clarifai/datasets/export/__pycache__/inputs_annotations.cpython-38.pyc +0 -0
- clarifai/datasets/export/__pycache__/inputs_annotations.cpython-39.pyc +0 -0
- clarifai/datasets/upload/__pycache__/__init__.cpython-312.pyc +0 -0
- clarifai/datasets/upload/__pycache__/__init__.cpython-38.pyc +0 -0
- clarifai/datasets/upload/__pycache__/__init__.cpython-39.pyc +0 -0
- clarifai/datasets/upload/__pycache__/base.cpython-312.pyc +0 -0
- clarifai/datasets/upload/__pycache__/base.cpython-38.pyc +0 -0
- clarifai/datasets/upload/__pycache__/base.cpython-39.pyc +0 -0
- clarifai/datasets/upload/__pycache__/features.cpython-312.pyc +0 -0
- clarifai/datasets/upload/__pycache__/features.cpython-38.pyc +0 -0
- clarifai/datasets/upload/__pycache__/features.cpython-39.pyc +0 -0
- clarifai/datasets/upload/__pycache__/image.cpython-312.pyc +0 -0
- clarifai/datasets/upload/__pycache__/image.cpython-38.pyc +0 -0
- clarifai/datasets/upload/__pycache__/image.cpython-39.pyc +0 -0
- clarifai/datasets/upload/__pycache__/multimodal.cpython-312.pyc +0 -0
- clarifai/datasets/upload/__pycache__/multimodal.cpython-38.pyc +0 -0
- clarifai/datasets/upload/__pycache__/multimodal.cpython-39.pyc +0 -0
- clarifai/datasets/upload/__pycache__/text.cpython-312.pyc +0 -0
- clarifai/datasets/upload/__pycache__/text.cpython-38.pyc +0 -0
- clarifai/datasets/upload/__pycache__/text.cpython-39.pyc +0 -0
- clarifai/datasets/upload/__pycache__/utils.cpython-312.pyc +0 -0
- clarifai/datasets/upload/__pycache__/utils.cpython-38.pyc +0 -0
- clarifai/datasets/upload/__pycache__/utils.cpython-39.pyc +0 -0
- clarifai/runners/__pycache__/__init__.cpython-39.pyc +0 -0
- clarifai/runners/models/__pycache__/__init__.cpython-39.pyc +0 -0
- clarifai/runners/models/__pycache__/mcp_class.cpython-39.pyc +0 -0
- clarifai/runners/models/__pycache__/model_class.cpython-39.pyc +0 -0
- clarifai/runners/models/__pycache__/model_runner.cpython-39.pyc +0 -0
- clarifai/runners/models/__pycache__/openai_class.cpython-39.pyc +0 -0
- clarifai/runners/utils/__pycache__/__init__.cpython-39.pyc +0 -0
- clarifai/runners/utils/__pycache__/code_script.cpython-39.pyc +0 -0
- clarifai/runners/utils/__pycache__/data_utils.cpython-39.pyc +0 -0
- clarifai/runners/utils/__pycache__/method_signatures.cpython-39.pyc +0 -0
- clarifai/runners/utils/__pycache__/openai_convertor.cpython-39.pyc +0 -0
- clarifai/runners/utils/__pycache__/serializers.cpython-39.pyc +0 -0
- clarifai/runners/utils/__pycache__/url_fetcher.cpython-39.pyc +0 -0
- clarifai/runners/utils/data_types/__pycache__/__init__.cpython-39.pyc +0 -0
- clarifai/runners/utils/data_types/__pycache__/data_types.cpython-39.pyc +0 -0
- clarifai/schema/__pycache__/search.cpython-312.pyc +0 -0
- clarifai/schema/__pycache__/search.cpython-38.pyc +0 -0
- clarifai/schema/__pycache__/search.cpython-39.pyc +0 -0
- clarifai/urls/__pycache__/helper.cpython-312.pyc +0 -0
- clarifai/urls/__pycache__/helper.cpython-38.pyc +0 -0
- clarifai/urls/__pycache__/helper.cpython-39.pyc +0 -0
- clarifai/utils/__pycache__/__init__.cpython-312.pyc +0 -0
- clarifai/utils/__pycache__/__init__.cpython-38.pyc +0 -0
- clarifai/utils/__pycache__/__init__.cpython-39.pyc +0 -0
- clarifai/utils/__pycache__/constants.cpython-312.pyc +0 -0
- clarifai/utils/__pycache__/constants.cpython-38.pyc +0 -0
- clarifai/utils/__pycache__/constants.cpython-39.pyc +0 -0
- clarifai/utils/__pycache__/logging.cpython-312.pyc +0 -0
- clarifai/utils/__pycache__/logging.cpython-38.pyc +0 -0
- clarifai/utils/__pycache__/logging.cpython-39.pyc +0 -0
- clarifai/utils/__pycache__/misc.cpython-312.pyc +0 -0
- clarifai/utils/__pycache__/misc.cpython-38.pyc +0 -0
- clarifai/utils/__pycache__/misc.cpython-39.pyc +0 -0
- clarifai/utils/__pycache__/model_train.cpython-312.pyc +0 -0
- clarifai/utils/__pycache__/model_train.cpython-38.pyc +0 -0
- clarifai/utils/__pycache__/model_train.cpython-39.pyc +0 -0
- clarifai/utils/__pycache__/protobuf.cpython-39.pyc +0 -0
- clarifai/workflows/__pycache__/__init__.cpython-312.pyc +0 -0
- clarifai/workflows/__pycache__/__init__.cpython-38.pyc +0 -0
- clarifai/workflows/__pycache__/__init__.cpython-39.pyc +0 -0
- clarifai/workflows/__pycache__/export.cpython-312.pyc +0 -0
- clarifai/workflows/__pycache__/export.cpython-38.pyc +0 -0
- clarifai/workflows/__pycache__/export.cpython-39.pyc +0 -0
- clarifai/workflows/__pycache__/utils.cpython-312.pyc +0 -0
- clarifai/workflows/__pycache__/utils.cpython-38.pyc +0 -0
- clarifai/workflows/__pycache__/utils.cpython-39.pyc +0 -0
- clarifai/workflows/__pycache__/validate.cpython-312.pyc +0 -0
- clarifai/workflows/__pycache__/validate.cpython-38.pyc +0 -0
- clarifai/workflows/__pycache__/validate.cpython-39.pyc +0 -0
- clarifai-11.5.3rc2.dist-info/RECORD +0 -288
- {clarifai-11.5.3rc2.dist-info → clarifai-11.5.5.dist-info}/WHEEL +0 -0
- {clarifai-11.5.3rc2.dist-info → clarifai-11.5.5.dist-info}/entry_points.txt +0 -0
- {clarifai-11.5.3rc2.dist-info → clarifai-11.5.5.dist-info}/licenses/LICENSE +0 -0
- {clarifai-11.5.3rc2.dist-info → clarifai-11.5.5.dist-info}/top_level.txt +0 -0
clarifai/__init__.py
CHANGED
@@ -1 +1 @@
|
|
1
|
-
__version__ = "11.5.
|
1
|
+
__version__ = "11.5.5"
|
clarifai/client/model.py
CHANGED
@@ -528,7 +528,10 @@ class Model(Lister, BaseClient):
|
|
528
528
|
inference_params = kwargs.get('inference_params', {})
|
529
529
|
output_config = kwargs.get('output_config', {})
|
530
530
|
return self.client._predict_by_proto(
|
531
|
-
inputs=inputs,
|
531
|
+
inputs=inputs,
|
532
|
+
# method_name="PostModelOutputs",
|
533
|
+
inference_params=inference_params,
|
534
|
+
output_config=output_config,
|
532
535
|
)
|
533
536
|
|
534
537
|
return self.client.predict(*args, **kwargs)
|
@@ -457,8 +457,8 @@ class ModelBuilder:
|
|
457
457
|
Returns the method signatures for the model class in YAML format.
|
458
458
|
"""
|
459
459
|
model_class = self.load_model_class(mocking=True)
|
460
|
-
|
461
|
-
signatures = {method.name: method.signature for method in
|
460
|
+
method_infos = model_class._get_method_infos()
|
461
|
+
signatures = {method.name: method.signature for method in method_infos.values()}
|
462
462
|
return signatures_to_yaml(signatures)
|
463
463
|
|
464
464
|
def get_method_signatures(self, mocking=True):
|
@@ -472,8 +472,8 @@ class ModelBuilder:
|
|
472
472
|
list: A list of method signatures for the model class.
|
473
473
|
"""
|
474
474
|
model_class = self.load_model_class(mocking=mocking)
|
475
|
-
|
476
|
-
signatures = [method.signature for method in
|
475
|
+
method_infos = model_class._get_method_infos()
|
476
|
+
signatures = [method.signature for method in method_infos.values()]
|
477
477
|
return signatures
|
478
478
|
|
479
479
|
@property
|
@@ -1,6 +1,5 @@
|
|
1
1
|
import inspect
|
2
2
|
import itertools
|
3
|
-
import logging
|
4
3
|
import os
|
5
4
|
import traceback
|
6
5
|
from abc import ABC
|
@@ -20,11 +19,16 @@ from clarifai.runners.utils.method_signatures import (
|
|
20
19
|
serialize,
|
21
20
|
signatures_to_json,
|
22
21
|
)
|
22
|
+
from clarifai.runners.utils.model_utils import is_proto_style_method
|
23
|
+
from clarifai.utils.logging import logger
|
23
24
|
|
24
25
|
_METHOD_INFO_ATTR = '_cf_method_info'
|
25
26
|
|
26
27
|
_RAISE_EXCEPTIONS = os.getenv("RAISE_EXCEPTIONS", "false").lower() in ("true", "1")
|
27
28
|
|
29
|
+
FALLBACK_METHOD_PROTO = 'PostModelOutputs'
|
30
|
+
FALLBACK_METHOD_PYTHON = 'predict'
|
31
|
+
|
28
32
|
|
29
33
|
class ModelClass(ABC):
|
30
34
|
'''
|
@@ -69,8 +73,12 @@ class ModelClass(ABC):
|
|
69
73
|
"""Load the model."""
|
70
74
|
|
71
75
|
def _handle_get_signatures_request(self) -> service_pb2.MultiOutputResponse:
|
72
|
-
methods = self.
|
73
|
-
signatures = {
|
76
|
+
methods = self._get_method_infos()
|
77
|
+
signatures = {
|
78
|
+
method.name: method.signature
|
79
|
+
for method in methods.values()
|
80
|
+
if method.signature is not None
|
81
|
+
}
|
74
82
|
resp = service_pb2.MultiOutputResponse(
|
75
83
|
status=status_pb2.Status(code=status_code_pb2.SUCCESS)
|
76
84
|
)
|
@@ -99,18 +107,61 @@ class ModelClass(ABC):
|
|
99
107
|
outputs = []
|
100
108
|
try:
|
101
109
|
# TODO add method name field to proto
|
102
|
-
|
110
|
+
# to support old callers who might not pass in the method name we have a few defaults.
|
111
|
+
# first we look for a PostModelOutputs method that is implemented as protos and use that
|
112
|
+
# if it exists.
|
113
|
+
# if not we default to 'predict'.
|
114
|
+
method_name = None
|
103
115
|
if len(request.inputs) > 0 and '_method_name' in request.inputs[0].data.metadata:
|
104
116
|
method_name = request.inputs[0].data.metadata['_method_name']
|
117
|
+
if method_name is None and FALLBACK_METHOD_PROTO in self._get_method_infos():
|
118
|
+
_info = self._get_method_infos(FALLBACK_METHOD_PROTO)
|
119
|
+
if _info.proto_method:
|
120
|
+
method_name = FALLBACK_METHOD_PROTO
|
121
|
+
if method_name is None:
|
122
|
+
method_name = FALLBACK_METHOD_PYTHON
|
105
123
|
if (
|
106
124
|
method_name == '_GET_SIGNATURES'
|
107
125
|
): # special case to fetch signatures, TODO add endpoint for this
|
108
126
|
return self._handle_get_signatures_request()
|
109
|
-
if method_name not in self.
|
127
|
+
if method_name not in self._get_method_infos():
|
110
128
|
raise ValueError(f"Method {method_name} not found in model class")
|
111
129
|
method = getattr(self, method_name)
|
112
|
-
method_info =
|
130
|
+
method_info = self._get_method_infos(method_name)
|
113
131
|
signature = method_info.signature
|
132
|
+
proto_method = method_info.proto_method
|
133
|
+
|
134
|
+
# If this is an old predict(proto) -> proto method, just call it and return
|
135
|
+
# the response.
|
136
|
+
if proto_method:
|
137
|
+
out_proto = method(request)
|
138
|
+
# if we already have out_proto.status.code set then return
|
139
|
+
if out_proto.status.code != status_code_pb2.ZERO:
|
140
|
+
return out_proto
|
141
|
+
|
142
|
+
successes = [
|
143
|
+
out.status.code == status_code_pb2.SUCCESS for out in out_proto.outputs
|
144
|
+
]
|
145
|
+
if all(successes):
|
146
|
+
# If all outputs are successful, we can return the response.
|
147
|
+
out_proto.status.CopyFrom(
|
148
|
+
status_pb2.Status(code=status_code_pb2.SUCCESS, description='Success')
|
149
|
+
)
|
150
|
+
return out_proto
|
151
|
+
if any(successes):
|
152
|
+
# If some outputs are successful and some are not, we return a mixed status.
|
153
|
+
out_proto.status.CopyFrom(
|
154
|
+
status_pb2.Status(
|
155
|
+
code=status_code_pb2.MIXED_STATUS, description='Mixed Status'
|
156
|
+
)
|
157
|
+
)
|
158
|
+
return out_proto
|
159
|
+
# If all outputs are failures, we return a failure status.
|
160
|
+
out_proto.status.CopyFrom(
|
161
|
+
status_pb2.Status(code=status_code_pb2.FAILURE, description='Failed')
|
162
|
+
)
|
163
|
+
return out_proto
|
164
|
+
|
114
165
|
python_param_types = method_info.python_param_types
|
115
166
|
for input in request.inputs:
|
116
167
|
# check if input is in old format
|
@@ -121,6 +172,7 @@ class ModelClass(ABC):
|
|
121
172
|
input.data, signature.input_fields
|
122
173
|
)
|
123
174
|
input.data.CopyFrom(new_data)
|
175
|
+
|
124
176
|
# convert inputs to python types
|
125
177
|
inputs = self._convert_input_protos_to_python(
|
126
178
|
request.inputs, signature.input_fields, python_param_types
|
@@ -148,7 +200,7 @@ class ModelClass(ABC):
|
|
148
200
|
except Exception as e:
|
149
201
|
if _RAISE_EXCEPTIONS:
|
150
202
|
raise
|
151
|
-
|
203
|
+
logger.exception("Error in predict")
|
152
204
|
return service_pb2.MultiOutputResponse(
|
153
205
|
status=status_pb2.Status(
|
154
206
|
code=status_code_pb2.FAILURE,
|
@@ -165,7 +217,7 @@ class ModelClass(ABC):
|
|
165
217
|
if len(request.inputs) > 0 and '_method_name' in request.inputs[0].data.metadata:
|
166
218
|
method_name = request.inputs[0].data.metadata['_method_name']
|
167
219
|
method = getattr(self, method_name)
|
168
|
-
method_info =
|
220
|
+
method_info = self._get_method_infos(method_name)
|
169
221
|
signature = method_info.signature
|
170
222
|
python_param_types = method_info.python_param_types
|
171
223
|
for input in request.inputs:
|
@@ -207,7 +259,7 @@ class ModelClass(ABC):
|
|
207
259
|
except Exception as e:
|
208
260
|
if _RAISE_EXCEPTIONS:
|
209
261
|
raise
|
210
|
-
|
262
|
+
logger.exception("Error in generate")
|
211
263
|
yield service_pb2.MultiOutputResponse(
|
212
264
|
status=status_pb2.Status(
|
213
265
|
code=status_code_pb2.FAILURE,
|
@@ -227,7 +279,7 @@ class ModelClass(ABC):
|
|
227
279
|
if len(request.inputs) > 0 and '_method_name' in request.inputs[0].data.metadata:
|
228
280
|
method_name = request.inputs[0].data.metadata['_method_name']
|
229
281
|
method = getattr(self, method_name)
|
230
|
-
method_info =
|
282
|
+
method_info = self._get_method_infos(method_name)
|
231
283
|
signature = method_info.signature
|
232
284
|
python_param_types = method_info.python_param_types
|
233
285
|
|
@@ -282,7 +334,7 @@ class ModelClass(ABC):
|
|
282
334
|
except Exception as e:
|
283
335
|
if _RAISE_EXCEPTIONS:
|
284
336
|
raise
|
285
|
-
|
337
|
+
logger.exception("Error in stream")
|
286
338
|
yield service_pb2.MultiOutputResponse(
|
287
339
|
status=status_pb2.Status(
|
288
340
|
code=status_code_pb2.FAILURE,
|
@@ -359,28 +411,44 @@ class ModelClass(ABC):
|
|
359
411
|
continue
|
360
412
|
methods[name] = method_info
|
361
413
|
# check for generic predict(request) -> response, etc. methods
|
362
|
-
#
|
363
|
-
#
|
364
|
-
|
365
|
-
|
366
|
-
|
414
|
+
# older models never had generate or stream so don't bother with them.
|
415
|
+
for name in [FALLBACK_METHOD_PROTO]: # , 'GenerateModelOutputs', 'StreamModelOutputs'):
|
416
|
+
if hasattr(cls, name) and name not in methods:
|
417
|
+
method = getattr(cls, name)
|
418
|
+
if not callable(method):
|
419
|
+
continue
|
420
|
+
if is_proto_style_method(method):
|
421
|
+
# If this is a proto-style method, we can add it to the registry as a special case.
|
422
|
+
methods[name] = _MethodInfo(method, proto_method=True)
|
367
423
|
# set method table for this class in the registry
|
368
424
|
return methods
|
369
425
|
|
370
426
|
@classmethod
|
371
|
-
def
|
427
|
+
def _get_method_infos(cls, func_name=None):
|
428
|
+
# FIXME: this is a re-use of the _METHOD_INFO_ATTR attribute to store the method info
|
429
|
+
# for all methods on the class. Should use a different attribute name to avoid confusion.
|
372
430
|
if not hasattr(cls, _METHOD_INFO_ATTR):
|
373
431
|
setattr(cls, _METHOD_INFO_ATTR, cls._register_model_methods())
|
374
|
-
|
432
|
+
method_infos = getattr(cls, _METHOD_INFO_ATTR)
|
375
433
|
if func_name:
|
376
|
-
return
|
377
|
-
return
|
434
|
+
return method_infos[func_name]
|
435
|
+
return method_infos
|
378
436
|
|
379
437
|
|
380
438
|
class _MethodInfo:
|
381
|
-
def __init__(self, method):
|
439
|
+
def __init__(self, method, proto_method=False):
|
440
|
+
"""Initialize a MethodInfo instance.
|
441
|
+
|
442
|
+
Args:
|
443
|
+
method: The method to wrap.
|
444
|
+
old_method: If True, this is an old proto-style method that returns a proto directly.
|
445
|
+
"""
|
382
446
|
self.name = method.__name__
|
383
|
-
self.
|
447
|
+
self.proto_method = proto_method
|
448
|
+
if not proto_method:
|
449
|
+
self.signature = build_function_signature(method)
|
450
|
+
else:
|
451
|
+
self.signature = None
|
384
452
|
self.python_param_types = {
|
385
453
|
p.name: p.annotation
|
386
454
|
for p in inspect.signature(method).parameters.values()
|
@@ -5,6 +5,8 @@ from clarifai_grpc.grpc.api.status import status_code_pb2, status_pb2
|
|
5
5
|
from clarifai_protocol import BaseRunner
|
6
6
|
from clarifai_protocol.utils.health import HealthProbeRequestHandler
|
7
7
|
|
8
|
+
from clarifai.client.auth.helper import ClarifaiAuthHelper
|
9
|
+
|
8
10
|
from ..utils.url_fetcher import ensure_urls_downloaded
|
9
11
|
from .model_class import ModelClass
|
10
12
|
|
@@ -42,6 +44,28 @@ class ModelRunner(BaseRunner, HealthProbeRequestHandler):
|
|
42
44
|
)
|
43
45
|
self.model = model
|
44
46
|
|
47
|
+
# Store authentication parameters for URL fetching
|
48
|
+
self._user_id = user_id
|
49
|
+
self._pat = pat
|
50
|
+
self._token = token
|
51
|
+
self._base_url = base_url
|
52
|
+
|
53
|
+
# Create auth helper if we have sufficient authentication information
|
54
|
+
self._auth_helper = None
|
55
|
+
if self._user_id and (self._pat or self._token):
|
56
|
+
try:
|
57
|
+
self._auth_helper = ClarifaiAuthHelper(
|
58
|
+
user_id=self._user_id,
|
59
|
+
app_id="", # app_id not needed for URL fetching
|
60
|
+
pat=self._pat or "",
|
61
|
+
token=self._token or "",
|
62
|
+
base=self._base_url,
|
63
|
+
validate=False, # Don't validate since app_id is empty
|
64
|
+
)
|
65
|
+
except Exception:
|
66
|
+
# If auth helper creation fails, proceed without authentication
|
67
|
+
self._auth_helper = None
|
68
|
+
|
45
69
|
# After model load successfully set the health probe to ready and startup
|
46
70
|
HealthProbeRequestHandler.is_ready = True
|
47
71
|
HealthProbeRequestHandler.is_startup = True
|
@@ -81,12 +105,22 @@ class ModelRunner(BaseRunner, HealthProbeRequestHandler):
|
|
81
105
|
if not runner_item.HasField('post_model_outputs_request'):
|
82
106
|
raise Exception("Unexpected work item type: {}".format(runner_item))
|
83
107
|
request = runner_item.post_model_outputs_request
|
84
|
-
ensure_urls_downloaded(request)
|
108
|
+
ensure_urls_downloaded(request, auth_helper=self._auth_helper)
|
85
109
|
|
86
110
|
resp = self.model.predict_wrapper(request)
|
87
|
-
if
|
111
|
+
# if we have any non-successful code already it's an error we can return.
|
112
|
+
if (
|
113
|
+
resp.status.code != status_code_pb2.SUCCESS
|
114
|
+
and resp.status.code != status_code_pb2.ZERO
|
115
|
+
):
|
88
116
|
return service_pb2.RunnerItemOutput(multi_output_response=resp)
|
89
|
-
successes = [
|
117
|
+
successes = []
|
118
|
+
for output in resp.outputs:
|
119
|
+
if not output.HasField('status') or not output.status.code:
|
120
|
+
raise Exception(
|
121
|
+
"Output must have a status code, please check the model implementation."
|
122
|
+
)
|
123
|
+
successes.append(output.status.code == status_code_pb2.SUCCESS)
|
90
124
|
if all(successes):
|
91
125
|
status = status_pb2.Status(
|
92
126
|
code=status_code_pb2.SUCCESS,
|
@@ -114,10 +148,14 @@ class ModelRunner(BaseRunner, HealthProbeRequestHandler):
|
|
114
148
|
if not runner_item.HasField('post_model_outputs_request'):
|
115
149
|
raise Exception("Unexpected work item type: {}".format(runner_item))
|
116
150
|
request = runner_item.post_model_outputs_request
|
117
|
-
ensure_urls_downloaded(request)
|
151
|
+
ensure_urls_downloaded(request, auth_helper=self._auth_helper)
|
118
152
|
|
119
153
|
for resp in self.model.generate_wrapper(request):
|
120
|
-
if
|
154
|
+
# if we have any non-successful code already it's an error we can return.
|
155
|
+
if (
|
156
|
+
resp.status.code != status_code_pb2.SUCCESS
|
157
|
+
and resp.status.code != status_code_pb2.ZERO
|
158
|
+
):
|
121
159
|
yield service_pb2.RunnerItemOutput(multi_output_response=resp)
|
122
160
|
continue
|
123
161
|
successes = []
|
@@ -151,7 +189,11 @@ class ModelRunner(BaseRunner, HealthProbeRequestHandler):
|
|
151
189
|
) -> Iterator[service_pb2.RunnerItemOutput]:
|
152
190
|
# Call the generate() method the underlying model implements.
|
153
191
|
for resp in self.model.stream_wrapper(pmo_iterator(runner_item_iterator)):
|
154
|
-
if
|
192
|
+
# if we have any non-successful code already it's an error we can return.
|
193
|
+
if (
|
194
|
+
resp.status.code != status_code_pb2.SUCCESS
|
195
|
+
and resp.status.code != status_code_pb2.ZERO
|
196
|
+
):
|
155
197
|
yield service_pb2.RunnerItemOutput(multi_output_response=resp)
|
156
198
|
continue
|
157
199
|
successes = []
|
@@ -181,9 +223,9 @@ class ModelRunner(BaseRunner, HealthProbeRequestHandler):
|
|
181
223
|
yield service_pb2.RunnerItemOutput(multi_output_response=resp)
|
182
224
|
|
183
225
|
|
184
|
-
def pmo_iterator(runner_item_iterator):
|
226
|
+
def pmo_iterator(runner_item_iterator, auth_helper=None):
|
185
227
|
for runner_item in runner_item_iterator:
|
186
228
|
if not runner_item.HasField('post_model_outputs_request'):
|
187
229
|
raise Exception("Unexpected work item type: {}".format(runner_item))
|
188
|
-
ensure_urls_downloaded(runner_item.post_model_outputs_request)
|
230
|
+
ensure_urls_downloaded(runner_item.post_model_outputs_request, auth_helper=auth_helper)
|
189
231
|
yield runner_item.post_model_outputs_request
|
@@ -5,6 +5,8 @@ from typing import Iterator
|
|
5
5
|
from clarifai_grpc.grpc.api import service_pb2, service_pb2_grpc
|
6
6
|
from clarifai_grpc.grpc.api.status import status_code_pb2, status_pb2
|
7
7
|
|
8
|
+
from clarifai.client.auth.helper import ClarifaiAuthHelper
|
9
|
+
|
8
10
|
from ..utils.url_fetcher import ensure_urls_downloaded
|
9
11
|
|
10
12
|
_RAISE_EXCEPTIONS = os.getenv("RAISE_EXCEPTIONS", "false").lower() in ("true", "1")
|
@@ -23,6 +25,27 @@ class ModelServicer(service_pb2_grpc.V2Servicer):
|
|
23
25
|
"""
|
24
26
|
self.model = model
|
25
27
|
|
28
|
+
# Try to create auth helper from environment variables if available
|
29
|
+
self._auth_helper = None
|
30
|
+
try:
|
31
|
+
user_id = os.environ.get("CLARIFAI_USER_ID")
|
32
|
+
pat = os.environ.get("CLARIFAI_PAT")
|
33
|
+
token = os.environ.get("CLARIFAI_SESSION_TOKEN")
|
34
|
+
base_url = os.environ.get("CLARIFAI_API_BASE", "https://api.clarifai.com")
|
35
|
+
|
36
|
+
if user_id and (pat or token):
|
37
|
+
self._auth_helper = ClarifaiAuthHelper(
|
38
|
+
user_id=user_id,
|
39
|
+
app_id="", # app_id not needed for URL fetching
|
40
|
+
pat=pat or "",
|
41
|
+
token=token or "",
|
42
|
+
base=base_url,
|
43
|
+
validate=False, # Don't validate since app_id is empty
|
44
|
+
)
|
45
|
+
except Exception:
|
46
|
+
# If auth helper creation fails, proceed without authentication
|
47
|
+
self._auth_helper = None
|
48
|
+
|
26
49
|
def PostModelOutputs(
|
27
50
|
self, request: service_pb2.PostModelOutputsRequest, context=None
|
28
51
|
) -> service_pb2.MultiOutputResponse:
|
@@ -32,7 +55,7 @@ class ModelServicer(service_pb2_grpc.V2Servicer):
|
|
32
55
|
"""
|
33
56
|
|
34
57
|
# Download any urls that are not already bytes.
|
35
|
-
ensure_urls_downloaded(request)
|
58
|
+
ensure_urls_downloaded(request, auth_helper=self._auth_helper)
|
36
59
|
|
37
60
|
try:
|
38
61
|
return self.model.predict_wrapper(request)
|
@@ -56,7 +79,7 @@ class ModelServicer(service_pb2_grpc.V2Servicer):
|
|
56
79
|
returns an output.
|
57
80
|
"""
|
58
81
|
# Download any urls that are not already bytes.
|
59
|
-
ensure_urls_downloaded(request)
|
82
|
+
ensure_urls_downloaded(request, auth_helper=self._auth_helper)
|
60
83
|
|
61
84
|
try:
|
62
85
|
yield from self.model.generate_wrapper(request)
|
@@ -84,7 +107,7 @@ class ModelServicer(service_pb2_grpc.V2Servicer):
|
|
84
107
|
|
85
108
|
# Download any urls that are not already bytes.
|
86
109
|
for req in request:
|
87
|
-
ensure_urls_downloaded(req)
|
110
|
+
ensure_urls_downloaded(req, auth_helper=self._auth_helper)
|
88
111
|
|
89
112
|
try:
|
90
113
|
yield from self.model.stream_wrapper(request_copy)
|
@@ -18,7 +18,9 @@ def has_signature_method(
|
|
18
18
|
:param method_signatures: List of MethodSignature objects to search in.
|
19
19
|
:return: True if a method with the given name exists, False otherwise.
|
20
20
|
"""
|
21
|
-
return any(
|
21
|
+
return any(
|
22
|
+
method_signature.name == name for method_signature in method_signatures if method_signature
|
23
|
+
)
|
22
24
|
|
23
25
|
|
24
26
|
def generate_client_script(
|
@@ -153,6 +155,8 @@ model = Model("{model_ui_url}",
|
|
153
155
|
# Generate method signatures
|
154
156
|
method_signatures_str = []
|
155
157
|
for method_signature in method_signatures:
|
158
|
+
if method_signature is None:
|
159
|
+
continue
|
156
160
|
method_name = method_signature.name
|
157
161
|
client_script_str = f'response = model.{method_name}('
|
158
162
|
annotations = _get_annotations_source(method_signature)
|
@@ -174,13 +174,10 @@ class Text(MessageData):
|
|
174
174
|
|
175
175
|
|
176
176
|
class Concept(MessageData):
|
177
|
-
def __init__(self, name: str, value: float = 1.0, id: str = None
|
178
|
-
user_id: str = None, app_id: str = None):
|
177
|
+
def __init__(self, name: str, value: float = 1.0, id: str = None):
|
179
178
|
self.name = name
|
180
179
|
self.value = value
|
181
180
|
self.id = id or Concept._concept_name_to_id(name)
|
182
|
-
self.user_id = user_id
|
183
|
-
self.app_id = app_id
|
184
181
|
|
185
182
|
@staticmethod
|
186
183
|
def _concept_name_to_id(name: str):
|
@@ -189,14 +186,14 @@ class Concept(MessageData):
|
|
189
186
|
return _name
|
190
187
|
|
191
188
|
def __repr__(self) -> str:
|
192
|
-
return f"Concept(id={self.id!r}, name={self.name!r}, value={self.value
|
189
|
+
return f"Concept(id={self.id!r}, name={self.name!r}, value={self.value})"
|
193
190
|
|
194
191
|
def to_proto(self):
|
195
|
-
return ConceptProto(id=self.id, name=self.name, value=self.value
|
192
|
+
return ConceptProto(id=self.id, name=self.name, value=self.value)
|
196
193
|
|
197
194
|
@classmethod
|
198
195
|
def from_proto(cls, proto: ConceptProto) -> "Concept":
|
199
|
-
return cls(id=proto.id, name=proto.name, value=proto.value
|
196
|
+
return cls(id=proto.id, name=proto.name, value=proto.value)
|
200
197
|
|
201
198
|
|
202
199
|
class Region(MessageData):
|
@@ -210,7 +207,6 @@ class Region(MessageData):
|
|
210
207
|
track_id: str = None,
|
211
208
|
text: str = None,
|
212
209
|
id: str = None,
|
213
|
-
value: float = None,
|
214
210
|
):
|
215
211
|
if proto_region is None:
|
216
212
|
self.proto = RegionProto()
|
@@ -227,8 +223,6 @@ class Region(MessageData):
|
|
227
223
|
self.track_id = track_id
|
228
224
|
if text:
|
229
225
|
self.text = text
|
230
|
-
if value:
|
231
|
-
self.value = value
|
232
226
|
self.id = id if id is not None else uuid.uuid4().hex
|
233
227
|
elif isinstance(proto_region, RegionProto):
|
234
228
|
self.proto = proto_region
|
@@ -328,19 +322,8 @@ class Region(MessageData):
|
|
328
322
|
point_proto = PointProto(col=x, row=y, z=z)
|
329
323
|
self.proto.region_info.point.CopyFrom(point_proto)
|
330
324
|
|
331
|
-
@property
|
332
|
-
def value(self) -> float:
|
333
|
-
"""Value of the region, typically used for confidence scores."""
|
334
|
-
return self.proto.value
|
335
|
-
|
336
|
-
@value.setter
|
337
|
-
def value(self, value: float):
|
338
|
-
if not isinstance(value, (int, float)):
|
339
|
-
raise TypeError(f"Expected int or float for value, got {type(value).__name__}")
|
340
|
-
self.proto.value = float(value)
|
341
|
-
|
342
325
|
def __repr__(self) -> str:
|
343
|
-
return f"Region(id={self.id},box={self.box or []}, concepts={self.concepts or None}, point={self.point or None}, mask={self.mask or None}, track_id={self.track_id or None}
|
326
|
+
return f"Region(id={self.id},box={self.box or []}, concepts={self.concepts or None}, point={self.point or None}, mask={self.mask or None}, track_id={self.track_id or None})"
|
344
327
|
|
345
328
|
def to_proto(self) -> RegionProto:
|
346
329
|
return self.proto
|
@@ -1,3 +1,4 @@
|
|
1
|
+
import inspect
|
1
2
|
import os
|
2
3
|
import shlex
|
3
4
|
import signal
|
@@ -8,6 +9,7 @@ import time
|
|
8
9
|
|
9
10
|
import psutil
|
10
11
|
import requests
|
12
|
+
from clarifai_grpc.grpc.api import service_pb2
|
11
13
|
|
12
14
|
from clarifai.utils.logging import logger
|
13
15
|
|
@@ -133,3 +135,50 @@ def wait_for_server(base_url: str, timeout: int = None) -> None:
|
|
133
135
|
raise TimeoutError("Server did not become ready within timeout period")
|
134
136
|
except requests.exceptions.RequestException:
|
135
137
|
time.sleep(1)
|
138
|
+
|
139
|
+
|
140
|
+
def is_proto_style_method(method):
|
141
|
+
"""
|
142
|
+
Determines if the given method is likely an old-style proto method:
|
143
|
+
- Has a 'request' parameter after 'self'
|
144
|
+
- Optionally, returns a known proto response type
|
145
|
+
"""
|
146
|
+
try:
|
147
|
+
sig = inspect.signature(method)
|
148
|
+
params = list(sig.parameters.values())
|
149
|
+
|
150
|
+
# Must have at least 'self' and one argument
|
151
|
+
if len(params) < 2:
|
152
|
+
return False
|
153
|
+
|
154
|
+
# First parameter should be 'self'
|
155
|
+
if params[0].name != 'self':
|
156
|
+
return False
|
157
|
+
# Second param typically should be named 'request'
|
158
|
+
request_param = params[1]
|
159
|
+
if request_param.name != 'request':
|
160
|
+
return False
|
161
|
+
# Optionally: check annotation is a proto type
|
162
|
+
# (If signature is incomplete, this part will gracefully fall through)
|
163
|
+
return_annotation = sig.return_annotation
|
164
|
+
# If type annotation is available, check it's PostModelOutputsRequest
|
165
|
+
if (
|
166
|
+
request_param.annotation != inspect.Parameter.empty
|
167
|
+
and request_param.annotation != service_pb2.PostModelOutputsRequest
|
168
|
+
):
|
169
|
+
return False
|
170
|
+
# If return annotation is available, check it's MultiOutputResponse
|
171
|
+
if (
|
172
|
+
return_annotation != inspect.Signature.empty
|
173
|
+
and return_annotation != service_pb2.MultiOutputResponse
|
174
|
+
):
|
175
|
+
return False
|
176
|
+
if (
|
177
|
+
request_param.annotation is inspect.Parameter.empty
|
178
|
+
and return_annotation is inspect.Signature.empty
|
179
|
+
):
|
180
|
+
return True # signature OK, even if signature is empty
|
181
|
+
return True
|
182
|
+
|
183
|
+
except (ValueError, TypeError):
|
184
|
+
return False
|
@@ -5,43 +5,88 @@ import fsspec
|
|
5
5
|
from clarifai.utils.logging import logger
|
6
6
|
|
7
7
|
|
8
|
-
def download_input(input):
|
9
|
-
_download_input_data(input.data)
|
8
|
+
def download_input(input, auth_helper=None):
|
9
|
+
_download_input_data(input.data, auth_helper=auth_helper)
|
10
10
|
if input.data.parts:
|
11
11
|
for i in range(len(input.data.parts)):
|
12
|
-
_download_input_data(input.data.parts[i].data)
|
12
|
+
_download_input_data(input.data.parts[i].data, auth_helper=auth_helper)
|
13
13
|
|
14
14
|
|
15
|
-
def _download_input_data(input_data):
|
15
|
+
def _download_input_data(input_data, auth_helper=None):
|
16
16
|
"""
|
17
17
|
This function will download any urls that are not already bytes.
|
18
|
+
|
19
|
+
Args:
|
20
|
+
input_data: The input data containing URLs to download
|
21
|
+
auth_helper: Optional ClarifaiAuthHelper instance for authentication
|
18
22
|
"""
|
23
|
+
# Get auth headers if auth_helper is provided
|
24
|
+
auth_kwargs = {}
|
25
|
+
if auth_helper is not None:
|
26
|
+
auth_kwargs = _get_auth_kwargs(auth_helper)
|
27
|
+
|
19
28
|
if input_data.image.url and not input_data.image.base64:
|
20
29
|
# Download the image
|
21
|
-
with fsspec.open(input_data.image.url, 'rb') as f:
|
30
|
+
with fsspec.open(input_data.image.url, 'rb', **auth_kwargs) as f:
|
22
31
|
input_data.image.base64 = f.read()
|
23
32
|
if input_data.video.url and not input_data.video.base64:
|
24
33
|
# Download the video
|
25
|
-
with fsspec.open(input_data.video.url, 'rb') as f:
|
34
|
+
with fsspec.open(input_data.video.url, 'rb', **auth_kwargs) as f:
|
26
35
|
input_data.video.base64 = f.read()
|
27
36
|
if input_data.audio.url and not input_data.audio.base64:
|
28
37
|
# Download the audio
|
29
|
-
with fsspec.open(input_data.audio.url, 'rb') as f:
|
38
|
+
with fsspec.open(input_data.audio.url, 'rb', **auth_kwargs) as f:
|
30
39
|
input_data.audio.base64 = f.read()
|
31
40
|
if input_data.text.url and not input_data.text.raw:
|
32
41
|
# Download the text
|
33
|
-
with fsspec.open(input_data.text.url, 'r') as f:
|
42
|
+
with fsspec.open(input_data.text.url, 'r', **auth_kwargs) as f:
|
34
43
|
input_data.text.raw = f.read()
|
35
44
|
|
36
45
|
|
37
|
-
def
|
46
|
+
def _get_auth_kwargs(auth_helper):
|
47
|
+
"""
|
48
|
+
Convert ClarifaiAuthHelper metadata to fsspec-compatible kwargs.
|
49
|
+
|
50
|
+
Args:
|
51
|
+
auth_helper: ClarifaiAuthHelper instance
|
52
|
+
|
53
|
+
Returns:
|
54
|
+
dict: kwargs to pass to fsspec.open() for authentication
|
55
|
+
"""
|
56
|
+
if auth_helper is None:
|
57
|
+
return {}
|
58
|
+
|
59
|
+
try:
|
60
|
+
# Get authentication metadata from the helper
|
61
|
+
metadata = auth_helper.metadata
|
62
|
+
|
63
|
+
# Convert gRPC metadata tuples to HTTP headers dict
|
64
|
+
headers = {}
|
65
|
+
for key, value in metadata:
|
66
|
+
# Skip non-auth headers
|
67
|
+
if key in ('authorization', 'x-clarifai-session-token'):
|
68
|
+
headers[key] = value
|
69
|
+
|
70
|
+
# Return fsspec-compatible kwargs
|
71
|
+
return {'client_kwargs': {'headers': headers}}
|
72
|
+
except Exception as e:
|
73
|
+
logger.warning(f"Failed to get authentication headers: {e}")
|
74
|
+
return {}
|
75
|
+
|
76
|
+
|
77
|
+
def ensure_urls_downloaded(request, max_threads=128, auth_helper=None):
|
38
78
|
"""
|
39
79
|
This function will download any urls that are not already bytes and parallelize with a thread pool.
|
80
|
+
|
81
|
+
Args:
|
82
|
+
request: The request containing inputs with URLs to download
|
83
|
+
max_threads: Maximum number of threads to use for parallel downloads
|
84
|
+
auth_helper: Optional ClarifaiAuthHelper instance for authentication
|
40
85
|
"""
|
41
86
|
with concurrent.futures.ThreadPoolExecutor(max_workers=max_threads) as executor:
|
42
87
|
futures = []
|
43
88
|
for input in request.inputs:
|
44
|
-
futures.append(executor.submit(download_input, input))
|
89
|
+
futures.append(executor.submit(download_input, input, auth_helper))
|
45
90
|
for future in concurrent.futures.as_completed(futures):
|
46
91
|
try:
|
47
92
|
future.result()
|