· 4 years ago · Jul 16, 2021, 08:12 AM
1# -*- coding: utf-8 -*-
2"""
3Clarifai API Python Client
4"""
5
6import base64 as base64_lib
7import copy
8import logging
9import os
10import platform
11import time
12import typing # noqa
13import warnings
14
15from configparser import ConfigParser
16from enum import Enum
17from io import BytesIO
18from posixpath import join as urljoin
19from pprint import pformat
20
21import requests
22from future.moves.urllib.parse import urlparse
23from google.protobuf.struct_pb2 import Struct
24from jsonschema import validate
25from past.builtins import basestring
26
27from clarifai.errors import ApiClientError, ApiError, TokenError, UserError # noqa
28from clarifai.rest.geo import Geo, GeoBox, GeoLimit, GeoPoint
29from clarifai.rest.grpc.grpc_json_channel import GRPCJSONChannel, dict_to_protobuf, protobuf_to_dict
30from clarifai.rest.grpc.proto.clarifai.api.concept_pb2 import Concept as ConceptPB
31from clarifai.rest.grpc.proto.clarifai.api.concept_pb2 import (
32 ConceptQuery, GetConceptRequest, ListConceptsRequest, PatchConceptsRequest,
33 PostConceptsRequest, PostConceptsSearchesRequest)
34from clarifai.rest.grpc.proto.clarifai.api.data_pb2 import Data as DataPB
35from clarifai.rest.grpc.proto.clarifai.api.endpoint_pb2 import _V2
36from clarifai.rest.grpc.proto.clarifai.api.endpoint_pb2_grpc import V2Stub
37from clarifai.rest.grpc.proto.clarifai.api.input_pb2 import (DeleteInputRequest,
38 DeleteInputsRequest,
39 GetInputCountRequest, GetInputRequest)
40from clarifai.rest.grpc.proto.clarifai.api.input_pb2 import Input as InputPB
41from clarifai.rest.grpc.proto.clarifai.api.input_pb2 import (
42 ListInputsRequest, ListModelInputsRequest, PatchInputsRequest, PostInputsRequest,
43 PostModelFeedbackRequest, PostModelOutputsRequest)
44from clarifai.rest.grpc.proto.clarifai.api.model_pb2 import (DeleteModelRequest,
45 DeleteModelsRequest, GetModelRequest,
46 ListModelsRequest)
47from clarifai.rest.grpc.proto.clarifai.api.model_pb2 import Model as ModelPB
48from clarifai.rest.grpc.proto.clarifai.api.model_pb2 import ModelQuery
49from clarifai.rest.grpc.proto.clarifai.api.model_pb2 import OutputConfig as OutputConfigPB
50from clarifai.rest.grpc.proto.clarifai.api.model_pb2 import OutputInfo as OutputInfoPB
51from clarifai.rest.grpc.proto.clarifai.api.model_pb2 import (PatchModelsRequest, PostModelsRequest,
52 PostModelsSearchesRequest)
53from clarifai.rest.grpc.proto.clarifai.api.model_version_pb2 import (
54 DeleteModelVersionRequest, GetModelVersionRequest, ListModelVersionsRequest,
55 PostModelVersionMetricsRequest, PostModelVersionsRequest)
56from clarifai.rest.grpc.proto.clarifai.api.search_pb2 import (PostSearchesRequest,
57 PostSearchFeedbackRequest, Query)
58from clarifai.rest.grpc.proto.clarifai.api.workflow_pb2 import (
59 GetWorkflowRequest, ListPublicWorkflowsRequest, ListWorkflowsRequest,
60 PostWorkflowResultsRequest)
61from clarifai.rest.grpc.proto.clarifai.utils.pagination.pagination_pb2 import Pagination
62from clarifai.rest.solutions.solutions import Solutions
63# Versions are imported here to avoid breaking existing client code.
64from clarifai.versions import CLIENT_VERSION, OS_VER, PYTHON_VERSION # noqa
65
66logger = logging.getLogger('clarifai')
67logger.handlers = []
68logger.addHandler(logging.StreamHandler())
69logger.setLevel(logging.ERROR)
70
71logging.getLogger("requests").setLevel(logging.WARNING)
72
73GITHUB_TAG_ENDPOINT = 'https://api.github.com/repos/clarifai/clarifai-python/git/refs/tags'
74
75DEFAULT_TAG_MODEL = 'general-v1.3'
76
77RETRIES = 2 # if connections fail retry a couple times.
78CONNECTIONS = 20 # number of connections to maintain in pool.
79
80TOKENS_DEPRECATED_MESSAGE = (
81 "App ID/secret are deprecated, please switch to API keys. See here how: "
82 "http://help.clarifai.com/api/account-related/all-about-api-keys")
83
84
85class ClarifaiApp(object):
86 """ Clarifai Application Object
87
88 This is the entry point of the Clarifai Client API.
89 With authentication to an application, you can access
90 all the models, concepts, and inputs in this application through
91 the attributes of this class.
92
93 | To access the models: use ``app.models``
94 | To access the inputs: use ``app.inputs``
95 | To access the concepts: use ``app.concepts``
96 |
97
98 """
99
100 def __init__(
101 self, # type: ClarifaiApp
102 app_id=None, # type: typing.Optional[str]
103 app_secret=None, # type: typing.Optional[str]
104 base_url=None, # type: typing.Optional[str]
105 api_key=None, # type: typing.Optional[str]
106 quiet=True, # type: bool
107 log_level=None # type: typing.Optional[int]
108 ):
109 # type: (...) -> None
110
111 self.api = ApiClient(
112 app_id=app_id,
113 app_secret=app_secret,
114 base_url=base_url,
115 api_key=api_key,
116 quiet=quiet,
117 log_level=log_level) # type: ApiClient
118 self.solutions = Solutions(api_key) # type: Solutions
119
120 self.public_models = PublicModels(self.api) # type: PublicModels
121
122 self.concepts = Concepts(self.api) # type: Concepts
123 self.inputs = Inputs(self.api) # type: Inputs
124 self.models = Models(self.api, self.solutions) # type: Models
125 self.workflows = Workflows(self.api) # type: Workflows
126
127 """
128 Below are the shortcut functions for a more smooth transition of the v1 users
129 Also they are convenient functions for the tag only users so they do not have
130 to know the extra concepts of Inputs, Models, etc.
131 """
132
133 def tag_urls(self, urls, model_name=DEFAULT_TAG_MODEL, model_id=None):
134 # type: (typing.Union[typing.List[str], str], str, typing.Optional[str]) -> dict
135 warnings.warn('tag_* methods are deprecated. Please switch to using model.predict_* methods.',
136 DeprecationWarning)
137
138 # validate input
139 if not isinstance(urls, list) or (len(urls) > 1 and not isinstance(urls[0], basestring)):
140 raise UserError('urls must be a list of string urls')
141
142 if len(urls) > 128:
143 raise UserError('max batch size is 128')
144
145 images = [Image(url=url) for url in urls]
146
147 if model_id is not None:
148 model = Model(self.api, model_id=model_id)
149 else:
150 model = self.models.get(model_name)
151
152 res = model.predict(images)
153 return res
154
155 def tag_files(self, files, model_name=DEFAULT_TAG_MODEL, model_id=None):
156 # type: (typing.Union[typing.List[str], str], str, typing.Optional[str]) -> dict
157 warnings.warn('tag_* methods are deprecated. Please switch to using model.predict_* methods.',
158 DeprecationWarning)
159
160 # validate input
161 if not isinstance(files, list) or (len(files) > 1 and not isinstance(files[0], basestring)):
162 raise UserError('files must be a list of string file names')
163
164 if len(files) > 128:
165 raise UserError('max batch size is 128')
166
167 images = [Image(filename=filename) for filename in files]
168
169 if model_id is not None:
170 model = Model(self.api, model_id=model_id)
171 else:
172 model = self.models.get(model_name)
173
174 res = model.predict(images)
175 return res
176
177 def wait_until_inputs_delete_finish(self): # type: () -> None
178 """ Block until a current inputs deletion operation finishes
179
180 The criteria for unblocking is 0 inputs returned from GET /inputs
181
182 Returns:
183 None
184 """
185
186 inputs = self.inputs.get_by_page()
187
188 while len(inputs) > 0:
189 time.sleep(0.2)
190 inputs = self.inputs.get_by_page()
191
192 # type: (int) -> None
193 def wait_until_inputs_upload_finish(self, max_wait=666666):
194 """ Block until the inputs upload finishes
195
196 The criteria for unblocking is 0 "to_process" inputs
197 from GET /inputs/status
198
199 Returns:
200 None
201 """
202 to_process = 1
203 elapsed = 0.0
204 time_start = time.time()
205
206 while to_process != 0 and elapsed > max_wait:
207 status = self.inputs.check_status()
208 to_process = status.to_process
209 elapsed = time.time() - time_start
210 time.sleep(1)
211
212 def wait_until_models_delete_finish(self): # type: () -> None
213 """ Block until the inputs deletion finishes
214
215 The criteria for unblocking is 0 models returned from GET /models
216
217 Returns:
218 None
219 """
220
221 private_models = list(self.models.get_all(private_only=True))
222
223 while len(private_models) > 0:
224 time.sleep(0.2)
225 private_models = list(self.models.get_all(private_only=True))
226
227
228class Input(object):
229 """ The Clarifai Input object
230 """
231
232 def __init__(
233 self, # type: Input
234 input_id=None, # type: typing.Optional[str]
235 concepts=None, # type: typing.Optional[typing.List[str]]
236 not_concepts=None, # type: typing.Optional[typing.List[str]]
237 metadata=None, # type: typing.Optional[dict]
238 geo=None, # type: typing.Optional[Geo]
239 regions=None, # type: typing.Optional[typing.List[Region]]
240 feedback_info=None # type: typing.Optional[FeedbackInfo]
241 ):
242 # type: (...) -> None
243 """ Construct an Image/Video object. it must have one of url or file_obj set.
244 Args:
245 input_id: unique id to set for the image. If None then the server will create and return
246 one for you.
247 concepts: a list of concept names this asset is associated with
248 not_concepts: a list of concept names this asset does not associate with
249 metadata: metadata as a JSON object to associate arbitrary info with the input
250 geo: geographical info for the input, as a Geo() object
251 regions: regions of Region object
252 feedback_info: FeedbackInfo object
253 """
254
255 self.input_id = input_id
256
257 if concepts and not isinstance(concepts, (list, tuple)) and concepts:
258 raise UserError('concepts should be a list or tuple')
259
260 if not_concepts and not isinstance(not_concepts, (list, tuple)):
261 raise UserError('not_concepts should be a list or tuple')
262
263 if metadata and not isinstance(metadata, dict):
264 raise UserError('metadata should be a dictionary')
265
266 # validate geo
267 if geo and not isinstance(geo, Geo):
268 raise UserError('geo should be a Geo object')
269
270 # validate more
271 if (regions and not isinstance(regions, list) and
272 not all(isinstance(r, Region) for r in regions)):
273 raise UserError('regions should be a list of Region')
274
275 if feedback_info and not isinstance(feedback_info, FeedbackInfo):
276 raise UserError('feedback_info should be a FeedbackInfo object')
277
278 self.concepts = concepts
279 self.not_concepts = not_concepts
280 self.metadata = metadata
281 self.geo = geo
282 self.feedback_info = feedback_info
283 self.regions = regions
284 self.score = 0 # type: int
285 self.status = None # type: ApiStatus
286
287 def dict(self): # type: () -> dict
288 """ Return the data of the Input as a dict ready to be input to json.dumps. """
289 data = {}
290 positive_concepts = [(name, True) for name in (self.concepts or [])]
291 negative_concepts = [(name, False)
292 for name in (self.not_concepts or [])]
293 concepts = positive_concepts + negative_concepts
294 if concepts:
295 data['concepts'] = [{'id': name, 'value': value}
296 for name, value in concepts]
297 if self.metadata:
298 data['metadata'] = self.metadata
299 if self.geo:
300 data.update(self.geo.dict())
301 if self.regions:
302 data['regions'] = [r.dict() for r in self.regions]
303
304 input_ = {}
305 if self.input_id:
306 input_['id'] = self.input_id
307 if self.feedback_info:
308 input_.update(self.feedback_info.dict())
309 if data:
310 input_['data'] = data
311
312 return input_
313
314
315class Image(Input):
316
317 def __init__(
318 self, # type: Image
319 url=None, # type: typing.Optional[str]
320 file_obj=None, # type: typing.Optional[typing.Any]
321 base64=None, # type: typing.Optional[typing.Union[str, bytes]]
322 filename=None, # type: typing.Optional[str]
323 crop=None, # type: typing.Optional[BoundingBox]
324 image_id=None, # type: typing.Optional[str]
325 concepts=None, # type: typing.Optional[typing.List[str]]
326 not_concepts=None, # type: typing.Optional[typing.List[str]]
327 regions=None, # type: typing.Optional[typing.List[Region]]
328 metadata=None, # type: typing.Optional[dict]
329 geo=None, # type: typing.Optional[Geo]
330 feedback_info=None, # type: typing.Optional[FeedbackInfo]
331 allow_dup_url=False # type: bool
332 ):
333 # type: (...) -> None
334 """ construct an image
335
336 Args:
337 url: the url to a publically accessible image.
338 file_obj: a file-like object in which read() will give you the bytes.
339 crop: a list of float in the range 0-1.0 in the order [top, left, bottom, right] to crop out
340 the asset before use.
341 image_id: the image ID
342 concepts: the concepts associated with the image
343 not_concepts: the concepts not associated with the image
344 regions: regions of an image
345 metadata: the metadata attached to the image
346 geo: geographical information about the image
347 feedback_info: feedback information
348 allow_dup_url: whether to allow duplicate URLs
349 """
350
351 super(Image, self).__init__(
352 image_id,
353 concepts,
354 not_concepts,
355 metadata=metadata,
356 geo=geo,
357 regions=regions,
358 feedback_info=feedback_info)
359
360 if crop and (not isinstance(crop, list) or len(crop) != 4):
361 raise UserError("crop arg must be list of 4 floats or None")
362
363 self.url = url.strip() if url else url
364 self.file_obj = file_obj
365 self.filename = filename
366 self.base64 = base64
367 self.crop = crop
368 self.allow_dup_url = allow_dup_url
369
370 we_opened_file = False
371
372 # override the filename with the fileobj as fileobj
373 if self.filename is not None:
374 if not os.path.exists(self.filename):
375 raise UserError(
376 "Invalid file path %s. Please check!" % self.filename)
377 elif not os.path.isfile(self.filename):
378 raise UserError(
379 "Not a regular file %s. Please check!" % self.filename)
380
381 self.file_obj = open(self.filename, 'rb')
382 self.filename = None
383
384 we_opened_file = True
385
386 if self.file_obj:
387 if hasattr(self.file_obj, 'mode') and self.file_obj.mode != 'rb':
388 raise UserError(
389 ("If you're using open(), then you need to read bytes using the 'rb' mode. "
390 "For example: open(filename, 'rb')"))
391
392 # DO NOT put 'read' as first condition
393 # as io.BytesIO() has both read() and getvalue() and read() gives you an empty buffer...
394 if hasattr(self.file_obj, 'getvalue'):
395 self.file_obj.seek(0)
396 self.base64 = base64_lib.b64encode(file_obj.getvalue())
397 elif hasattr(self.file_obj, 'read'):
398 self.file_obj.seek(0)
399 self.base64 = base64_lib.b64encode(self.file_obj.read())
400 else:
401 raise UserError("Not sure how to read your file_obj")
402
403 # Only close the file if we opened it. The users are responsible for closing
404 # their open files.
405 if we_opened_file:
406 self.file_obj.close()
407
408 def dict(self): # type: () -> dict
409
410 data = super(Image, self).dict()
411
412 image = {}
413
414 if self.base64:
415 image['base64'] = self.base64.decode('UTF-8')
416 if self.url:
417 image['url'] = self.url
418 if self.crop:
419 image['crop'] = self.crop
420 if self.allow_dup_url:
421 image['allow_duplicate_url'] = self.allow_dup_url
422
423 if image:
424 image_data = {'image': image}
425 if 'data' in data:
426 data['data'].update(image_data)
427 else:
428 data['data'] = image_data
429 return data
430
431
432class Video(Input):
433
434 def __init__(
435 self, # type: Video
436 url=None, # type: typing.Optional[str]
437 file_obj=None, # type: typing.Optional[typing.Any]
438 base64=None, # type: typing.Optional[typing.Union[str, bytes]]
439 filename=None, # type: typing.Optional[str]
440 video_id=None # type: typing.Optional[str]
441 ):
442 # type: (...) -> None
443 """
444 url: the url to a publicly accessible video.
445 file_obj: a file-like object in which read() will give you the bytes.
446 base64: base64 encoded string for the video
447 filename: a local file name
448 video_id: user-defined identifier of this video
449 """
450
451 super(Video, self).__init__(input_id=video_id)
452
453 self.url = url.strip() if url else url
454 self.file_obj = file_obj
455 self.filename = filename
456 self.base64 = base64
457
458 we_opened_file = False
459
460 # override the filename with the fileobj as fileobj
461 if self.filename is not None:
462 if not os.path.exists(self.filename):
463 raise UserError("Invalid file path %s. Please check!")
464 elif not os.path.isfile(self.filename):
465 raise UserError("Not a regular file %s. Please check!")
466
467 self.file_obj = open(self.filename, 'rb')
468 self.filename = None
469
470 we_opened_file = True
471
472 if self.file_obj is not None:
473 if hasattr(self.file_obj, 'mode') and self.file_obj.mode != 'rb':
474 raise UserError(
475 ("If you're using open(), then you need to read bytes using the 'rb' mode. "
476 "For example: open(filename, 'rb')"))
477
478 # DO NOT put 'read' as first condition
479 # as io.BytesIO() has both read() and getvalue() and read() gives you an empty buffer...
480 if hasattr(self.file_obj, 'getvalue'):
481 self.file_obj.seek(0)
482 self.base64 = base64_lib.b64encode(self.file_obj.getvalue())
483 elif hasattr(self.file_obj, 'read'):
484 self.file_obj.seek(0)
485 self.base64 = base64_lib.b64encode(self.file_obj.read())
486 else:
487 raise UserError("Not sure how to read your file_obj")
488
489 # Only close the file if we opened it. The users are responsible for closing
490 # their open files.
491 if we_opened_file:
492 self.file_obj.close()
493
494 def dict(self): # type: () -> dict
495
496 data = super(Video, self).dict()
497
498 video = {'video': {}}
499
500 if self.base64 is not None:
501 video['video']['base64'] = self.base64.decode('UTF-8')
502 else:
503 video['video']['url'] = self.url
504
505 if 'data' in data:
506 data['data'].update(video)
507 else:
508 data['data'] = video
509 return data
510
511
512class FeedbackType(Enum):
513 """ Enum class for feedback type """
514
515 accurate = 1
516 misplaced = 2
517 not_detected = 3
518 false_positive = 4
519
520
521class FeedbackInfo(object):
522 """
523 FeedbackInfo holds the metadata of a feedback
524 """
525
526 def __init__(
527 self, # type: FeedbackInfo
528 end_user_id=None, # type: typing.Optional[str]
529 session_id=None, # type: typing.Optional[str]
530 event_type=None, # type: typing.Optional[str]
531 output_id=None, # type: typing.Optional[str]
532 search_id=None # type: typing.Optional[str]
533 ):
534 # type: (...) -> None
535
536 self.end_user_id = end_user_id
537 self.session_id = session_id
538 self.event_type = event_type
539 self.output_id = output_id
540 self.search_id = search_id
541
542 def dict(self): # type: () -> dict
543
544 data = {
545 "feedback_info": {
546 "end_user_id": self.end_user_id,
547 "session_id": self.session_id,
548 "event_type": self.event_type,
549 }
550 }
551
552 if self.output_id:
553 data['feedback_info']['output_id'] = self.output_id
554
555 if self.search_id:
556 data['feedback_info']['search_id'] = self.search_id
557
558 return data
559
560
561class SearchTerm(object):
562 """
563 Clarifai search term interface. This is the base class for InputSearchTerm and OutputSearchTerm
564
565 It is used to build SearchQueryBuilder
566 """
567
568 def __init__(self): # type: () -> None
569 pass # if changed, please also change the type hint for this function
570
571 def dict(self): # type: () -> None
572 pass # if changed, please also change the type hint for this function
573
574
575class InputSearchTerm(SearchTerm):
576 """
577 Clarifai Input Search Term for an image search.
578 For input search, you can specify search terms for url string match, input_id string match,
579 concept string match, concept_id string match, and geographic information.
580 Value indicates whether the concept search is a NOT search
581
582 Examples:
583 >>> # search for url, string match
584 >>> InputSearchTerm(url='http://blabla')
585 >>> # search for input ID, string match
586 >>> InputSearchTerm(input_id='site1_bla')
587 >>> # search for annotated concept
588 >>> InputSearchTerm(concept='tag1')
589 >>> # search for not the annotated concept
590 >>> InputSearchTerm(concept='tag1', value=False)
591 >>> # search for metadata
592 >>> InputSearchTerm(metadata={'key':'value'})
593 >>> # search for geo
594 >>> InputSearchTerm(geo=Geo(geo_point=GeoPoint(-40, 30),
595 >>> geo_limit=GeoLimit('withinMiles', 10)))
596 """
597
598 def __init__(
599 self, # type: InputSearchTerm
600 url=None, # type: typing.Optional[str]
601 input_id=None, # type: typing.Optional[str]
602 concept=None, # type: typing.Optional[str]
603 concept_id=None, # type: typing.Optional[str]
604 value=True, # type: typing.Optional[typing.Union[bool, float]]
605 metadata=None, # type: typing.Optional[dict]
606 geo=None # type: typing.Optional[Geo]
607 ):
608 self.url = url
609 self.input_id = input_id
610 self.concept = concept
611 self.concept_id = concept_id
612 self.value = value
613 self.metadata = metadata
614 self.geo = geo
615
616 def dict(self): # type: () -> dict
617 if self.url:
618 obj = {"input": {"data": {"image": {"url": self.url}}}}
619 elif self.input_id:
620 obj = {"input": {"id": self.input_id, "data": {"image": {}}}}
621 elif self.concept:
622 obj = {"input": {"data": {"concepts": [
623 {"name": self.concept, "value": self.value}]}}}
624 elif self.concept_id:
625 obj = {"input": {"data": {"concepts": [
626 {"id": self.concept_id, "value": self.value}]}}}
627 elif self.metadata:
628 obj = {"input": {"data": {"metadata": self.metadata}}}
629 elif self.geo:
630 obj = {"input": {"data": {}}}
631 obj['input']['data'].update(self.geo.dict())
632
633 return obj
634
635
636class OutputSearchTerm(SearchTerm):
637 """
638 Clarifai Output Search Term for image search.
639 For output search, you can specify search term for url, base64, and input_id for
640 visual search,
641 or specify concept and concept_id for string match.
642 Value indicates whether the concept search is a NOT search
643
644 Examples:
645 >>> # search for visual similarity from url
646 >>> OutputSearchTerm(url='http://blabla')
647 >>> # search for visual similarity from base64 encoded image
648 >>> OutputSearchTerm(base64='sdfds')
649 >>> # search for visual similarity from input id
650 >>> OutputSearchTerm(input_id='site1_bla')
651 >>> # search for predicted concept
652 >>> OutputSearchTerm(concept='tag1')
653 >>> # search for not the predicted concept
654 >>> OutputSearchTerm(concept='tag1', value=False)
655 """
656
657 def __init__(
658 self, # type: OutputSearchTerm
659 url=None, # type: typing.Optional[str]
660 base64=None, # type: typing.Optional[typing.Union[str, bytes]]
661 input_id=None, # type: typing.Optional[str]
662 concept=None, # type: typing.Optional[str]
663 concept_id=None, # type: typing.Optional[str]
664 value=True, # type: typing.Optional[typing.Union[bool, float]]
665 crop=None # type: typing.Optional[BoundingBox]
666 ):
667 self.url = url
668 self.base64 = base64
669 self.input_id = input_id
670 self.concept = concept
671 self.concept_id = concept_id
672 self.value = value
673 self.crop = crop
674
675 def dict(self): # type: () -> dict
676 if self.url:
677 obj = {"output": {"input": {"data": {"image": {"url": self.url}}}}}
678
679 # add crop as needed
680 if self.crop:
681 obj['output']['input']['data']['image']['crop'] = self.crop
682
683 if self.base64:
684 obj = {"output": {"input": {"data": {"image": {"base64": self.base64}}}}}
685
686 # add crop as needed
687 if self.crop:
688 obj['output']['input']['data']['image']['crop'] = self.crop
689
690 elif self.input_id:
691 obj = {"output": {"input": {"id": self.input_id, "data": {"image": {}}}}}
692
693 # add crop as needed
694 if self.crop:
695 obj['output']['input']['data']['image']['crop'] = self.crop
696
697 elif self.concept:
698 obj = {"output": {"data": {"concepts": [
699 {"name": self.concept, "value": self.value}]}}}
700
701 elif self.concept_id:
702 obj = {"output": {"data": {"concepts": [
703 {"id": self.concept_id, "value": self.value}]}}}
704
705 return obj
706
707
708class SearchQueryBuilder(object):
709 """
710 Clarifai Image Search Query Builder
711
712 This builder is for advanced search use ONLY.
713
714 If you are looking for simple concept search, or simple image similarity search,
715 you should use one of the existing functions ``search_by_annotated_concepts``,
716 ``search_by_predicted_concepts``,
717 ``search_by_image`` or ``search_by_metadata``
718
719 Currently the query builder only supports a list of query terms with AND.
720 InputSearchTerm and OutputSearchTerm are the only terms supported by the query builder
721
722 Examples:
723 >>> qb = SearchQueryBuilder()
724 >>> qb.add_term(term1)
725 >>> qb.add_term(term2)
726 >>>
727 >>> app.inputs.search(qb)
728 >>>
729 >>> # for search over translated output concepts
730 >>> qb = SearchQueryBuilder(language='zh')
731 >>> qb.add_term(term1)
732 >>> qb.add_term(term2)
733 >>>
734 >>> app.inputs.search(qb)
735
736 """
737
738 def __init__(self, language=None): # type: (typing.Optional[str]) -> None
739 self.terms = [
740 ] # type: typing.List[typing.Optional[typing.Union[InputSearchTerm, OutputSearchTerm]]]
741 self.language = language
742
743 def add_term(self, term):
744 # type: (typing.Optional[typing.Union[InputSearchTerm, OutputSearchTerm]]) -> None
745 """ add a search term to the query.
746 This can search by input or by output.
747 Construct the term argument with an InputSearchTerm
748 or OutputSearchTerm object.
749 """
750 if not isinstance(term, InputSearchTerm) and not isinstance(term, OutputSearchTerm):
751 raise UserError(
752 'first level search term could be only InputSearchTerm, OutputSearchTerm')
753
754 self.terms.append(term)
755
756 def dict(self): # type: () -> dict
757 """ construct the raw query for the RESTful API """
758
759 query = {"ands": [term.dict() for term in self.terms]}
760
761 if self.language is not None:
762 query.update({'language': self.language})
763
764 return query
765
766
767class Workflow(object):
768 """ the workflow class
769 has the workflow attributes and a list of models associated with it
770 """
771
772 api = None # type: ApiClient
773
774 def __init__(self, api, workflow=None, workflow_id=None):
775 # type: (ApiClient, dict, str) -> None
776
777 self.api = api
778
779 if workflow is not None:
780 self.wf_id = workflow['id'] # type: str
781 if workflow.get('nodes'):
782 self.nodes = [WorkflowNode(node) for node in workflow['nodes']]
783 else:
784 self.nodes = []
785 elif workflow_id is not None:
786 self.wf_id = workflow_id # type: str
787 self.nodes = []
788
789 def dict(self): # type: () -> dict
790 obj = {
791 'id': self.wf_id,
792 }
793
794 if self.nodes:
795 obj['nodes'] = [node.dict() for node in self.nodes]
796
797 return obj
798
799 def predict_by_url(
800 self, # type: Workflow
801 url, # type: str
802 lang=None, # type: typing.Optional[str]
803 is_video=False, # type: typing.Optional[bool]
804 min_value=None, # type: typing.Optional[float]
805 max_concepts=None, # type: typing.Optional[int]
806 select_concepts=None # type: typing.Optional[typing.List[Concept]]
807 ):
808 # type: (...) -> dict
809 """ predict a model with url
810
811 Args:
812 url: publicly accessible url of an image
813 lang: language to predict, if the translation is available
814 is_video: whether this is a video
815 min_value: threshold to cut the predictions, 0-1.0
816 max_concepts: max concepts to keep in the predictions, 0-200
817 select_concepts: a list of concepts that are selected to be exposed
818
819 Returns:
820 the prediction of the model in JSON format
821 """
822
823 url = url.strip()
824
825 if is_video is True:
826 input_ = Video(url=url)
827 else:
828 input_ = Image(url=url)
829
830 output_config = ModelOutputConfig(
831 language=lang,
832 min_value=min_value,
833 max_concepts=max_concepts,
834 select_concepts=select_concepts)
835
836 res = self.predict([input_], output_config)
837 return res
838
839 def predict_by_filename(
840 self, # type: Workflow
841 filename, # type: str
842 lang=None, # type: typing.Optional[str]
843 is_video=False, # type: typing.Optional[bool]
844 min_value=None, # type: typing.Optional[float]
845 max_concepts=None, # type: typing.Optional[int]
846 select_concepts=None # type: typing.Optional[typing.List[Concept]]
847 ):
848 # type: (...) -> dict
849 """ predict a model with a local filename
850
851 Args:
852 filename: filename on local filesystem
853 lang: language to predict, if the translation is available
854 is_video: whether this is a video
855 min_value: threshold to cut the predictions, 0-1.0
856 max_concepts: max concepts to keep in the predictions, 0-200
857 select_concepts: a list of concepts that are selected to be exposed
858
859 Returns:
860 the prediction of the model in JSON format
861 """
862
863 fileio = open(filename, 'rb')
864
865 if is_video is True:
866 input_ = Video(file_obj=fileio)
867 else:
868 input_ = Image(file_obj=fileio)
869
870 output_config = ModelOutputConfig(
871 language=lang,
872 min_value=min_value,
873 max_concepts=max_concepts,
874 select_concepts=select_concepts)
875
876 res = self.predict([input_], output_config)
877 return res
878
879 def predict_by_bytes(
880 self, # type: Workflow
881 raw_bytes, # type: bytes
882 lang=None, # type: typing.Optional[str]
883 is_video=False, # type: typing.Optional[bool]
884 min_value=None, # type: typing.Optional[float]
885 max_concepts=None, # type: typing.Optional[int]
886 select_concepts=None # type: typing.Optional[typing.List[Concept]]
887 ):
888 # type: (...) -> dict
889 """ predict a model with image raw bytes
890
891 Args:
892 raw_bytes: raw bytes of an image
893 lang: language to predict, if the translation is available
894 is_video: whether this is a video
895 min_value: threshold to cut the predictions, 0-1.0
896 max_concepts: max concepts to keep in the predictions, 0-200
897 select_concepts: a list of concepts that are selected to be exposed
898
899 Returns:
900 the prediction of the model in JSON format
901 """
902
903 base64_bytes = base64_lib.b64encode(raw_bytes)
904
905 if is_video is True:
906 input_ = Video(base64=base64_bytes)
907 else:
908 input_ = Image(base64=base64_bytes)
909
910 output_config = ModelOutputConfig(
911 language=lang,
912 min_value=min_value,
913 max_concepts=max_concepts,
914 select_concepts=select_concepts)
915
916 res = self.predict([input_], output_config)
917 return res
918
919 def predict_by_base64(
920 self, # type: Workflow
921 base64_bytes, # type: str
922 lang=None, # type: typing.Optional[str]
923 is_video=False, # type: typing.Optional[bool]
924 min_value=None, # type: typing.Optional[float]
925 max_concepts=None, # type: typing.Optional[int]
926 select_concepts=None # type: typing.Optional[typing.List[Concept]]
927 ):
928 # type: (...) -> dict
929 """ predict a model with base64 encoded image bytes
930
931 Args:
932 base64_bytes: base64 encoded image bytes
933 lang: language to predict, if the translation is available
934 is_video: whether this is a video
935 min_value: threshold to cut the predictions, 0-1.0
936 max_concepts: max concepts to keep in the predictions, 0-200
937 select_concepts: a list of concepts that are selected to be exposed
938
939 Returns:
940 the prediction of the model in JSON format
941 """
942
943 if is_video is True:
944 input_ = Video(base64=base64_bytes)
945 else:
946 input_ = Image(base64=base64_bytes)
947
948 model_output_config = ModelOutputConfig(
949 language=lang,
950 min_value=min_value,
951 max_concepts=max_concepts,
952 select_concepts=select_concepts)
953
954 res = self.predict([input_], model_output_config)
955 return res
956
957 def predict(self, inputs, output_config=None):
958 # type: (typing.List[typing.Union[Input]], ModelOutputConfig) -> dict
959 """ predict with multiple images
960
961 Args:
962 inputs: a list of Image objectsg
963 output_config: output_config for more prediction parameters
964
965 Returns:
966 the prediction of the model in JSON format
967 """
968
969 res = self.api.predict_workflow(self.wf_id, inputs, output_config)
970 return res
971
972
973class WorkflowNode(object):
974 """ the node in the workflow
975 """
976
977 def __init__(self, wf_node): # type: (dict) -> None
978 self.node_id = wf_node['id'] # type: str
979 self.model_id = wf_node['model']['id'] # type: str
980 # type: str
981 self.model_version_id = wf_node['model']['model_version']['id']
982
983 def dict(self): # type: () -> dict
984 node = {
985 'id': self.node_id,
986 'model': {
987 'id': self.model_id,
988 'model_version': {
989 'id': self.model_version_id
990 }
991 }
992 }
993 return node
994
995
996class Workflows(object):
997
998 def __init__(self, api): # type: (ApiClient) -> None
999 self.api = api # type: ApiClient
1000
1001 def get_all(self, public_only=False):
1002 # type: (typing.Optional[bool]) -> typing.Generator[Workflow, None, None]
1003 """ get all workflows in the application
1004
1005 Args:
1006 public_only: whether to get public workflow
1007
1008 Returns:
1009 a generator that yields Workflow object
1010
1011 Examples:
1012 >>> for workflow in app.workflows.get_all():
1013 >>> print(workflow.id)
1014 """
1015
1016 res = self.api.get_workflows(public_only)
1017
1018 # FIXME(robert): hack to correct the empty workflow
1019 if not res.get('workflows'):
1020 res['workflows'] = []
1021
1022 if not res['workflows']:
1023 return
1024
1025 for one in res['workflows']:
1026 workflow = Workflow(self.api, one)
1027 yield workflow
1028
1029 def get_by_page(self, public_only=False, page=1, per_page=20):
1030 # type: (bool, int, int) -> typing.List[Workflow]
1031 """ get paginated workflows from the application
1032
1033 When the number of workflows get high, you may want to get
1034 the paginated results from all the models
1035
1036 Args:
1037 public_only: whether to get public workflow
1038 page: page number
1039 per_page: number of models returned in one page
1040
1041 Returns:
1042 a list of Workflow objects
1043
1044 Examples:
1045 >>> workflows = app.workflows.get_by_page(2, 20)
1046 """
1047
1048 res = self.api.get_workflows(public_only)
1049 results = [Workflow(self.api, one) for one in res['workflows']]
1050
1051 return results
1052
1053 def get(self, workflow_id): # type: (str) -> Workflow
1054 """ get workflow by id
1055
1056 Args:
1057 workflow_id: ID of the workflow
1058
1059 Returns:
1060 A Workflow object or None
1061
1062 Examples:
1063 >>> workflow = app.workflows.get('General')
1064 """
1065
1066 res = self.api.get_workflow(workflow_id)
1067 workflow = Workflow(self.api, res['workflow'])
1068 return workflow
1069
1070
1071class Models(object):
1072
1073 def __init__(self, api, solutions): # type: (ApiClient, Solutions) -> None
1074 self.api = api # type: ApiClient
1075 self.solutions = solutions # type: Solutions
1076
1077 # the cache of the model name -> model id mapping
1078 # to avoid an extra model query on every prediction by model name
1079 self.model_id_cache = self.init_model_cache()
1080
1081 def init_model_cache(self):
1082 # type: () -> typing.Dict[typing.Tuple[typing.Optional[str], typing.Optional[str]], str]
1083 """ Initialize the model cache for the public models
1084
1085 This will go through all public models and cache them
1086
1087 Returns:
1088 JSON object containing the name, type, and id of all cached models
1089 """
1090
1091 model_cache = {}
1092
1093 # this is a generator, will NOT raise Exception
1094 models = self.get_all(public_only=True)
1095
1096 try:
1097 for m in models:
1098 try:
1099 # print(m.output_info['output_config'])
1100 model_name = m.model_name
1101 model_type = m.output_info['type']
1102 model_id = m.model_id
1103 model_cache.update({(model_name, model_type): model_id})
1104
1105 # for general-v1.3 concept model, make an extra cache entry
1106 if model_name == 'general-v1.3' and model_type == 'concept':
1107 model_cache.update({(model_name, None): model_id})
1108 except:
1109 pass
1110 except ApiError as e:
1111 if e.error_code == 11007:
1112 logger.debug(
1113 "not authorized to call GET /models. Unable to cache models")
1114 else:
1115 raise e
1116
1117 return model_cache
1118
1119 def clear_model_cache(self): # type: () -> None
1120 """ clear model_name -> model_id cache
1121
1122 WARNING: This is an internal function, user should not call this
1123
1124 We cache model_name to model_id mapping for API efficiency.
1125 The first time you call a models.get() by name, the name to ID
1126 mapping is saved so next time there is no query. Then user does not
1127 have to query the model ID every time when they want to work on it.
1128 """
1129
1130 self.model_id_cache = {}
1131
1132 def create(
1133 self, # type: Models
1134 model_id, # type: str
1135 model_name=None, # type: typing.Optional[str]
1136 concepts=None, # type: typing.Optional[typing.List[str]]
1137 concepts_mutually_exclusive=False, # type: bool
1138 closed_environment=False, # type: bool
1139 hyper_parameters=None # type: typing.Optional[dict]
1140 ):
1141 # type (...) -> Model
1142 """ Create a new model
1143
1144 Args:
1145 model_id: ID of the model
1146 model_name: optional name of the model
1147 concepts: optional concepts to be associated with this model
1148 concepts_mutually_exclusive: True or False, whether concepts are mutually exclusive
1149 closed_environment: True or False, whether to use negatives for prediction
1150 hyper_parameters: hyper parameters for the model, with a json object
1151
1152 Returns:
1153 Model object
1154
1155 Examples:
1156 >>> # create a model with no concepts
1157 >>> app.models.create('my_model1')
1158 >>> # create a model with a few concepts
1159 >>> app.models.create('my_model2', concepts=['bird', 'fish'])
1160 >>> # create a model with closed environment
1161 >>> app.models.create('my_model3', closed_environment=True)
1162 """
1163 if not model_name:
1164 model_name = model_id
1165
1166 res = self.api.create_model(model_id, model_name, concepts, concepts_mutually_exclusive,
1167 closed_environment, hyper_parameters)
1168
1169 if res.get('model'):
1170 model = self._to_obj(res['model'])
1171 elif res.get('status'):
1172 status = res['status']
1173 raise UserError('code: %d, desc: %s, details: %s' % (status['code'], status['description'],
1174 status['details']))
1175 else:
1176 raise NotImplementedError('The response returned no model and no status, unable to handle'
1177 'such response in the client')
1178
1179 return model
1180
1181 def _is_public(self, model): # type: (Model) -> bool
1182 """ use app_id to determine whether it is a public model
1183
1184 For public model, the app_id is either '' or 'main'
1185 For private model, the app_id is not empty but not 'main'
1186 """
1187 return model.app_id == '' or model.app_id == 'main'
1188
1189 def get_all(self, public_only=False, private_only=False):
1190 # type: (bool, bool) -> typing.Generator[Model, None, None]
1191 """ Get all models in the application
1192
1193 Args:
1194 public_only: only yield public models
1195 private_only: only yield private models that tie to your own account
1196
1197 Returns:
1198 a generator function that yields Model objects
1199
1200 Examples:
1201 >>> for model in app.models.get_all():
1202 >>> print(model.model_name)
1203 """
1204
1205 page = 1
1206 per_page = 20
1207
1208 while True:
1209 res = self.api.get_models(page, per_page)
1210
1211 if not res['models']:
1212 break
1213
1214 for one in res['models']:
1215 model = self._to_obj(one)
1216
1217 if public_only is True and not self._is_public(model):
1218 continue
1219
1220 if private_only is True and self._is_public(model):
1221 continue
1222
1223 yield model
1224
1225 page += 1
1226
1227 def get_by_page(self, public_only=False, private_only=False, page=1, per_page=20):
1228 # type: (bool, bool, int, int) -> typing.List[Model]
1229 """ get paginated models from the application
1230
1231 When the number of models gets high, you may want to get
1232 the paginated results from all the models
1233
1234 Args:
1235 public_only: only yield public models
1236 private_only: only yield private models that tie to your own account
1237 page: page number
1238 per_page: number of models returned in one page
1239
1240 Returns:
1241 a list of Model objects
1242
1243 Examples:
1244 >>> models = app.models.get_by_page(2, 20)
1245 """
1246
1247 res = self.api.get_models(page, per_page)
1248 results = [self._to_obj(one) for one in res['models']]
1249
1250 if public_only:
1251 results = filter(lambda m: self._is_public(m), results)
1252 elif private_only:
1253 results = filter(lambda m: not self._is_public(m), results)
1254
1255 return results
1256
1257 # type: (str, typing.Optional[str]) -> dict
1258 def delete(self, model_id, version_id=None):
1259 """ delete the model, or a specific version of the model
1260
1261 Without model version id specified, all the versions associated with this model
1262 will be deleted as well.
1263
1264 With model version id specified, it will delete a
1265 particular model version from the model
1266
1267 Args:
1268 model_id: the unique ID of the model
1269 version_id: the unique ID of the model version
1270
1271 Returns:
1272 the raw JSON response from the server
1273
1274 Examples:
1275 >>> # delete a model
1276 >>> app.models.delete('model_id1')
1277 >>> # delete a model version
1278 >>> app.models.delete('model_id1', version_id='version1')
1279 """
1280
1281 if not version_id:
1282 res = self.api.delete_model(model_id)
1283 else:
1284 res = self.api.delete_model_version(model_id, version_id)
1285
1286 return res
1287
1288 def bulk_delete(self, model_ids): # type: (typing.List[str]) -> dict
1289 """ Delete multiple models.
1290
1291 Args:
1292 model_ids: a list of unique IDs of the models to delete
1293
1294 Returns:
1295 the raw JSON response from the server
1296
1297 Examples:
1298 >>> app.models.delete_models(['model_id1', 'model_id2'])
1299 """
1300
1301 res = self.api.delete_models(model_ids)
1302 return res
1303
1304 def delete_all(self): # type: () -> dict
1305 """ Delete all models and the versions associated with each one
1306
1307 After this operation, you will have no models in the
1308 application
1309
1310 Returns:
1311 the raw JSON response from the server
1312
1313 Examples:
1314 >>> app.models.delete_all()
1315 """
1316
1317 res = self.api.delete_all_models()
1318 return res
1319
1320 def get(
1321 self, # type: Models
1322 model_name=None, # type: typing.Optional[str]
1323 model_id=None, # type: typing.Optional[str]
1324 model_type=None # type:typing.Optional[str]
1325 ):
1326 # type: (...) -> Model
1327 """ Get a model, by ID or name
1328
1329 Args:
1330 model_name: name of the model
1331 model_id: unique identifier of the model
1332 model_type: type of the model
1333
1334 Returns:
1335 the Model object
1336
1337 Examples:
1338 >>> # get general-v1.3 model
1339 >>> app.models.get('general-v1.3')
1340 """
1341
1342 # if the model ID is specified, just make the Model
1343 if model_id:
1344 model = Model(self.api, model_id=model_id,
1345 solutions=self.solutions)
1346 return model
1347
1348 # search for the model_name together with the model_type
1349 if self.model_id_cache.get((model_name, model_type)):
1350 model_id = self.model_id_cache[(model_name, model_type)]
1351 model = Model(self.api, model_id=model_id,
1352 solutions=self.solutions)
1353 return model
1354
1355 try:
1356 res = self.api.get_model(model_name)
1357 model = self._to_obj(res['model'])
1358 except ApiError as e:
1359
1360 if e.response.status_code == 401:
1361 raise e
1362
1363 if e.response.status_code == 404:
1364 res = self.search(model_name, model_type)
1365
1366 if res is None:
1367 raise e
1368
1369 # exclude embed and cluster model when it's not explicitly searched for
1370 if not model_type:
1371 res = [
1372 found_model for found_model in res
1373 if found_model.output_info['type'] not in (u'embed', u'cluster')
1374 ]
1375 if len(res) > 1:
1376 logging.error('A model by the name of %s or a single similarly-named model could not be '
1377 'found' % model_name)
1378 return None
1379
1380 # TODO(Rok) HIGH: This sets the return value to a dict, but previous return values are
1381 # Model objects.
1382 model = res[0]
1383 self.model_id_cache.update(
1384 {(model_name, model_type): model.model_id})
1385 else:
1386 model = None
1387
1388 return model
1389
1390 def search(self, model_name, model_type=None):
1391 # type: (typing.Optional[str], typing.Optional[str]) -> typing.List[Model]
1392 """
1393 Search the model by name and optionally type. Default is to search concept models
1394 only. All the custom model trained are concept models.
1395
1396 Args:
1397 model_name: name of the model. name is not unique.
1398 model_type: default to None, equivalent to wildcards search
1399
1400 Returns:
1401 a list of Model objects or None
1402
1403 Examples:
1404 >>> # search for general-v1.3 models
1405 >>> app.models.search('general-v1.3')
1406 >>>
1407 >>> # search for color model
1408 >>> app.models.search('color', model_type='color')
1409 >>>
1410 >>> # search for face model
1411 >>> app.models.search('face', model_type='facedetect')
1412 """
1413
1414 res = self.api.search_models(model_name, model_type)
1415 if res.get('models'):
1416 results = [self._to_obj(one) for one in res['models']]
1417 else:
1418 results = None
1419
1420 return results
1421
1422 def _to_obj(self, item): # type: (dict) -> Model
1423 """ convert a model json object to Model object """
1424 return Model(self.api, item, solutions=self.solutions)
1425
1426
1427def _escape(param): # type: (str) -> str
1428 return param.replace('/', '%2F')
1429
1430
1431class Inputs(object):
1432
1433 def __init__(self, api): # type: (ApiClient) -> None
1434 self.api = api # type: ApiClient
1435
1436 def create_image(self, image): # type: (Image) -> Image
1437 """ create an image from Image object
1438
1439 Args:
1440 image: a Clarifai Image object
1441
1442 Returns:
1443 the Image object that just got created and uploaded
1444
1445 Examples:
1446 >>> app.inputs.create_image(Image(url='https://samples.clarifai.com/metro-north.jpg'))
1447 """
1448
1449 ret = self.api.add_inputs([image])
1450
1451 img = self._to_obj(ret['inputs'][0])
1452 return img
1453
1454 def create_image_from_url(
1455 self, # type: Inputs
1456 url, # type: str
1457 image_id=None, # type: typing.Optional[str]
1458 concepts=None, # type: typing.Optional[typing.List[str]]
1459 not_concepts=None, # type: typing.Optional[typing.List[str]]
1460 crop=None, # type: typing.Optional[BoundingBox]
1461 metadata=None, # type: typing.Optional[dict]
1462 geo=None, # type: typing.Optional[Geo]
1463 allow_duplicate_url=False # type: bool
1464 ):
1465 # type: (...) -> Image
1466 """ create an image from Image url
1467
1468 Args:
1469 url: image url
1470 image_id: ID of the image
1471 concepts: a list of concept names this image is associated with
1472 not_concepts: a list of concept names this image is not associated with
1473 crop: crop information, with four corner coordinates
1474 metadata: meta data with a dictionary
1475 geo: geo info with a dictionary
1476 allow_duplicate_url: True or False, the flag to allow a duplicate url to be imported
1477
1478 Returns:
1479 the Image object that just got created and uploaded
1480
1481 Examples:
1482 >>> app.inputs.create_image_from_url(url='https://samples.clarifai.com/metro-north.jpg')
1483 >>>
1484 >>> # create image with geo point
1485 >>> app.inputs.create_image_from_url(url='https://samples.clarifai.com/metro-north.jpg',
1486 >>> geo=Geo(geo_point=GeoPoint(22.22, 44.44))
1487 """
1488
1489 url = url.strip() if url else url
1490
1491 image = Image(
1492 url=url,
1493 image_id=image_id,
1494 concepts=concepts,
1495 not_concepts=not_concepts,
1496 crop=crop,
1497 metadata=metadata,
1498 geo=geo,
1499 allow_dup_url=allow_duplicate_url)
1500
1501 return self.create_image(image)
1502
1503 def create_image_from_filename(
1504 self, # type: Inputs
1505 filename, # type: str
1506 image_id=None, # type: typing.Optional[str]
1507 concepts=None, # type: typing.Optional[typing.List[str]]
1508 not_concepts=None, # type: typing.Optional[typing.List[str]]
1509 crop=None, # type: typing.Optional[BoundingBox]
1510 metadata=None, # type: typing.Optional[dict]
1511 geo=None, # type: typing.Optional[Geo]
1512 allow_duplicate_url=False # type: bool
1513 ):
1514 # type: (...) -> Image
1515 """ create an image by local filename
1516
1517 Args:
1518 filename: local filename
1519 image_id: ID of the image
1520 concepts: a list of concept names this image is associated with
1521 not_concepts: a list of concept names this image is not associated with
1522 crop: crop information, with four corner coordinates
1523 metadata: meta data with a dictionary
1524 geo: geo info with a dictionary
1525 allow_duplicate_url: True or False, the flag to allow a duplicate url to be imported
1526
1527 Returns:
1528 the Image object that just got created and uploaded
1529
1530 Examples:
1531 >>> app.inputs.create_image_filename(filename="a.jpeg")
1532 """
1533
1534 with open(filename, 'rb') as fileio:
1535 image = Image(
1536 file_obj=fileio,
1537 image_id=image_id,
1538 concepts=concepts,
1539 not_concepts=not_concepts,
1540 crop=crop,
1541 metadata=metadata,
1542 geo=geo,
1543 allow_dup_url=allow_duplicate_url)
1544 return self.create_image(image)
1545
1546 def create_image_from_bytes(
1547 self, # type: Inputs
1548 img_bytes, # type: bytes
1549 image_id=None, # type: typing.Optional[str]
1550 concepts=None, # type: typing.Optional[typing.List[str]]
1551 not_concepts=None, # type: typing.Optional[typing.List[str]]
1552 crop=None, # type: typing.Optional[BoundingBox]
1553 metadata=None, # type: typing.Optional[str]
1554 geo=None, # type: typing.Optional[Geo]
1555 allow_duplicate_url=False # type: bool
1556 ):
1557 # type: (...) -> Image
1558 """ create an image by image bytes
1559
1560 Args:
1561 img_bytes: raw bytes of an image
1562 image_id: ID of the image
1563 concepts: a list of concept names this image is associated with
1564 not_concepts: a list of concept names this image is not associated with
1565 crop: crop information, with four corner coordinates
1566 metadata: meta data with a dictionary
1567 geo: geo info with a dictionary
1568 allow_duplicate_url: True or False, the flag to allow a duplicate url to be imported
1569
1570 Returns:
1571 the Image object that just got created and uploaded
1572
1573 Examples:
1574 >>> app.inputs.create_image_bytes(img_bytes="raw image bytes...")
1575 """
1576
1577 fileio = BytesIO(img_bytes)
1578 image = Image(
1579 file_obj=fileio,
1580 image_id=image_id,
1581 concepts=concepts,
1582 not_concepts=not_concepts,
1583 crop=crop,
1584 metadata=metadata,
1585 geo=geo,
1586 allow_dup_url=allow_duplicate_url)
1587 return self.create_image(image)
1588
1589 def create_image_from_base64(
1590 self, # type: Inputs
1591 base64_bytes, # type: str
1592 image_id=None, # type: typing.Optional[str]
1593 concepts=None, # type: typing.Optional[typing.List[str]]
1594 not_concepts=None, # type: typing.Optional[typing.List[str]]
1595 crop=None, # type: typing.Optional[BoundingBox]
1596 metadata=None, # type: typing.Optional[dict]
1597 geo=None, # type: typing.Optional[Geo]
1598 allow_duplicate_url=False # type: bool
1599 ):
1600 # type: (...) -> Image
1601 """ create an image by base64 bytes
1602
1603 Args:
1604 base64_bytes: base64 encoded image bytes
1605 image_id: ID of the image
1606 concepts: a list of concept names this image is associated with
1607 not_concepts: a list of concept names this image is not associated with
1608 crop: crop information, with four corner coordinates
1609 metadata: meta data with a dictionary
1610 geo: geo info with a dictionary
1611 allow_duplicate_url: True or False, the flag to allow a duplicate url to be imported
1612
1613 Returns:
1614 the Image object that just got created and uploaded
1615
1616 Examples:
1617 >>> app.inputs.create_image_bytes(base64_bytes="base64 encoded image bytes...")
1618 """
1619
1620 image = Image(
1621 base64=base64_bytes,
1622 image_id=image_id,
1623 concepts=concepts,
1624 not_concepts=not_concepts,
1625 crop=crop,
1626 metadata=metadata,
1627 geo=geo,
1628 allow_dup_url=allow_duplicate_url)
1629 return self.create_image(image)
1630
1631 # type: (typing.List[Image]) -> typing.List[Image]
1632 def bulk_create_images(self, images):
1633 """ Create images in bulk
1634
1635 Args:
1636 images: a list of Image objects
1637
1638 Returns:
1639 a list of the Image objects that were just created
1640
1641 Examples:
1642 >>> img1 = Image(url="", concepts=['cat', 'kitty'])
1643 >>> img2 = Image(url="", concepts=['dog'], not_concepts=['cat'])
1644 >>> app.inputs.bulk_create_images([img1, img2])
1645 """
1646
1647 lens = len(images)
1648 if lens > 128:
1649 raise UserError('the maximum number of inputs in a batch is 128')
1650
1651 res = self.api.add_inputs(images)
1652 images = [self._to_obj(one) for one in res['inputs']]
1653 return images
1654
1655 def check_status(self): # type: () -> InputCounts
1656 """ check the input upload status
1657
1658 Returns:
1659 InputCounts object
1660
1661 Examples:
1662 >>> status = app.inputs.check_status()
1663 >>> print(status.code)
1664 >>> print(status.description)
1665 """
1666
1667 ret = self.api.get_inputs_status()
1668 counts = InputCounts(ret)
1669 return counts
1670
1671 # type: (bool) -> typing.Generator[Input, None, None]
1672 def get_all(self, ignore_error=False):
1673 """ Get all inputs
1674
1675
1676 Args:
1677 ignore_error: ignore errored inputs. For example some images may fail to be imported
1678 due to bad url
1679
1680 Returns:
1681 a generator function that yields Input objects
1682
1683 Examples:
1684 >>> for image in app.inputs.get_all():
1685 >>> print(image.input_id)
1686 """
1687
1688 page = 1
1689 per_page = 20
1690
1691 while True:
1692 try:
1693 res = self.api.get_inputs(page, per_page)
1694 except ApiError as e:
1695 if e.response.status_code == 207 and e.error_code == 10010:
1696 res = e.response.json()
1697 else:
1698 raise e
1699
1700 if not res['inputs']:
1701 break
1702
1703 for one in res['inputs']:
1704 input_ = self._to_obj(one)
1705
1706 if ignore_error is True and input_.status.code != 30000:
1707 continue
1708
1709 yield input_
1710
1711 page += 1
1712
1713 def get_by_page(self, page=1, per_page=20, ignore_error=False):
1714 # type: (int, int, bool) -> typing.List[Input]
1715 """ Get inputs with pagination
1716
1717 Args:
1718 page: page number
1719 per_page: number of inputs to retrieve per page
1720 ignore_error: ignore errored inputs. For example some images may fail to be imported
1721 due to bad url
1722
1723 Returns:
1724 a list of Input objects
1725
1726 Examples:
1727 >>> for image in app.inputs.get_by_page(2, 10):
1728 >>> print(image.input_id)
1729 """
1730
1731 try:
1732 res = self.api.get_inputs(page, per_page)
1733 except ApiError as e:
1734 if e.response.status_code == 207 and e.error_code == 10010:
1735 res = e.response.json()
1736 else:
1737 raise e
1738
1739 results = []
1740 for one in res['inputs']:
1741 input_ = self._to_obj(one)
1742
1743 if ignore_error is True and input_.status.code != 30000:
1744 continue
1745
1746 results.append(input_)
1747
1748 return results
1749
1750 def delete(self, input_id): # type: (str) -> ApiStatus
1751 """ delete an input with input ID
1752
1753 Args:
1754 input_id: the unique input ID
1755
1756 Returns:
1757 ApiStatus object
1758
1759 Examples:
1760 >>> ret = app.inputs.delete('id1')
1761 >>> print(ret.code)
1762 """
1763
1764 if isinstance(input_id, list):
1765 res = self.api.delete_inputs(input_id)
1766 else:
1767 res = self.api.delete_input(input_id)
1768
1769 return ApiStatus(res['status'])
1770
1771 def delete_all(self): # type: () -> ApiStatus
1772 """ delete all inputs from the application
1773 """
1774 res = self.api.delete_all_inputs()
1775 return ApiStatus(res['status'])
1776
1777 def get(self, input_id): # type: (str) -> Image
1778 """ get an Input object by input ID
1779
1780 Args:
1781 input_id: the unique identifier of the input
1782
1783 Returns:
1784 an Image/Input object
1785
1786 Examples:
1787 >>> image = app.inputs.get('id1')
1788 >>> print(image.input_id)
1789
1790 """
1791
1792 res = self.api.get_input(input_id)
1793 one = res['input']
1794 return self._to_obj(one)
1795
1796 def search(self, qb, page=1, per_page=20, raw=False):
1797 # type: (SearchQueryBuilder, int, int, bool) -> typing.List[Image]
1798 """ search with a clarifai image query builder
1799
1800 WARNING: this is the advanced search function. You will need to build a query builder
1801 in order to use this.
1802
1803 There are a few simple search functions:
1804 search_by_annotated_concepts()
1805 search_by_predicted_concepts()
1806 search_by_image()
1807 search_by_metadata()
1808
1809 Args:
1810 qb: clarifai query builder
1811 page: the results page
1812 per_page: results per page
1813 raw: whether to return the original JSON object instead of a list of Image objects
1814
1815 Returns:
1816 a list of Input/Image object
1817 """
1818
1819 res = self.api.search_inputs(qb.dict(), page, per_page)
1820
1821 # output raw result when the flag is set
1822 if raw:
1823 return res
1824
1825 hits = [self._to_search_obj(one) for one in res['hits']]
1826 return hits
1827
1828 def search_by_image(
1829 self, # type: Inputs
1830 image_id=None, # type: typing.Optional[str]
1831 image=None, # type: typing.Optional[Image]
1832 url=None, # type: typing.Optional[str]
1833 imgbytes=None, # type: typing.Optional[bytes]
1834 base64bytes=None, # type: typing.Optional[str]
1835 fileobj=None, # type: typing.Optional[typing.Any]
1836 filename=None, # type: typing.Optional[str]
1837 crop=None, # type: typing.Optional[BoundingBox]
1838 page=1, # type: int
1839 per_page=20, # type: int
1840 raw=False # type: bool
1841 ):
1842 # type: (...) -> typing.List[Image]
1843 """ Search for visually similar images
1844
1845 By passing image_id, raw image bytes, base64 encoded bytes, image file io stream,
1846 image filename, or Clarifai Image object, you can use the visual search power of
1847 the Clarifai API.
1848
1849 You can specify a crop of the image to search over
1850
1851 Args:
1852 image_id: unique ID of the image for search
1853 image: Image object for search
1854 imgbytes: raw image bytes for search
1855 base64bytes: base63 encoded image bytes
1856 fileobj: file io stream, like open(file)
1857 filename: filename on local filesystem
1858 crop: crop of the image as a list of four floats representing the corner coordinates
1859 page: page number
1860 per_page: number of images returned per page
1861 raw: raw result indicator
1862
1863 Returns:
1864 a list of Image object
1865
1866 Examples:
1867 >>> # search by image url
1868 >>> app.inputs.search_by_image(url='http://blabla')
1869 >>> # search by local filename
1870 >>> app.inputs.search_by_image(filename='bla')
1871 >>> # search by raw image bytes
1872 >>> app.inputs.search_by_image(imgbytes='data')
1873 >>> # search by base64 encoded image bytes
1874 >>> app.inputs.search_by_image(base64bytes='data')
1875 >>> # search by file stream io
1876 >>> app.inputs.search_by_image(fileobj=open('file'))
1877 """
1878
1879 not_nones = [
1880 x for x in [image_id, image, url, imgbytes, base64bytes, fileobj, filename]
1881 if x is not None
1882 ]
1883 if len(not_nones) != 1:
1884 raise UserError('Unable to construct an image')
1885
1886 if image_id:
1887 qb = SearchQueryBuilder()
1888 term = OutputSearchTerm(input_id=image_id, crop=crop)
1889 qb.add_term(term)
1890
1891 return self.search(qb, page, per_page, raw)
1892 elif image:
1893 qb = SearchQueryBuilder()
1894
1895 if image.url:
1896 term = OutputSearchTerm(url=image.url, crop=crop)
1897 elif image.base64:
1898 term = OutputSearchTerm(
1899 base64=image.base64.decode('UTF-8'), crop=crop)
1900 elif image.file_obj:
1901 if hasattr(image.file_obj, 'getvalue'):
1902 base64_bytes = base64_lib.b64encode(
1903 image.file_obj.getvalue()).decode('UTF-8')
1904 elif hasattr(image.file_obj, 'read'):
1905 base64_bytes = base64_lib.b64encode(
1906 image.file_obj.read()).decode('UTF-8')
1907 else:
1908 raise UserError("Not sure how to read your file_obj")
1909
1910 term = OutputSearchTerm(base64=base64_bytes, crop=crop)
1911 else:
1912 raise UserError('Unrecognized image object')
1913
1914 qb.add_term(term)
1915
1916 return self.search(qb, page, per_page, raw)
1917
1918 if url:
1919 img = Image(url=url)
1920 elif fileobj:
1921 img = Image(file_obj=fileobj)
1922 elif imgbytes:
1923 fileio = BytesIO(imgbytes)
1924 img = Image(file_obj=fileio)
1925 elif filename:
1926 fileio = open(filename, 'rb')
1927 img = Image(file_obj=fileio)
1928 elif base64bytes:
1929 img = Image(base64=base64bytes)
1930 else:
1931 raise UserError('None of the arguments was passed in')
1932
1933 return self.search_by_image(image=img, page=page, per_page=per_page, raw=raw, crop=crop)
1934
1935 def search_by_original_url(self, url, page=1, per_page=20, raw=False):
1936 # type: (str, int, int, bool) -> typing.List[Image]
1937 """ search by the original url of the uploaded images
1938
1939 Args:
1940 url: url of the image
1941 page: page number
1942 per_page: the number of images to return per page
1943 raw: raw result indicator
1944
1945 Returns:
1946 a list of Image objects
1947
1948 Examples:
1949 >>> app.inputs.search_by_original_url(url='http://bla')
1950 """
1951
1952 qb = SearchQueryBuilder()
1953
1954 term = InputSearchTerm(url=url)
1955 qb.add_term(term)
1956 res = self.search(qb, page, per_page, raw)
1957
1958 return res
1959
1960 def search_by_metadata(self, metadata, page=1, per_page=20, raw=False):
1961 # type: (dict, int, int, bool) -> typing.List[Image]
1962 """ search by meta data of the image rather than concept
1963
1964 Args:
1965 metadata: a dictionary for meta data search.
1966 The dictionary could be a simple one with only one key and value,
1967 Or a nested dictionary with multi levels.
1968 page: page number
1969 per_page: the number of images to return per page
1970 raw: raw result indicator
1971
1972 Returns:
1973 a list of Image objects
1974
1975 Examples:
1976 >>> app.inputs.search_by_metadata(metadata={'name':'bla'})
1977 >>> app.inputs.search_by_metadata(metadata={'my_class1': { 'name' : 'bla' }})
1978 """
1979
1980 if isinstance(metadata, dict):
1981 qb = SearchQueryBuilder()
1982
1983 term = InputSearchTerm(metadata=metadata)
1984 qb.add_term(term)
1985 res = self.search(qb, page, per_page, raw)
1986 else:
1987 raise UserError(
1988 'Metadata must be a valid dictionary. Please double check.')
1989
1990 return res
1991
1992 def search_by_annotated_concepts(
1993 self, # type: Inputs
1994 concept=None, # type: typing.Optional[str]
1995 concepts=None, # type: typing.Optional[typing.List[str]]
1996 value=True, # type: bool
1997 values=None, # type: typing.Optional[typing.List[bool]]
1998 concept_id=None, # type: typing.Optional[str]
1999 concept_ids=None, # type: typing.Optional[typing.List[str]]
2000 page=1, # type: int
2001 per_page=20, # type: int
2002 raw=False # type: bool
2003 ):
2004 # type: (...) -> typing.List[Image]
2005 """ search using the concepts the user has manually specified
2006
2007 Args:
2008 concept: concept name to search
2009 concepts: a list of concept name to search
2010 concept_id: concept IDs to search
2011 concept_ids: a list of concept IDs to search
2012 value: whether the concept should be a positive tag or negative
2013 values: the list of values corresponding to the concepts
2014 page: page number
2015 per_page: number of images to return per page
2016 raw: raw result indicator
2017
2018 Returns:
2019 a list of Image objects
2020
2021 Examples:
2022 >>> app.inputs.search_by_annotated_concepts(concept='cat')
2023 """
2024
2025 if not concept and not concepts and concept_id and concept_ids:
2026 raise UserError('concept could not be null.')
2027
2028 if concept or concepts:
2029
2030 if concept and not isinstance(concept, basestring):
2031 raise UserError('concept should be a string')
2032 elif concepts and not isinstance(concepts, list):
2033 raise UserError('concepts must be a list')
2034 elif concepts and not all([isinstance(one, basestring) for one in concepts]):
2035 raise UserError('concepts must be a list of all string')
2036
2037 if concept and concepts:
2038 raise UserError(
2039 'you can either search by concept or concepts but not both')
2040
2041 if concept:
2042 concepts = [concept]
2043
2044 if not values:
2045 values = [value]
2046
2047 qb = SearchQueryBuilder()
2048
2049 for concept, value in zip(concepts, values):
2050 term = InputSearchTerm(concept=concept, value=value)
2051 qb.add_term(term)
2052
2053 else:
2054
2055 if concept_id and not isinstance(concept_id, basestring):
2056 raise UserError('concept should be a string')
2057 elif concept_ids and not isinstance(concept_ids, list):
2058 raise UserError('concepts must be a list')
2059 elif concept_ids and not all([isinstance(one, basestring) for one in concept_ids]):
2060 raise UserError('concepts must be a list of all string')
2061
2062 if concept_id and concept_ids:
2063 raise UserError(
2064 'you can either search by concept_id or concept_ids but not both')
2065
2066 if concept_id:
2067 concept_ids = [concept_id]
2068
2069 if not values:
2070 values = [value]
2071
2072 qb = SearchQueryBuilder()
2073
2074 for concept_id, value in zip(concept_ids, values):
2075 term = InputSearchTerm(concept_id=concept_id, value=value)
2076 qb.add_term(term)
2077
2078 return self.search(qb, page, per_page, raw)
2079
2080 def search_by_geo(
2081 self, # type: Inputs
2082 geo_point=None, # type: typing.Optional[GeoPoint]
2083 geo_limit=None, # type: typing.Optional[GeoLimit]
2084 geo_box=None, # type: typing.Optional[GeoBox]
2085 page=1, # type: int
2086 per_page=20, # type: int
2087 raw=False # type: bool
2088 ):
2089 # type: (...) -> typing.List[Image]
2090 """ search by geo point and geo limit
2091
2092 Args:
2093 geo_point: A GeoPoint object, which represents the (longitude, latitude) of a location
2094 geo_limit: A GeoLimit object, which represents a range to a GeoPoint
2095 geo_box: A GeoBox object, which represents a box area
2096 page: page number
2097 per_page: number of images to return per page
2098 raw: raw result indicator
2099
2100 Returns:
2101 a list of Image objects
2102
2103 Examples:
2104 >>> app.inputs.search_by_geo(GeoPoint(30, 40), GeoLimit("mile", 10))
2105 """
2106 if geo_limit is None:
2107 geo_limit = GeoLimit("mile", 10)
2108
2109 if geo_point and not isinstance(geo_point, GeoPoint):
2110 raise UserError(
2111 'geo_point type not match GeoPoint. Please check data type.')
2112
2113 if not isinstance(geo_limit, GeoLimit):
2114 raise UserError(
2115 'geo_limit type not match GeoLimit. Please check data type.')
2116
2117 if geo_box and not isinstance(geo_box, GeoBox):
2118 raise UserError(
2119 'geo_box type not match GeoBox. Please check data type.')
2120
2121 if geo_point is None and geo_box is None:
2122 raise UserError(
2123 'at least geo_point or geo_box needs to be specified for the geo search.')
2124
2125 if geo_point and geo_box:
2126 raise UserError(
2127 'confusing. you cannot search by geo_point and geo_box together.')
2128
2129 qb = SearchQueryBuilder()
2130
2131 if geo_point is not None:
2132 term = InputSearchTerm(
2133 geo=Geo(geo_point=geo_point, geo_limit=geo_limit))
2134 elif geo_box is not None:
2135 term = InputSearchTerm(geo=Geo(geo_box=geo_box))
2136
2137 qb.add_term(term)
2138
2139 return self.search(qb, page, per_page, raw)
2140
2141 def search_by_predicted_concepts(
2142 self, # type: Inputs
2143 concept=None, # type: typing.Optional[str]
2144 concepts=None, # type: typing.Optional[typing.Optional[str]]
2145 value=True, # type: bool
2146 values=None, # type: typing.Optional[typing.List[bool]]
2147 concept_id=None, # type: typing.Optional[str]
2148 concept_ids=None, # type: typing.Optional[typing.List[str]]
2149 page=1, # type: int
2150 per_page=20, # type: int
2151 lang=None, # type: typing.Optional[str]
2152 raw=False # type: bool
2153 ):
2154 # type: (...) -> typing.List[Image]
2155 """ search over the predicted concepts
2156
2157 Args:
2158 concept: concept name to search
2159 concepts: a list of concept names to search
2160 concept_id: concept id to search
2161 concept_ids: a list of concept ids to search
2162 value: whether the concept should be a positive tag or negative
2163 values: the list of values corresponding to the concepts
2164 page: page number
2165 per_page: number of images to return per page
2166 lang: language to search over for translated concepts
2167 raw: raw result indicator
2168
2169 Returns:
2170 a list of Image objects
2171
2172 Examples:
2173 >>> app.inputs.search_by_predicted_concepts(concept='cat')
2174 >>> # search over simplified Chinese label
2175 >>> app.inputs.search_by_predicted_concepts(concept=u'狗', lang='zh')
2176 """
2177 if not concept and not concepts and concept_id and concept_ids:
2178 raise UserError('concept could not be null.')
2179
2180 if concept and not isinstance(concept, basestring):
2181 raise UserError('concept should be a string')
2182 elif concepts and not isinstance(concepts, list):
2183 raise UserError('concepts must be a list')
2184 elif concepts and not all([isinstance(one, basestring) for one in concepts]):
2185 raise UserError('concepts must be a list of all string')
2186
2187 if concept or concepts:
2188 if concept and concepts:
2189 raise UserError(
2190 'you can either search by concept or concepts but not both')
2191
2192 if concept:
2193 concepts = [concept]
2194
2195 if not values:
2196 values = [value]
2197
2198 qb = SearchQueryBuilder(language=lang)
2199
2200 for concept, value in zip(concepts, values):
2201 term = OutputSearchTerm(concept=concept, value=value)
2202 qb.add_term(term)
2203
2204 else:
2205
2206 if concept_id and concept_ids:
2207 raise UserError(
2208 'you can either search by concept_id or concept_ids but not both')
2209
2210 if concept_id:
2211 concept_ids = [concept_id]
2212
2213 if not values:
2214 values = [value]
2215
2216 qb = SearchQueryBuilder()
2217
2218 for concept_id, value in zip(concept_ids, values):
2219 term = OutputSearchTerm(concept_id=concept_id, value=value)
2220 qb.add_term(term)
2221
2222 return self.search(qb, page, per_page, raw)
2223
2224 def send_search_feedback(self, input_id, feedback_info=None):
2225 # type: (str, typing.Optional[FeedbackInfo]) -> dict
2226 """
2227 Send feedback for search
2228
2229 Args:
2230 input_id: unique identifier for the input
2231 feedback_info: the feedback information
2232
2233 Returns:
2234 None
2235 """
2236
2237 feedback_input = Image(image_id=input_id, feedback_info=feedback_info)
2238 res = self.api.send_search_feedback(feedback_input)
2239
2240 return res
2241
2242 def update(self, image, action='merge'): # type: (Image, str) -> Image
2243 """
2244 Update the information of an input/image
2245
2246 Args:
2247 image: an Image object that has concepts, metadata, etc.
2248 action: one of ['merge', 'overwrite']
2249
2250 'merge' is to append the info onto the existing info, for either concept or
2251 metadata
2252
2253 'overwrite' is to overwrite the existing metadata and concepts with the
2254 existing ones
2255
2256 Returns:
2257 an Image object
2258
2259 Examples:
2260 >>> new_img = Image(image_id="abc", concepts=['c1', 'c2'], not_concepts=['c3'],
2261 >>> metadata={'key':'val'})
2262 >>> app.inputs.update(new_img, action='overwrite')
2263 """
2264 res = self.api.patch_inputs(action=action, inputs=[image])
2265
2266 one = res['inputs'][0]
2267 return self._to_obj(one)
2268
2269 # TODO(Rok) MEDIUM: Unconsistent name. Should be bulk_update_image. Deprecate this method
2270 # and create a new one.
2271 def bulk_update(self, images, action='merge'):
2272 # type: (typing.List[typing.Union[Input]], str) -> typing.List[Image]
2273 """ Update the input
2274 update the information of an input/image
2275
2276 Args:
2277 images: a list of Image objects that have concepts, metadata, etc.
2278 action: one of ['merge', 'overwrite']
2279
2280 'merge' is to append the info onto the existing info, for either concept or
2281 metadata
2282
2283 'overwrite' is to overwrite the existing metadata and concepts with the
2284 existing ones
2285
2286 Returns:
2287 an Image object
2288
2289 Examples:
2290 >>> new_img1 = Image(image_id="abc1", concepts=['c1', 'c2'], not_concepts=['c3'],
2291 >>> metadata={'key':'val'})
2292 >>> new_img2 = Image(image_id="abc2", concepts=['c1', 'c2'], not_concepts=['c3'],
2293 >>> metadata={'key':'val'})
2294 >>> app.inputs.update([new_img1, new_img2], action='overwrite')
2295 """
2296 ret = self.api.patch_inputs(action=action, inputs=images)
2297 objs = [self._to_obj(item) for item in ret['inputs']]
2298 return objs
2299
2300 # type: (str, typing.List[str]) -> Image
2301 def delete_concepts(self, input_id, concepts):
2302 """ delete concepts from an input/image
2303
2304 Args:
2305 input_id: unique ID of the input
2306 concepts: a list of concept names
2307
2308 Returns:
2309 an Image object
2310 """
2311
2312 res = self.update(
2313 Image(image_id=input_id, concepts=concepts), action='remove')
2314 return res
2315
2316 def bulk_merge_concepts(self, input_ids, concept_lists):
2317 # type: (typing.List[str], typing.List[typing.List[str]]) -> typing.List[Image]
2318 """ bulk merge concepts from a list of input ids
2319
2320 Args:
2321 input_ids: a list of input IDs
2322 concept_lists: a list of concept lists, each one corresponding to a listed input ID and
2323 filled with concepts to be added to that input
2324
2325 Returns:
2326 an Input object
2327
2328 Examples:
2329 >>> app.inputs.bulk_merge_concepts('id', [[('cat',True), ('dog',False)]])
2330 """
2331
2332 if len(input_ids) != len(concept_lists):
2333 raise UserError('Argument error. please check')
2334
2335 inputs = []
2336 for input_id, concept_list in zip(input_ids, concept_lists):
2337 concepts = []
2338 not_concepts = []
2339 for concept_id, value in concept_list:
2340 if value is True:
2341 concepts.append(concept_id)
2342 else:
2343 not_concepts.append(concept_id)
2344
2345 image = Image(image_id=input_id, concepts=concepts,
2346 not_concepts=not_concepts)
2347 inputs.append(image)
2348
2349 res = self.bulk_update(inputs, action='merge')
2350 return res
2351
2352 def bulk_delete_concepts(self, input_ids, concept_lists):
2353 # type: (typing.List[str], typing.List[typing.List[str]]) -> typing.List[Image]
2354 """ bulk delete concepts from a list of input ids
2355
2356 Args:
2357 input_ids: a list of input IDs
2358 concept_lists: a list of concept lists, each one corresponding to a listed input ID and
2359 filled with concepts to be deleted from that input
2360
2361 Returns:
2362 an Input object
2363
2364 Examples:
2365 >>> app.inputs.bulk_delete_concepts(['id'], [['cat', 'dog']])
2366 """
2367
2368 # the reason list comprehension is not used is it breaks the 100 chars width
2369 inputs = []
2370 for input_id, concepts in zip(input_ids, concept_lists):
2371 one_input = Image(image_id=input_id, concepts=concepts)
2372 inputs.append(one_input)
2373
2374 res = self.bulk_update(inputs, action='remove')
2375 return res
2376
2377 def merge_concepts(
2378 self, # type: Inputs
2379 input_id, # type: str
2380 concepts=None, # type: typing.Optional[typing.List[str]]
2381 not_concepts=None, # type: typing.Optional[typing.List[str]]
2382 overwrite=False # type: bool
2383 ):
2384 # type: (...) -> Image
2385 """ Merge concepts for one input
2386
2387 Args:
2388 input_id: the unique ID of the input
2389 concepts: the list of concepts
2390 not_concepts: the list of negative concepts
2391 overwrite: if True, this operation will replace the previous concepts. If False,
2392 it will append them.
2393
2394
2395 Returns:
2396 an Input object
2397
2398 Examples:
2399 >>> app.inputs.merge_concepts('id', ['cat', 'kitty'], ['dog'])
2400 """
2401
2402 image = Image(image_id=input_id, concepts=concepts,
2403 not_concepts=not_concepts)
2404
2405 if overwrite is True:
2406 action = 'overwrite'
2407 else:
2408 action = 'merge'
2409
2410 res = self.update(image, action=action)
2411 return res
2412
2413 def add_concepts(self, input_id, concepts=None, not_concepts=None):
2414 # type: (str, typing.Optional[typing.List[str]], typing.Optional[typing.List[str]]) -> Image
2415 """ Add concepts for one input
2416
2417 This is just an alias of `merge_concepts` for easier understanding
2418 when you try to add some new concepts to an image
2419
2420 Args:
2421 input_id: the unique ID of the input
2422 concepts: the list of concepts
2423 not_concepts: the list of negative concepts
2424
2425 Returns:
2426 an Input object
2427
2428 Examples:
2429 >>> app.inputs.add_concepts('id', ['cat', 'kitty'], ['dog'])
2430 """
2431 return self.merge_concepts(input_id, concepts, not_concepts)
2432
2433 def merge_metadata(self, input_id, metadata): # type: (str, dict) -> Image
2434 """ merge metadata for the image
2435
2436 This is to merge/update the metadata of the given image
2437
2438 Args:
2439 input_id: the unique ID of the input
2440 metadata: the metadata dictionary
2441
2442 Examples:
2443 >>> # merge the metadata
2444 >>> # metadata will be appended to the existing key/value pairs
2445 >>> app.inputs.merge_metadata('id', {'key1':'value1', 'key2':'value2'})
2446 """
2447 image = Image(image_id=input_id, metadata=metadata)
2448
2449 action = 'merge'
2450 res = self.update(image, action=action)
2451 return res
2452
2453 def _to_search_obj(self, one): # type: (dict) -> Image
2454 """ convert the search candidate to input object """
2455 score = one['score']
2456 one_input = self._to_obj(one['input'])
2457 one_input.score = score
2458 return one_input
2459
2460 def _to_obj(self, one): # type: (dict) -> Image
2461
2462 # get concepts
2463 concepts = []
2464 not_concepts = []
2465 for concept in one['data'].get('concepts', []):
2466 if concept.get('value', 1) == 1:
2467 concepts.append(concept.get('name') or concept['id'])
2468 else:
2469 not_concepts.append(concept.get('name') or concept['id'])
2470
2471 if not concepts:
2472 concepts = None
2473
2474 if not not_concepts:
2475 not_concepts = None
2476
2477 # get metadata
2478 metadata = one['data'].get('metadata')
2479
2480 # get geo
2481 geo = geo_json = one['data'].get('geo')
2482
2483 if geo_json:
2484 geo_schema = {
2485 'additionalProperties': False,
2486 'type': 'object',
2487 'properties': {
2488 'geo_point': {
2489 'type': 'object',
2490 'properties': {
2491 'longitude': {
2492 'type': 'number'
2493 },
2494 'latitude': {
2495 'type': 'number'
2496 }
2497 }
2498 }
2499 }
2500 }
2501
2502 validate(geo_json, geo_schema)
2503 geo = Geo(GeoPoint(
2504 geo_json['geo_point']['longitude'], geo_json['geo_point']['latitude']))
2505
2506 # get regions
2507 regions = None
2508 regions_json = one['data'].get('regions')
2509 if regions_json:
2510 regions = [
2511 Region(
2512 region_id=r['id'],
2513 region_info=RegionInfo(
2514 bbox=BoundingBox(
2515 top_row=r['region_info']['bounding_box']['top_row'],
2516 left_col=r['region_info']['bounding_box']['left_col'],
2517 bottom_row=r['region_info']['bounding_box']['bottom_row'],
2518 right_col=r['region_info']['bounding_box']['right_col'])),
2519 face=Face(FaceIdentity(
2520 [c for c in r['data']['face']['identity']['concepts']]))
2521 if r.get('data', {}).get('face') else None) for r in regions_json
2522 ]
2523
2524 input_id = one['id']
2525 if one['data'].get('image'):
2526 allow_dup_url = one['data']['image'].get(
2527 'allow_duplicate_url', False)
2528
2529 if one['data']['image'].get('url'):
2530 if one['data']['image'].get('crop'):
2531 crop = one['data']['image']['crop']
2532 one_input = Image(
2533 image_id=input_id,
2534 url=one['data']['image']['url'],
2535 concepts=concepts,
2536 not_concepts=not_concepts,
2537 crop=crop,
2538 metadata=metadata,
2539 geo=geo,
2540 regions=regions,
2541 allow_dup_url=allow_dup_url)
2542 else:
2543 one_input = Image(
2544 image_id=input_id,
2545 url=one['data']['image']['url'],
2546 concepts=concepts,
2547 not_concepts=not_concepts,
2548 metadata=metadata,
2549 geo=geo,
2550 regions=regions,
2551 allow_dup_url=allow_dup_url)
2552 elif one['data']['image'].get('base64'):
2553 if one['data']['image'].get('crop'):
2554 crop = one['data']['image']['crop']
2555 one_input = Image(
2556 image_id=input_id,
2557 base64=one['data']['image']['base64'],
2558 concepts=concepts,
2559 not_concepts=not_concepts,
2560 crop=crop,
2561 metadata=metadata,
2562 geo=geo,
2563 regions=regions,
2564 allow_dup_url=allow_dup_url)
2565 else:
2566 one_input = Image(
2567 image_id=input_id,
2568 base64=one['data']['image']['base64'],
2569 concepts=concepts,
2570 not_concepts=not_concepts,
2571 metadata=metadata,
2572 geo=geo,
2573 regions=regions,
2574 allow_dup_url=allow_dup_url)
2575 else:
2576 raise UserError('Unknown input type')
2577 elif one['data'].get('video'):
2578 raise UserError('Not supported yet')
2579 else:
2580 raise UserError('Unknown input type')
2581
2582 if one.get('status'):
2583 one_input.status = ApiStatus(one['status'])
2584
2585 return one_input
2586
2587
2588class Concepts(object):
2589
2590 def __init__(self, api): # type: (ApiClient) -> None
2591 self.api = api # type: ApiClient
2592
2593 def get_all(self): # type: () -> typing.Generator[Concept, None, None]
2594 """ Get all concepts associated with the application
2595
2596 Returns:
2597 all concepts in a generator function
2598 """
2599
2600 page = 1
2601 per_page = 20
2602
2603 while True:
2604 res = self.api.get_concepts(page, per_page)
2605
2606 if not res['concepts']:
2607 break
2608
2609 for one in res['concepts']:
2610 yield self._to_obj(one)
2611
2612 page += 1
2613
2614 # type: (int, int) -> typing.List[Concept]
2615 def get_by_page(self, page=1, per_page=20):
2616 """ get concept with pagination
2617
2618 Args:
2619 page: page number
2620 per_page: number of concepts to retrieve per page
2621
2622 Returns:
2623 a list of Concept objects
2624
2625 Examples:
2626 >>> for concept in app.concepts.get_by_page(2, 10):
2627 >>> print(concept.concept_id)
2628 """
2629
2630 res = self.api.get_concepts(page, per_page)
2631 results = [self._to_obj(one) for one in res.get('concepts', [])]
2632
2633 return results
2634
2635 def get(self, concept_id): # type: (str) -> Concept
2636 """ Get a concept by id
2637
2638 Args:
2639 concept_id: concept ID, the unique identifier of the concept
2640
2641 Returns:
2642 If found, return the Concept object.
2643 Otherwise, return None
2644
2645 Examples:
2646 >>> app.concepts.get('id')
2647 """
2648
2649 res = self.api.get_concept(concept_id)
2650 if res.get('concept'):
2651 concept = self._to_obj(res['concept'])
2652 return concept
2653 else:
2654 return None
2655
2656 def search(self, term, lang=None):
2657 # type: (str, typing.Optional[str]) -> typing.Generator[Concept, None, None]
2658 """ search concepts by concept name with wildcards
2659
2660 Args:
2661 term: search term with wildcards allowed
2662 lang: language to search, if none is specified the default for the application will be
2663 used
2664
2665 Returns:
2666 a generator function with all concepts pertaining to the search term
2667
2668 Examples:
2669 >>> app.concepts.search('cat')
2670 >>> # search for Chinese label name
2671 >>> app.concepts.search(u'狗*', lang='zh')
2672 """
2673
2674 page = 1
2675 per_page = 20
2676
2677 while True:
2678 res = self.api.search_concepts(term, page, per_page, lang)
2679
2680 if not res.get('concepts'):
2681 break
2682
2683 for one in res['concepts']:
2684 yield self._to_obj(one)
2685
2686 page += 1
2687
2688 def update(self, concept_id, concept_name, action='overwrite'):
2689 # type: (str, str, str) -> Concept
2690 """ Patch concept
2691
2692 Args:
2693 concept_id: id of the concept
2694 concept_name: the new name for the concept
2695 action: the action
2696
2697 Returns:
2698 the new Concept object
2699
2700 Examples:
2701 >>> app.concepts.update(concept_id='myid1', concept_name='new_concept_name2')
2702 """
2703
2704 c = Concept(concept_name=concept_name, concept_id=concept_id)
2705 res = self.api.patch_concepts(action=action, concepts=[c])
2706
2707 return self._to_obj(res['concepts'][0])
2708
2709 def bulk_update(self, concept_ids, concept_names, action='overwrite'):
2710 # type: (typing.List[str], typing.List[str], str) -> typing.List[Concept]
2711 """ Patch multiple concepts
2712
2713 Args:
2714 concept_ids: a list of concept IDs, in sequence
2715 concept_names: a list of corresponding concept names, in the same sequence
2716 action: the type of update
2717
2718 Returns:
2719 the new Concept object
2720
2721 Examples:
2722 >>> app.concepts.bulk_update(concept_ids=['myid1', 'myid2'],
2723 >>> concept_names=['name2', 'name3'])
2724 """
2725
2726 concepts = [
2727 Concept(concept_name=concept_name, concept_id=concept_id)
2728 for concept_name, concept_id in zip(concept_names, concept_ids)
2729 ]
2730 res = self.api.patch_concepts(action=action, concepts=concepts)
2731
2732 return [self._to_obj(c) for c in res['concepts']]
2733
2734 def create(self, concept_id, concept_name=None):
2735 # type: (str, typing.Optional[str]) -> Concept
2736 """ Create a new concept
2737
2738 Args:
2739 concept_id: concept ID, the unique identifier of the concept
2740 concept_name: name of the concept.
2741 If name is not specified, it will be set to the same as the concept ID
2742
2743 Returns:
2744 the new Concept object
2745 """
2746
2747 res = self.api.add_concepts([concept_id], [concept_name])
2748 concept = self._to_obj(res['concepts'][0])
2749 return concept
2750
2751 def bulk_create(self, concept_ids, concept_names=None):
2752 # type: (typing.List[str], typing.Optional[typing.List[str]]) -> typing.List[Concept]
2753 """ Bulk create concepts
2754
2755 When the concept name is not set, it will be set as the same as concept ID.
2756
2757 Args:
2758 concept_ids: a list of concept IDs, in sequence
2759 concept_names: a list of corresponding concept names, in the same sequence
2760
2761 Returns:
2762 A list of Concept objects
2763
2764 Examples:
2765 >>> app.concepts.bulk_create(['id1', 'id2'], ['cute cat', 'cute dog'])
2766 """
2767
2768 res = self.api.add_concepts(concept_ids, concept_names)
2769 concepts = [self._to_obj(one) for one in res['concepts']]
2770 return concepts
2771
2772 def _to_obj(self, item): # type: (dict) -> Concept
2773
2774 concept_id = item['id']
2775 concept_name = item['name']
2776 app_id = item['app_id']
2777 created_at = item['created_at']
2778
2779 return Concept(
2780 concept_name=concept_name, concept_id=concept_id, app_id=app_id, created_at=created_at)
2781
2782
2783class Model(object):
2784
2785 def __init__(
2786 self, # type: Model
2787 api, # type: ApiClient
2788 item=None, # type: typing.Optional[dict]
2789 model_id=None, # type: typing.Optional[str]
2790 solutions=None # type: typing.Optional[Solutions]
2791 ):
2792 # type: (...) -> None
2793
2794 self.api = api # type: ApiClient
2795 self.solutions = ModelSolutions(
2796 solutions, self) # type: ModelSolutions
2797
2798 if model_id is not None:
2799 self.model_id = model_id
2800 self.model_version = None
2801 elif item:
2802 self.model_id = item['id']
2803 self.model_name = item['name']
2804 self.created_at = item['created_at']
2805 self.app_id = item['app_id']
2806 self.model_version = item['model_version']['id']
2807 self.model_status_code = item['model_version']['status']['code']
2808
2809 self.output_info = item.get('output_info', {})
2810
2811 output_config = self.output_info.get('output_config', {})
2812 self.concepts_mutually_exclusive = output_config.get('concepts_mutually_exclusive',
2813 False) # type: bool
2814 self.closed_environment = output_config.get(
2815 'closed_environment', False) # type: bool
2816
2817 self.hyper_parameters = output_config.get(
2818 'hyper_params') # type: typing.Optional[dict]
2819
2820 self.concepts = [] # type: typing.List[Concept]
2821 if self.output_info.get('data', {}).get('concepts'):
2822 for concept in self.output_info['data']['concepts']:
2823 concept = Concept(
2824 concept_name=concept['name'],
2825 concept_id=concept['id'],
2826 app_id=concept['app_id'],
2827 created_at=concept['created_at'])
2828 self.concepts.append(concept)
2829
2830 def get_info(self, verbose=False): # type: (bool) -> dict
2831 """ get model info, with or without the concepts associated with the model.
2832
2833 Args:
2834 verbose: default is False. True will yield output_info, with concepts of the model
2835
2836 Returns:
2837 raw json of the response
2838
2839 Examples:
2840 >>> # with basic model info
2841 >>> model.get_info()
2842 >>> # model info with concepts
2843 >>> model.get_info(verbose=True)
2844 """
2845
2846 if not verbose:
2847 ret = self.api.get_model(self.model_id, self.model_version)
2848 else:
2849 ret = self.api.get_model_output_info(
2850 self.model_id, self.model_version)
2851
2852 return ret
2853
2854 def get_concept_ids(self): # type: () -> typing.List[Concept]
2855 """ get concepts IDs associated with the model
2856
2857 Returns:
2858 a list of concept IDs
2859
2860 Examples:
2861 >>> ids = model.get_concept_ids()
2862 """
2863
2864 if self.concepts:
2865 concepts = [c.dict() for c in self.concepts]
2866 else:
2867 res = self.get_info(verbose=True)
2868 concepts = res['model']['output_info'].get(
2869 'data', {}).get('concepts', [])
2870
2871 return [c['id'] for c in concepts]
2872
2873 def dict(self): # type: () -> dict
2874
2875 data = {
2876 "model": {
2877 "name": self.model_name,
2878 "output_info": {
2879 "output_config": {
2880 "concepts_mutually_exclusive": self.concepts_mutually_exclusive,
2881 "closed_environment": self.closed_environment
2882 }
2883 }
2884 }
2885 }
2886
2887 if self.model_id:
2888 data['model']['id'] = self.model_id
2889
2890 if self.concepts:
2891 ids = [{"id": concept_id} for concept_id in self.concepts]
2892 data['model']['output_info']['data'] = {"concepts": ids}
2893
2894 return data
2895
2896 # type: (bool, int) -> typing.Union[Model, dict]
2897 def train(self, sync=True, timeout=60):
2898 """
2899 train the model in synchronous or asynchronous mode. Synchronous will block until the
2900 model is trained, async will not.
2901
2902 Args:
2903 sync: indicating synchronous or asynchronous, default is True
2904 timeout: Used when sync=True. Num. of seconds we should wait for training to complete.
2905
2906 Returns:
2907 the Model object
2908
2909 """
2910
2911 res = self.api.create_model_version(self.model_id)
2912
2913 status = res['status']
2914 if status['code'] == 10000:
2915 model_id = res['model']['id']
2916 model_version = res['model']['model_version']['id']
2917 model_status_code = res['model']['model_version']['status']['code']
2918 else:
2919 # TODO: This should probably be converted to a Model object.
2920 return res
2921
2922 if sync is False:
2923 # TODO: This should probably be converted to a Model object.
2924 return res
2925
2926 # train in sync despite the RESTful api is always async
2927 # will loop until the model is trained
2928 # 21103: queued for training
2929 # 21101: being trained
2930
2931 time_start = time.time()
2932
2933 res_ver = None
2934 while model_status_code == 21103 or model_status_code == 21101:
2935
2936 elapsed = time.time() - time_start
2937 if elapsed > timeout:
2938 break
2939
2940 if elapsed < 10:
2941 wait_interval = 1
2942 elif elapsed < 60:
2943 wait_interval = 5
2944 elif elapsed < 120:
2945 wait_interval = 10
2946 elif elapsed < 180:
2947 wait_interval = 20
2948 elif elapsed < 240:
2949 wait_interval = 30
2950 else:
2951 wait_interval = 60
2952
2953 time.sleep(wait_interval)
2954 res_ver = self.api.get_model_version(model_id, model_version)
2955 model_status_code = res_ver['model_version']['status']['code']
2956
2957 if res_ver:
2958 res['model']['model_version'] = res_ver['model_version']
2959
2960 return self._to_obj(res['model'])
2961
2962 def predict_by_url(
2963 self, # type: Model
2964 url, # type: str
2965 lang=None, # type: typing.Optional[str]
2966 is_video=False, # type: bool
2967 min_value=None, # type: typing.Optional[float]
2968 max_concepts=None, # type: typing.Optional[int]
2969 select_concepts=None, # type: typing.Optional[typing.List[Concept]]
2970 sample_ms=None # type: typing.Optional[int]
2971 ):
2972 # type: (...) -> dict
2973 """ predict a model with url
2974
2975 Args:
2976 url: publicly accessible url of an image
2977 lang: language to predict, if the translation is available
2978 is_video: whether this is a video
2979 min_value: threshold to cut the predictions, 0-1.0
2980 max_concepts: max concepts to keep in the predictions, 0-200
2981 select_concepts: a list of concepts that are selected to be exposed
2982 sample_ms: video frame prediction every sample_ms milliseconds
2983
2984 Returns:
2985 the prediction of the model in JSON format
2986 """
2987
2988 url = url.strip()
2989
2990 if is_video is True:
2991 input_ = Video(url=url) # type: Input
2992 else:
2993 input_ = Image(url=url)
2994
2995 model_output_info = ModelOutputInfo(
2996 output_config=ModelOutputConfig(
2997 language=lang,
2998 min_value=min_value,
2999 max_concepts=max_concepts,
3000 select_concepts=select_concepts,
3001 sample_ms=sample_ms))
3002
3003 res = self.predict([input_], model_output_info)
3004 return res
3005
3006 def predict_by_filename(
3007 self, # type: Model
3008 filename, # type: str
3009 lang=None, # type: typing.Optional[str]
3010 is_video=False, # type: bool
3011 min_value=None, # type: typing.Optional[float]
3012 max_concepts=None, # type: typing.Optional[int]
3013 select_concepts=None, # type: typing.Optional[typing.List[Concept]]
3014 sample_ms=None # type: typing.Optional[int]
3015 ):
3016 # type: (...) -> dict
3017 """ predict a model with a local filename
3018
3019 Args:
3020 filename: filename on local filesystem
3021 lang: language to predict, if the translation is available
3022 is_video: whether this is a video
3023 min_value: threshold to cut the predictions, 0-1.0
3024 max_concepts: max concepts to keep in the predictions, 0-200
3025 select_concepts: a list of concepts that are selected to be exposed
3026 sample_ms: video frame prediction every sample_ms milliseconds
3027
3028 Returns:
3029 the prediction of the model in JSON format
3030 """
3031
3032 with open(filename, 'rb') as fileio:
3033 if is_video is True:
3034 input_ = Video(file_obj=fileio) # type: Input
3035 else:
3036 input_ = Image(file_obj=fileio)
3037
3038 model_output_info = ModelOutputInfo(
3039 output_config=ModelOutputConfig(
3040 language=lang,
3041 min_value=min_value,
3042 max_concepts=max_concepts,
3043 select_concepts=select_concepts,
3044 sample_ms=sample_ms))
3045
3046 res = self.predict([input_], model_output_info)
3047 return res
3048
3049 def predict_by_bytes(
3050 self, # type: Model
3051 raw_bytes, # type: bytes
3052 lang=None, # type: typing.Optional[str]
3053 is_video=False, # type: bool
3054 min_value=None, # type: typing.Optional[float]
3055 max_concepts=None, # type: typing.Optional[int]
3056 select_concepts=None, # type: typing.Optional[typing.List[Concept]]
3057 sample_ms=None # type: typing.Optional[int]
3058 ):
3059 # type: (...) -> dict
3060 """ predict a model with image raw bytes
3061
3062 Args:
3063 raw_bytes: raw bytes of an image
3064 lang: language to predict, if the translation is available
3065 is_video: whether this is a video
3066 min_value: threshold to cut the predictions, 0-1.0
3067 max_concepts: max concepts to keep in the predictions, 0-200
3068 select_concepts: a list of concepts that are selected to be exposed
3069 sample_ms: video frame prediction every sample_ms milliseconds
3070
3071 Returns:
3072 the prediction of the model in JSON format
3073 """
3074
3075 base64_bytes = base64_lib.b64encode(raw_bytes)
3076
3077 if is_video is True:
3078 input_ = Video(base64=base64_bytes) # type: Input
3079 else:
3080 input_ = Image(base64=base64_bytes)
3081
3082 model_output_info = ModelOutputInfo(
3083 output_config=ModelOutputConfig(
3084 language=lang,
3085 min_value=min_value,
3086 max_concepts=max_concepts,
3087 select_concepts=select_concepts,
3088 sample_ms=sample_ms))
3089
3090 res = self.predict([input_], model_output_info)
3091 return res
3092
3093 def predict_by_base64(
3094 self, # type: Model
3095 base64_bytes, # type: str
3096 lang=None, # type: typing.Optional[str]
3097 is_video=False, # type: bool
3098 min_value=None, # type: typing.Optional[float]
3099 max_concepts=None, # type: typing.Optional[int]
3100 select_concepts=None, # type: typing.Optional[typing.List[Concept]]
3101 sample_ms=None # type: typing.Optional[int]
3102 ):
3103 # type: (...) -> dict
3104 """ predict a model with base64 encoded image bytes
3105
3106 Args:
3107 base64_bytes: base64 encoded image bytes
3108 lang: language to predict, if the translation is available
3109 is_video: whether this is a video
3110 min_value: threshold to cut the predictions, 0-1.0
3111 max_concepts: max concepts to keep in the predictions, 0-200
3112 select_concepts: a list of concepts that are selected to be exposed
3113 sample_ms: video frame prediction every sample_ms milliseconds
3114
3115 Returns:
3116 the prediction of the model in JSON format
3117 """
3118
3119 if is_video is True:
3120 input_ = Video(base64=base64_bytes) # type: Input
3121 else:
3122 input_ = Image(base64=base64_bytes)
3123
3124 model_output_info = ModelOutputInfo(
3125 output_config=ModelOutputConfig(
3126 language=lang,
3127 min_value=min_value,
3128 max_concepts=max_concepts,
3129 select_concepts=select_concepts,
3130 sample_ms=sample_ms))
3131
3132 res = self.predict([input_], model_output_info)
3133 return res
3134
3135 # TODO(Rok) MEDIUM: Add bulk_predict methods.
3136 def predict(self, inputs, model_output_info=None):
3137 # type: (typing.List[Input], typing.Optional[ModelOutputInfo]) -> dict
3138 """ predict with multiple images
3139
3140 Args:
3141 inputs: a list of Image objects
3142 model_output_info: the model output info
3143
3144 Returns:
3145 the prediction of the model in JSON format
3146 """
3147
3148 res = self.api.predict_model(
3149 self.model_id, inputs, self.model_version, model_output_info)
3150 return res
3151
3152 def merge_concepts(self, concept_ids, overwrite=False):
3153 # type: (typing.List[str], bool) -> Model
3154 """ merge concepts in a model
3155
3156 When overwrite is False, if the concept does not exist in the model it will be appended.
3157 Otherwise, the original one will be kept.
3158
3159 Args:
3160 concept_ids: a list of concept id
3161 overwrite: True or False. If True, the existing concepts will be replaced
3162
3163 Returns:
3164 the Model object
3165 """
3166
3167 if overwrite is True:
3168 action = 'overwrite'
3169 else:
3170 action = 'merge'
3171
3172 model = self.update(action=action, concept_ids=concept_ids)
3173 return model
3174
3175 def add_concepts(self, concept_ids): # type: (typing.List[str]) -> Model
3176 """ merge concepts into a model
3177
3178 This is just an alias of `merge_concepts`, for easier understanding of adding new concepts
3179 to the model without overwritting them.
3180
3181 Args:
3182 concept_ids: a list of concept IDs
3183
3184 Returns:
3185 the Model object
3186
3187 Examples:
3188 >>> model = self.app.models.get('model_id')
3189 >>> model.add_concepts(['cat', 'dog'])
3190 """
3191
3192 return self.merge_concepts(concept_ids)
3193
3194 def update(
3195 self, # type: Model
3196 action='merge', # type: str
3197 model_name=None, # type: typing.Optional[str]
3198 concepts_mutually_exclusive=None, # type: typing.Optional[bool]
3199 closed_environment=None, # type: typing.Optional[bool]
3200 concept_ids=None # type: typing.Optional[typing.List[str]]
3201 ):
3202 # type: (...) -> Model
3203 """
3204 Update the model attributes. The name of the model, list of concepts, and
3205 the attributes ``concepts_mutually_exclusive`` and ``closed_environment`` can
3206 be changed. Note this is a overwriting change. For a valid call, at least one or
3207 more attributes should be specified. Otherwise the call will be just skipped without error.
3208
3209 Args:
3210 action: the way to patch the model: ['merge', 'remove', 'overwrite']
3211 model_name: name of the model
3212 concepts_mutually_exclusive: whether the concepts are mutually exclusive
3213 closed_environment: whether negative concepts should be taken into account during training
3214 concept_ids: a list of concept ids
3215
3216 Returns:
3217 the Model object
3218
3219 Examples:
3220 >>> model = self.app.models.get('model_id')
3221 >>> model.update(model_name="new_model_name")
3222 >>> model.update(concepts_mutually_exclusive=False)
3223 >>> model.update(closed_environment=True)
3224 >>> model.update(concept_ids=["bird", "hurd"])
3225 >>> model.update(concepts_mutually_exclusive=True, concept_ids=["bird", "hurd"])
3226 """
3227
3228 args = [model_name, concepts_mutually_exclusive,
3229 closed_environment, concept_ids]
3230 if not any(map(lambda x: x is not None, args)):
3231 return self
3232
3233 model = {
3234 "id": self.model_id,
3235 } # type: typing.Dict[str, typing.Any]
3236
3237 if model_name:
3238 model["name"] = model_name
3239
3240 output_config = {}
3241 if concepts_mutually_exclusive is not None:
3242 # model["output_info"]["output_config"][
3243 # "concepts_mutually_exclusive"] = concepts_mutually_exclusive
3244 output_config["concepts_mutually_exclusive"] = concepts_mutually_exclusive
3245
3246 if closed_environment is not None:
3247 # model["output_info"]["output_config"]["closed_environment"] = closed_environment
3248 output_config["closed_environment"] = closed_environment
3249
3250 data = {}
3251 if concept_ids is not None:
3252 # model["output_info"]["data"]["concepts"] = [{"id": concept_id} for concept_id in concept_ids]
3253 data["concepts"] = [{"id": concept_id}
3254 for concept_id in concept_ids]
3255
3256 output_info = {}
3257 if output_config:
3258 output_info["output_config"] = output_config
3259 if data:
3260 output_info["data"] = data
3261
3262 if output_info:
3263 model["output_info"] = output_info
3264
3265 res = self.api.patch_model(model, action)
3266 model = res['models'][0]
3267 return self._to_obj(model)
3268
3269 # type: (typing.List[str]) -> Model
3270 def delete_concepts(self, concept_ids):
3271 """ delete concepts from a model
3272
3273 Args:
3274 concept_ids: a list of concept IDs to be removed
3275
3276 Returns:
3277 the Model object
3278
3279 Examples:
3280 >>> model = self.app.models.get('model_id')
3281 >>> model.delete_concepts(['cat', 'dog'])
3282 """
3283
3284 model = self.update(action='remove', concept_ids=concept_ids)
3285 return model
3286
3287 def list_versions(self): # type: () -> dict
3288 """ list all model versions
3289
3290 Returns:
3291 the JSON response
3292
3293 Examples:
3294 >>> model = self.app.models.get('model_id')
3295 >>> model.list_versions()
3296 """
3297
3298 res = self.api.get_model_versions(self.model_id)
3299 return res
3300
3301 def get_version(self, version_id): # type: (str) -> dict
3302 """ get model version info for a particular version
3303
3304 Args:
3305 version_id: version id of the model version
3306
3307 Returns:
3308 the JSON response
3309
3310 Examples:
3311 >>> model = self.app.models.get('model_id')
3312 >>> model.get_version('model_version_id')
3313 """
3314
3315 res = self.api.get_model_version(self.model_id, version_id)
3316 return res
3317
3318 def delete_version(self, version_id): # type: (str) -> dict
3319 """ delete model version by version_id
3320
3321 Args:
3322 version_id: version id of the model version
3323
3324 Returns:
3325 the JSON response
3326
3327 Examples:
3328 >>> model = self.app.models.get('model_id')
3329 >>> model.delete_version('model_version_id')
3330 """
3331
3332 res = self.api.delete_model_version(self.model_id, version_id)
3333 return res
3334
3335 def create_version(self): # type: () -> dict
3336
3337 res = self.api.create_model_version(self.model_id)
3338 return res
3339
3340 def get_inputs(self, version_id=None, page=1, per_page=20):
3341 # type: (typing.Optional[str], int, int) -> dict
3342 """
3343 Get all the inputs from the model or a specific model version.
3344 Without specifying a model version id, this will yield all inputs
3345
3346 Args:
3347 version_id: model version id
3348 page: page number
3349 per_page: number of inputs to return for each page
3350
3351 Returns:
3352 A list of Input objects
3353 """
3354
3355 res = self.api.get_model_inputs(
3356 self.model_id, version_id, page, per_page)
3357
3358 return res
3359
3360 def send_concept_feedback(
3361 self, # type: Model
3362 input_id, # type: str
3363 url, # type: str
3364 concepts=None, # type: typing.Optional[typing.List[str]]
3365 not_concepts=None, # type: typing.Optional[typing.List[str]]
3366 feedback_info=None # type: typing.Optional[FeedbackInfo]
3367 ):
3368 # type: (...) -> dict
3369 """
3370 Send feedback for this model
3371
3372 Args:
3373 input_id: input id for the feedback
3374 url: the url of the input
3375 concepts: concepts that are present
3376 not_concepts: concepts that aren't present
3377 feedback_info: feedback info
3378
3379 Returns:
3380 None
3381 """
3382
3383 feedback_input = Image(
3384 url=url,
3385 image_id=input_id,
3386 concepts=concepts,
3387 not_concepts=not_concepts,
3388 feedback_info=feedback_info)
3389 res = self.api.send_model_feedback(
3390 self.model_id, self.model_version, feedback_input)
3391
3392 return res
3393
3394 def send_region_feedback(
3395 self, # type: Model
3396 input_id, # type: str
3397 url, # type: str
3398 concepts=None, # type: typing.Optional[typing.List[str]]
3399 not_concepts=None, # type: typing.Optional[typing.List[str]]
3400 regions=None, # type: typing.Optional[typing.List[Region]]
3401 feedback_info=None # type: typing.Optional[FeedbackInfo]
3402 ):
3403 # type: (...) -> dict
3404 """
3405 Send feedback for this model
3406
3407 Args:
3408 input_id: input id for the feedback
3409 url: the input url
3410
3411 Returns:
3412 None
3413 """
3414
3415 feedback_input = Image(
3416 url=url,
3417 image_id=input_id,
3418 concepts=concepts,
3419 not_concepts=not_concepts,
3420 regions=regions,
3421 feedback_info=feedback_info)
3422 res = self.api.send_model_feedback(
3423 self.model_id, self.model_version, feedback_input)
3424
3425 return res
3426
3427 def _to_obj(self, item): # type: (dict) -> Model
3428 """ convert a model json object to Model object """
3429 return Model(self.api, item)
3430
3431 def evaluate(self): # type: () -> dict
3432 """ run model evaluation
3433
3434 Returns:
3435 the model version data with evaluation metrics in JSON format
3436 """
3437
3438 if self.model_version is None:
3439 raise UserError(
3440 'To run model evaluation, please set the model_version field')
3441
3442 res = self.api.run_model_evaluation(self.model_id, self.model_version)
3443 return res
3444
3445
3446class ModelSolutions(object):
3447
3448 def __init__(self, solutions, model): # type: (Solutions, Model) -> None
3449 self.moderation = ModelSolutionsModeration(
3450 solutions, model) # type: ModelSolutionsModeration
3451
3452
3453class ModelSolutionsModeration(object):
3454
3455 def __init__(self, solutions, model): # type: (Solutions, Model) -> None
3456 self.solutions = solutions # type: Solutions
3457 self.model = model # type: Model
3458
3459 def predict_by_url(self, url): # type: (str) -> dict
3460 return self.solutions.moderation.predict_model(self.model.model_id, url)
3461
3462
3463class Concept(object):
3464 """ Clarifai Concept
3465 """
3466
3467 def __init__(
3468 self, # type: Concept
3469 concept_name=None, # type: typing.Optional[str]
3470 concept_id=None, # type: typing.Optional[str]
3471 app_id=None, # type: typing.Optional[str]
3472 created_at=None, # type: typing.Optional[str]
3473 value=None # type: typing.Optional[float]
3474 ):
3475 self.concept_name = concept_name
3476 self.concept_id = concept_id
3477 self.app_id = app_id
3478 self.created_at = created_at
3479 self.value = value
3480
3481 def dict(self): # type: () -> dict
3482
3483 data = {} # type: typing.Dict[str, typing.Any]
3484
3485 if self.concept_name is not None:
3486 data['name'] = self.concept_name
3487
3488 if self.concept_id is not None:
3489 data['id'] = self.concept_id
3490
3491 if self.app_id is not None:
3492 data['app_id'] = self.app_id
3493
3494 if self.created_at is not None:
3495 data['created_at'] = self.created_at
3496
3497 if self.value is not None:
3498 data['value'] = self.value
3499
3500 return data
3501
3502
3503class PublicModels(object):
3504 """
3505 A collection of already existing models provided by the API for immediate use.
3506 """
3507
3508 # TODO(Rok) HIGH: Construct these with the solution object.
3509 def __init__(self, api): # type: (ApiClient) -> None
3510 """ Ctor. """
3511 """ Apparel model recognizes clothing, accessories, and other fashion-related items. """
3512 self.apparel_model = Model(
3513 api, model_id='e0be3b9d6a454f0493ac3a30784001ff') # type: Model
3514 """ Celebrity model identifies celebrities that closely resemble detected faces. """
3515 self.celebrity_model = Model(
3516 api, model_id='e466caa0619f444ab97497640cefc4dc') # type: Model
3517 """ Color model recognizes dominant colors on an input. """
3518 self.color_model = Model(
3519 api, model_id='eeed0b6733a644cea07cf4c60f87ebb7') # type: Model
3520 """ Demographics model predicts the age, gender, and cultural appearance. """
3521 self.demographics_model = Model(
3522 api, model_id='c0c0ac362b03416da06ab3fa36fb58e3') # type: Model
3523 """ Face detection model detects the presence and location of human faces. """
3524 self.face_detection_model = Model(
3525 api, model_id='a403429f2ddf4b49b307e318f00e528b') # type: Model
3526 """
3527 Face embedding model computes numerical embedding vectors using our Face detection model.
3528 """
3529 self.face_embedding_model = Model(
3530 api, model_id='d02b4508df58432fbb84e800597b8959') # type: Model
3531 """ Focus model returns overall focus and identifies in-focus regions. """
3532 self.focus_model = Model(
3533 api, model_id='c2cf7cecd8a6427da375b9f35fcd2381') # type: Model
3534 """ Food model recognizes food items and dishes, down to the ingredient level. """
3535 self.food_model = Model(
3536 api, model_id='bd367be194cf45149e75f01d59f77ba7') # type: Model
3537 """ General embedding model computes numerical embedding vectors using our General model. """
3538 self.general_embedding_model = Model(
3539 api, model_id='bbb5f41425b8468d9b7a554ff10f8581') # type: Model
3540 """ General model predicts most generally. """
3541 self.general_model = Model(
3542 api, model_id='aaa03c23b3724a16a56b629203edc62c') # type: Model
3543 """ Landscape quality model predicts the quality of a landscape image. """
3544 self.landscape_quality_model = Model(
3545 api, model_id='bec14810deb94c40a05f1f0eb3c91403') # type: Model
3546 """ Logo model detects and identifies brand logos. """
3547 self.logo_model = Model(
3548 api, model_id='c443119bf2ed4da98487520d01a0b1e3') # type: Model
3549 """ Moderation model predicts inputs such as safety, gore, nudity, etc. """
3550 self.moderation_model = Model(
3551 api, model_id='d16f390eb32cad478c7ae150069bd2c6') # type: Model
3552 """ NSFW model identifies different levels of nudity. """
3553 self.nsfw_model = Model(
3554 api, model_id='e9576d86d2004ed1a38ba0cf39ecb4b1') # type: Model
3555 """ Portrait quality model predicts the quality of a portrait image. """
3556 self.portrait_quality_model = Model(
3557 api, model_id='de9bd05cfdbf4534af151beb2a5d0953') # type: Model
3558 """ Textures & Patterns model predicts textures and patterns on an image. """
3559 self.textures_and_patterns_model = Model(
3560 api, model_id='fbefb47f9fdb410e8ce14f24f54b47ff') # type: Model
3561 """ Travel model recognizes travel and hospitality-related concepts. """
3562 self.travel_model = Model(
3563 api, model_id='eee28c313d69466f836ab83287a54ed9') # type: Model
3564 """ Wedding model recognizes wedding-related concepts bride, groom, flowers, and more. """
3565 self.wedding_model = Model(
3566 api, model_id='c386b7a870114f4a87477c0824499348') # type: Model
3567
3568
3569class ApiClient(object):
3570 """ Handles auth and making requests for you.
3571
3572 The constructor for API access. You must sign up at developer.clarifai.com first and create an
3573 application in order to generate your credentials for API access.
3574
3575 Args:
3576 self: instance of ApiClient
3577 app_id: (DEPRECATED) the app_id for an application you've created in your Clarifai account.
3578 app_secret: (DEPRECATED) the app_secret for the same application.
3579 base_url: base URL of the API endpoints.
3580 api_key: the API key, used for authentication.
3581 quiet: if True then silence debug prints.
3582 log_level: log level from logging module
3583 """
3584
3585 patch_actions = ['merge', 'remove', 'overwrite']
3586 concepts_patch_actions = ['overwrite']
3587
3588 def __init__(
3589 self, # type: ApiClient
3590 app_id=None, # type: typing.Optional[str]
3591 app_secret=None, # type: typing.Optional[str]
3592 base_url=None, # type: typing.Optional[str]
3593 api_key=None, # type: typing.Optional[str]
3594 quiet=True, # type: bool
3595 log_level=None # type: typing.Optional[int]
3596 ):
3597
3598 if app_id or app_secret:
3599 warnings.warn('Tokens deprecated', DeprecationWarning)
3600 raise DeprecationWarning(TOKENS_DEPRECATED_MESSAGE)
3601
3602 if not log_level:
3603 if quiet:
3604 log_level = logging.ERROR
3605 else:
3606 log_level = logging.DEBUG
3607 logger.setLevel(log_level)
3608
3609 if not api_key:
3610 api_key = self._read_key_from_env_or_os()
3611 self.api_key = api_key
3612
3613 if not base_url:
3614 base_url = self._read_base_from_env_or_os()
3615 parsed = urlparse(base_url)
3616 scheme = 'https' if parsed.scheme == '' else parsed.scheme
3617 base_url_parsed = parsed.path if not parsed.netloc else parsed.netloc
3618 self.base_url = base_url_parsed
3619 self.scheme = scheme # type: typing.Optional[str]
3620 self.basev2 = urljoin(scheme + '://', base_url_parsed) # type: str
3621 logger.debug("Base url: %s", self.basev2)
3622 self.token = None
3623 self.headers = None
3624
3625 self.session = self._make_requests_session() # type: requests.Session
3626
3627 def _make_requests_session(self): # type: () -> requests.Session
3628 http_adapter = requests.adapters.HTTPAdapter(
3629 max_retries=RETRIES, pool_connections=CONNECTIONS, pool_maxsize=CONNECTIONS)
3630
3631 session = requests.Session()
3632 session.mount('http://', http_adapter)
3633 session.mount('https://', http_adapter)
3634 return session
3635
3636 def _read_key_from_env_or_os(self): # type: () -> typing.Optional[str]
3637 conf_file = self._config_file_path()
3638 env_api_key = os.environ.get('CLARIFAI_API_KEY')
3639 if env_api_key:
3640 logger.debug("Using env. variable CLARIFAI_API_KEY for API key")
3641 return env_api_key
3642 elif os.path.exists(conf_file):
3643 parser = ConfigParser()
3644 parser.optionxform = str # type: ignore
3645
3646 with open(conf_file, 'r') as fdr:
3647 parser.readfp(fdr)
3648
3649 if parser.has_option('clarifai', 'CLARIFAI_API_KEY'):
3650 return parser.get('clarifai', 'CLARIFAI_API_KEY')
3651 return None
3652
3653 def _read_base_from_env_or_os(self): # type: () -> str
3654 conf_file = self._config_file_path()
3655 env_clarifai_api_base = os.environ.get('CLARIFAI_API_BASE')
3656 if env_clarifai_api_base:
3657 base_url = env_clarifai_api_base
3658 elif os.path.exists(conf_file):
3659 parser = ConfigParser()
3660 parser.optionxform = str # type: ignore
3661
3662 with open(conf_file, 'r') as fdr:
3663 parser.readfp(fdr)
3664
3665 if parser.has_option('clarifai', 'CLARIFAI_API_BASE'):
3666 base_url = parser.get('clarifai', 'CLARIFAI_API_BASE')
3667 else:
3668 base_url = 'api.clarifai.com'
3669 else:
3670 base_url = 'api.clarifai.com'
3671 return base_url
3672
3673 def _config_file_path(self): # type: () -> str
3674 if platform.system() == 'Windows':
3675 home_dir = os.environ.get('HOMEPATH', '.')
3676 else:
3677 home_dir = os.environ.get('HOME', '.')
3678 conf_file = os.path.join(home_dir, '.clarifai', 'config')
3679 return conf_file
3680
3681 def get_token(self): # type: () -> None
3682 """
3683 Tokens are deprecated, please switch to API keys. See here how:
3684 "http://help.clarifai.com/api/account-related/all-about-api-keys"
3685 """
3686 warnings.warn('Tokens deprecated', DeprecationWarning)
3687 raise DeprecationWarning(TOKENS_DEPRECATED_MESSAGE)
3688
3689 def set_token(self, token): # type: (str) -> None
3690 """
3691 Tokens are deprecated, please switch to API keys. See here how:
3692 "http://help.clarifai.com/api/account-related/all-about-api-keys"
3693 """
3694 warnings.warn('Tokens deprecated', DeprecationWarning)
3695 raise DeprecationWarning(TOKENS_DEPRECATED_MESSAGE)
3696
3697 def delete_token(self): # type: () -> None
3698 """
3699 Tokens are deprecated, please switch to API keys. See here how:
3700 "http://help.clarifai.com/api/account-related/all-about-api-keys"
3701 """
3702 warnings.warn('Tokens deprecated', DeprecationWarning)
3703 raise DeprecationWarning(TOKENS_DEPRECATED_MESSAGE)
3704
3705 def _grpc_stub(self): # type: () -> V2Stub
3706 return V2Stub( # type: ignore
3707 GRPCJSONChannel(
3708 session=self.session, key=self.api_key, base_url=self.basev2, service_descriptor=_V2))
3709
3710 # type: (typing.Callable, typing.Any) -> dict
3711 def _grpc_request(self, method, argument):
3712
3713 # only retry under when status_code is non-200, under max-tries
3714 # and under some circumstances
3715 max_attempts = 3
3716 for attempt_num in range(1, max_attempts + 1):
3717 try:
3718 res = method(argument)
3719
3720 dict_res = protobuf_to_dict(res)
3721 logger.debug("\nRESULT:\n%s", pformat(dict_res))
3722 return dict_res
3723 except ApiError as ex:
3724 if ex.response is not None:
3725 status_code = ex.response.status_code
3726
3727 if attempt_num == max_attempts:
3728 logger.debug("Failed after %d retries" % max_attempts)
3729 raise
3730
3731 # handle Gateway Error, normally retry will solve the problem
3732 if int(status_code / 100) == 5:
3733 continue
3734
3735 # handle throttling
3736 # back off with 2/4/8 seconds
3737 # normally, this will be settled in 1 or 2 retries
3738 if status_code == 429:
3739 time.sleep(2**attempt_num)
3740 continue
3741
3742 # in other cases, error out without retrying
3743 raise
3744
3745 # The for loop above either returns or raises.
3746 raise Exception('This code is never reached')
3747
3748 def add_inputs(self, objs): # type: (typing.List[Input]) -> dict
3749 """ Add a list of Images or Videos to an application.
3750
3751 Args:
3752 objs: A list of Image or Video objects.
3753
3754 Returns:
3755 raw JSON response from the API server, with a list of inputs and corresponding import
3756 status
3757 """
3758 if not isinstance(objs, list):
3759 raise UserError("objs must be a list")
3760
3761 inputs_pb = []
3762 for obj in objs:
3763 if not isinstance(obj, (Image, Video)):
3764 raise UserError(
3765 "Not valid type of content to add. Must be Image or Video")
3766 if obj.input_id:
3767 if not isinstance(obj.input_id, basestring):
3768 raise UserError(
3769 "Not valid input ID. Must be a string or None")
3770 if '/' in obj.input_id:
3771 raise UserError(
3772 'Not valid input ID. Cannot contain character: "/"')
3773
3774 resulting_protobuf = dict_to_protobuf(InputPB, obj.dict())
3775
3776 inputs_pb.append(resulting_protobuf)
3777
3778 return self._grpc_request(self._grpc_stub().PostInputs, PostInputsRequest(inputs=inputs_pb))
3779
3780 def search_inputs(self, query, page=1, per_page=20): # type: (dict, int, int) -> dict
3781 """ Search an application and get predictions (optional)
3782
3783 Args:
3784 query: the JSON query object that complies with Clarifai RESTful API
3785 page: the page of results to get, starts at 1.
3786 per_page: number of results returned per page
3787
3788 Returns:
3789 raw JSON response from the API server, with a list of inputs and corresponding ranking
3790 scores
3791 """
3792
3793 q = dict_to_protobuf(Query, query)
3794
3795 return self._grpc_request(
3796 self._grpc_stub().PostSearches,
3797 PostSearchesRequest(query=q, pagination=Pagination(page=page, per_page=per_page)))
3798
3799 def get_input(self, input_id): # type: (str) -> dict
3800 """ Get a single image by it's id.
3801
3802 Args:
3803 input_id: the id of the Image.
3804
3805 Returns:
3806 raw JSON response from the API server
3807
3808 HTTP code:
3809 200 for Found
3810 404 for Not Found
3811 """
3812
3813 return self._grpc_request(self._grpc_stub().GetInput, GetInputRequest(input_id=input_id))
3814
3815 def get_inputs(self, page=1, per_page=20): # type: (int, int) -> dict
3816 """ List all images for the Application, with pagination
3817
3818 Args:
3819 page: the page of results to get, starts at 1.
3820 per_page: number of results returned per page
3821
3822 Returns:
3823 raw JSON response from the API server, with paginated list of inputs and corresponding
3824 status
3825 """
3826
3827 return self._grpc_request(self._grpc_stub().ListInputs,
3828 ListInputsRequest(page=page, per_page=per_page))
3829
3830 def get_inputs_status(self): # type: () -> dict
3831 """ Get counts of inputs in the Application.
3832
3833 Returns:
3834 counts of the inputs, including processed, processing, etc. in JSON format.
3835 """
3836
3837 return self._grpc_request(self._grpc_stub().GetInputCount, GetInputCountRequest())
3838
3839 def delete_input(self, input_id): # type: (str) -> dict
3840 """ Delete a single input by its id.
3841
3842 Args:
3843 input_id: the id of the input
3844
3845 Returns:
3846 status of the deletion, in JSON format.
3847 """
3848
3849 return self._grpc_request(self._grpc_stub().DeleteInput, DeleteInputRequest(input_id=input_id))
3850
3851 def delete_inputs(self, input_ids): # type: (typing.List[str]) -> dict
3852 """ bulk delete inputs with a list of input IDs
3853
3854 Args:
3855 input_ids: the ids of the input, in a list
3856
3857 Returns:
3858 status of the bulk deletion, in JSON format.
3859 """
3860
3861 return self._grpc_request(self._grpc_stub().DeleteInputs, DeleteInputsRequest(ids=input_ids))
3862
3863 def delete_all_inputs(self): # type: () -> dict
3864 """ delete all inputs from the application
3865
3866 Returns:
3867 status of the deletion, in JSON format.
3868 """
3869
3870 return self._grpc_request(self._grpc_stub().DeleteInputs, DeleteInputsRequest(delete_all=True))
3871
3872 def patch_inputs(self, action, inputs):
3873 # type: (str, typing.List[typing.Union[Input]]) -> dict
3874 """ bulk update inputs, to delete or modify concepts
3875
3876 Args:
3877 action: "merge" or "remove" or "overwrite"
3878 inputs: list of inputs
3879
3880 Returns:
3881 the update status, in JSON format
3882
3883 """
3884
3885 if action not in self.patch_actions:
3886 raise UserError("action not supported.")
3887
3888 inputs_pb = []
3889 for input_ in inputs:
3890 input_dict = input_.dict()
3891
3892 if 'data' not in input_dict:
3893 continue
3894
3895 reduced_input_dict = copy.deepcopy(input_dict)
3896 for key in input_dict['data'].keys():
3897 if key not in ['concepts', 'metadata', 'regions']:
3898 del reduced_input_dict['data'][key]
3899
3900 resulting_protobuf = dict_to_protobuf(InputPB, reduced_input_dict)
3901
3902 inputs_pb.append(resulting_protobuf)
3903
3904 return self._grpc_request(self._grpc_stub().PatchInputs,
3905 PatchInputsRequest(action=action, inputs=inputs_pb))
3906
3907 def get_concept(self, concept_id): # type: (str) -> dict
3908 """ Get a single concept by it's id.
3909
3910 Args:
3911 concept_id: unique id of the concept
3912
3913 Returns:
3914 the concept in JSON format with HTTP 200 Status
3915 or HTTP 404 with concept not found
3916 """
3917 return self._grpc_request(
3918 self._grpc_stub().GetConcept, GetConceptRequest(concept_id=concept_id))
3919
3920 def get_concepts(self, page=1, per_page=20): # type: (int, int) -> dict
3921 """ List all concepts for the Application.
3922
3923 Args:
3924 page: the page of results to get, starts at 1.
3925 per_page: number of results returned per page
3926
3927 Returns:
3928 a list of concepts in JSON format
3929 """
3930
3931 return self._grpc_request(self._grpc_stub().ListConcepts,
3932 ListConceptsRequest(page=page, per_page=per_page))
3933
3934 # TODO(Rok) MEDIUM: Allow skipping concept_names.
3935 def add_concepts(self, concept_ids, concept_names):
3936 # type: (typing.List[str], typing.List[typing.Optional[str]]) -> dict
3937 """ Add a list of concepts
3938
3939 Args:
3940 concept_ids: a list of concept id
3941 concept_names: a list of concept name
3942
3943 Returns:
3944 a list of concepts in JSON format along with the status code
3945 """
3946
3947 if not isinstance(concept_ids, list) or \
3948 not isinstance(concept_names, list):
3949 raise UserError(
3950 'concept_ids and concept_names should be both be list ')
3951
3952 if len(concept_ids) != len(concept_names):
3953 raise UserError(
3954 'length of concept id list should match length of the concept name list')
3955
3956 concepts = []
3957 for cid, cname in zip(concept_ids, concept_names):
3958 if cname is None:
3959 concept = {'id': cid}
3960 else:
3961 concept = {'id': cid, 'name': cname}
3962 concepts.append(dict_to_protobuf(ConceptPB, concept))
3963
3964 return self._grpc_request(
3965 self._grpc_stub().PostConcepts, PostConceptsRequest(concepts=concepts))
3966
3967 def search_concepts(self, term, page=1, per_page=20, language=None):
3968 # type: (str, int, int, typing.Optional[str]) -> dict
3969 """ Search concepts
3970
3971 Args:
3972 term: search term with wildcards
3973 page: the page of results to get, starts at 1.
3974 per_page: number of results returned per page
3975 language: language to search for the translation
3976
3977 Returns:
3978 a list of concepts in JSON format along with the status code
3979
3980 """
3981
3982 return self._grpc_request(
3983 self._grpc_stub().PostConceptsSearches,
3984 PostConceptsSearchesRequest(
3985 concept_query=ConceptQuery(name=term, language=language),
3986 pagination=Pagination(page=page, per_page=per_page)))
3987
3988 # type: (str, typing.List[Concept]) -> dict
3989 def patch_concepts(self, action, concepts):
3990 """ bulk update concepts, to delete or modify concepts
3991
3992 Args:
3993 action: only "overwrite" is supported
3994 concepts: a list of Concept(concept_name='', concept_id='')
3995
3996 Returns:
3997 the update status, in JSON format
3998
3999 """
4000
4001 if action not in self.concepts_patch_actions:
4002 raise UserError("action not supported.")
4003
4004 concepts_pb = [dict_to_protobuf(ConceptPB, c.dict()) for c in concepts]
4005 return self._grpc_request(self._grpc_stub().PatchConcepts,
4006 PatchConceptsRequest(action=action, concepts=concepts_pb))
4007
4008 def get_models(self, page=1, per_page=20): # type: (int, int) -> dict
4009 """ get all models with pagination
4010
4011 Args:
4012 page: page number
4013 per_page: number of models to return per page
4014
4015 Returns:
4016 a list of models in JSON format
4017 """
4018
4019 response = self._grpc_request(self._grpc_stub().ListModels,
4020 ListModelsRequest(page=page, per_page=per_page))
4021 return response
4022
4023 def get_model(self, model_id,
4024 model_version_id=None): # type: (str, typing.Optional[str]) -> dict
4025 """ get model basic info by model id
4026
4027 Args:
4028 model_id: the unique identifier of the model
4029 model_version_id: the unique identifier of the model version
4030
4031 Returns:
4032 the model info in JSON format
4033 """
4034
4035 return self._grpc_request(
4036 self._grpc_stub().GetModel,
4037 GetModelRequest(model_id=_escape(model_id), version_id=model_version_id))
4038
4039 def get_model_output_info(self, model_id,
4040 model_version_id=None): # type: (str, typing.Optional[str]) -> dict
4041 """ get model output info by model id
4042
4043 Args:
4044 model_id: the unique identifier of the model
4045 model_version_id: the unique identifier of the model version
4046
4047 Returns:
4048 the model info with output_info in JSON format
4049 """
4050 return self._grpc_request(
4051 self._grpc_stub().GetModelOutputInfo,
4052 GetModelRequest(model_id=_escape(model_id), version_id=model_version_id))
4053
4054 # type: (str, int, int) -> dict
4055 def get_model_versions(self, model_id, page=1, per_page=20):
4056 """ get model versions
4057
4058 Args:
4059 model_id: the unique identifier of the model
4060 page: page number
4061 per_page: the number of versions to return per page
4062
4063 Returns:
4064 a list of model versions in JSON format
4065 """
4066
4067 return self._grpc_request(
4068 self._grpc_stub().ListModelVersions,
4069 ListModelVersionsRequest(model_id=model_id, page=page, per_page=per_page))
4070
4071 def get_model_version(self, model_id, version_id): # type: (str, str) -> dict
4072 """ get model info for a specific model version
4073
4074 Args:
4075 model_id: the unique identifier of a model
4076 version_id: the model version id
4077 """
4078
4079 return self._grpc_request(self._grpc_stub().GetModelVersion,
4080 GetModelVersionRequest(model_id=model_id, version_id=version_id))
4081
4082 def delete_model_version(self, model_id, model_version): # type: (str, str) -> dict
4083 """ delete a model version """
4084
4085 return self._grpc_request(
4086 self._grpc_stub().DeleteModelVersion,
4087 DeleteModelVersionRequest(model_id=_escape(model_id), version_id=model_version))
4088
4089 def delete_model(self, model_id): # type: (str) -> dict
4090 """ delete a model """
4091
4092 return self._grpc_request(
4093 self._grpc_stub().DeleteModel, DeleteModelRequest(model_id=_escape(model_id)))
4094
4095 def delete_models(self, model_ids): # type: (typing.List[str]) -> dict
4096 """ delete the models """
4097
4098 return self._grpc_request(
4099 self._grpc_stub().DeleteModels,
4100 DeleteModelsRequest(ids=[_escape(id_) for id_ in model_ids]))
4101
4102 def delete_all_models(self): # type: () -> dict
4103 """ delete all models """
4104
4105 return self._grpc_request(self._grpc_stub().DeleteModels, DeleteModelsRequest(delete_all=True))
4106
4107 def get_model_inputs(self, model_id, version_id=None, page=1, per_page=20):
4108 # type: (str, typing.Optional[str], int, int) -> dict
4109 """ get inputs for the latest model or a specific model version """
4110
4111 if version_id:
4112 version_id = _escape(version_id)
4113
4114 return self._grpc_request(
4115 self._grpc_stub().ListModelInputs,
4116 ListModelInputsRequest(
4117 model_id=_escape(model_id), version_id=version_id, page=page, per_page=per_page))
4118
4119 def search_models(self, name=None, model_type=None):
4120 # type: (typing.Optional[str], typing.Optional[str]) -> dict
4121 """ search model by name and type """
4122
4123 return self._grpc_request(
4124 self._grpc_stub().PostModelsSearches,
4125 PostModelsSearchesRequest(model_query=ModelQuery(name=name, type=model_type)))
4126
4127 def create_model(
4128 self, # type: ApiClient
4129 model_id, # type: str
4130 model_name=None, # type: typing.Optional[str]
4131 concepts=None, # type: typing.Optional[typing.List[str]]
4132 concepts_mutually_exclusive=False, # type: bool
4133 closed_environment=False, # type: bool
4134 hyper_parameters=None # type: typing.Optional[dict]
4135 ):
4136 # type: (...) -> dict
4137 """
4138 Create a new model.
4139
4140 Args:
4141 model_id: The model ID
4142 model_name: The model name
4143 concepts: A list of concept IDs that this model will use. A better name here would be
4144 concept_ids
4145 concepts_mutually_exclusive: Whether the concepts are mutually exclusive
4146 closed_environment: Whether the concept environment is closed
4147 hyper_parameters: The hyper parameters
4148
4149 Returns:
4150 A model dictionary.
4151 """
4152
4153 if not model_name:
4154 model_name = model_id
4155
4156 data = None
4157 if concepts:
4158 data = dict_to_protobuf(DataPB,
4159 {'concepts': [{
4160 'id': concept_id
4161 } for concept_id in concepts]})
4162
4163 hyper_parameters_pb = None
4164 if hyper_parameters:
4165 hyper_parameters_pb = dict_to_protobuf(Struct, hyper_parameters)
4166
4167 output_info = OutputInfoPB(
4168 data=data,
4169 output_config=OutputConfigPB(
4170 concepts_mutually_exclusive=concepts_mutually_exclusive,
4171 closed_environment=closed_environment,
4172 hyper_params=hyper_parameters_pb))
4173
4174 model = ModelPB(id=model_id, name=model_name, output_info=output_info)
4175
4176 return self._grpc_request(self._grpc_stub().PostModels, PostModelsRequest(model=model))
4177
4178 def patch_model(self, model, action='merge'): # type: (dict, str) -> dict
4179 """
4180 Args:
4181 model: the model dictionary
4182 action: the patch action
4183
4184 Returns:
4185 the model object
4186 """
4187
4188 if action not in self.patch_actions:
4189 raise UserError("action not supported.")
4190
4191 model_pb = dict_to_protobuf(ModelPB, model)
4192
4193 return self._grpc_request(self._grpc_stub().PatchModels,
4194 PatchModelsRequest(action=action, models=[model_pb]))
4195
4196 def create_model_version(self, model_id): # type: (str) -> dict
4197 """ train for a model """
4198
4199 return self._grpc_request(
4200 self._grpc_stub().PostModelVersions, PostModelVersionsRequest(model_id=_escape(model_id)))
4201
4202 def predict_model(
4203 self, # type: ApiClient
4204 model_id, # type: str
4205 objs, # type: typing.List[Input]
4206 version_id=None, # type: typing.Optional[str]
4207 model_output_info=None # type: typing.Optional[ModelOutputInfo]
4208 ):
4209 # type: (...) -> dict
4210 if not isinstance(objs, list):
4211 raise UserError("objs must be a list")
4212
4213 for i, obj in enumerate(objs):
4214 if not isinstance(obj, (Image, Video)):
4215 raise UserError(
4216 "Object at position %d is not a valid type of content to add. Must be Image or Video" %
4217 i)
4218
4219 inputs_pb = []
4220 for input_ in objs:
4221 data_ = input_.dict().get('data')
4222 if data_:
4223 inputs_pb.append(dict_to_protobuf(InputPB, input_.dict()))
4224
4225 model = None
4226 if model_output_info:
4227 model_output_info_dict = model_output_info.dict().get('output_info')
4228 if model_output_info_dict:
4229 output_info = dict_to_protobuf(
4230 OutputInfoPB, model_output_info_dict)
4231 model = ModelPB(output_info=output_info)
4232
4233 return self._grpc_request(
4234 self._grpc_stub().PostModelOutputs,
4235 PostModelOutputsRequest(
4236 model_id=_escape(model_id), version_id=version_id, inputs=inputs_pb, model=model))
4237
4238 def get_workflows(self, public_only=False): # type: (bool) -> dict
4239 """ get all workflows with pagination
4240
4241 Args:
4242 public_only: whether to get public workflow
4243
4244 Returns:
4245 a list of workflows in JSON format
4246 """
4247
4248 if public_only:
4249 return self._grpc_request(self._grpc_stub().ListPublicWorkflows,
4250 ListPublicWorkflowsRequest())
4251 else:
4252 return self._grpc_request(self._grpc_stub().ListWorkflows, ListWorkflowsRequest())
4253
4254 def get_workflow(self, workflow_id): # type: (str) -> dict
4255 """ get workflow basic info by workflow id
4256
4257 Args:
4258 workflow_id: the unique identifier of the workflow
4259
4260 Returns:
4261 the workflow info in JSON format
4262 """
4263
4264 return self._grpc_request(
4265 self._grpc_stub().GetWorkflow, GetWorkflowRequest(workflow_id=workflow_id))
4266
4267 def predict_workflow(self, workflow_id, objs, output_config=None):
4268 # type: (str, typing.List[Input], typing.Optional[ModelOutputConfig]) -> dict
4269
4270 if not isinstance(objs, list):
4271 raise UserError("objs must be a list")
4272
4273 inputs_pb = []
4274 for i, obj in enumerate(objs):
4275 if not isinstance(obj, (Image, Video)):
4276 raise UserError(
4277 "Object at position %d is not a valid type of content to add. Must be Image or Video" %
4278 i)
4279
4280 inputs_pb.append(dict_to_protobuf(InputPB, obj.dict()))
4281
4282 output_config_pb = None
4283 if output_config:
4284 output_config_ = output_config.dict().get('output_config')
4285 if output_config_:
4286 output_config_pb = dict_to_protobuf(
4287 OutputConfigPB, output_config_)
4288
4289 return self._grpc_request(
4290 self._grpc_stub().PostWorkflowResults,
4291 PostWorkflowResultsRequest(
4292 workflow_id=_escape(workflow_id), inputs=inputs_pb, output_config=output_config_pb))
4293
4294 def send_model_feedback(self, model_id, version_id, obj):
4295 # type: (str, typing.Optional[str], Input) -> dict
4296
4297 input_pb = dict_to_protobuf(InputPB, obj.dict())
4298
4299 return self._grpc_request(
4300 self._grpc_stub().PostModelFeedback,
4301 PostModelFeedbackRequest(
4302 model_id=_escape(model_id), version_id=version_id, input=input_pb))
4303
4304 def send_search_feedback(self, obj): # type: (Input) -> dict
4305 input_dict = obj.dict()
4306
4307 input_pb = dict_to_protobuf(InputPB, input_dict)
4308
4309 return self._grpc_request(
4310 self._grpc_stub().PostSearchFeedback, PostSearchFeedbackRequest(input=input_pb))
4311
4312 def predict_concepts(self, objs, lang=None):
4313 # type: (typing.List[Input], typing.Optional[str]) -> dict
4314
4315 models = self.search_models(name='general-v1.3', model_type='concept')
4316 model = models['models'][0]
4317 model_id = model['id']
4318
4319 model_output_info = ModelOutputInfo(
4320 output_config=ModelOutputConfig(language=lang))
4321 return self.predict_model(model_id, objs, model_output_info=model_output_info)
4322
4323 def predict_colors(self, objs): # type: (typing.List[Input]) -> dict
4324
4325 models = self.search_models(name='color', model_type='color')
4326 model = models['models'][0]
4327 model_id = model['id']
4328
4329 return self.predict_model(model_id, objs)
4330
4331 # type: (typing.List[Input], str) -> dict
4332 def predict_embed(self, objs, model='general*'):
4333
4334 found_models = self.search_models(name=model, model_type='embed')
4335 found_model = found_models['models'][0]
4336 found_model_id = found_model['id']
4337
4338 return self.predict_model(found_model_id, objs)
4339
4340 def run_model_evaluation(self, model_id, version_id): # type: (str, str) -> dict
4341 """ run model evaluation by model id and by version id
4342
4343 Args:
4344 model_id: the unique identifier of the model
4345 version_id: the model version id
4346
4347 Returns:
4348 the model version data with evaluation metrics in JSON format
4349 """
4350
4351 return self._grpc_request(
4352 self._grpc_stub().PostModelVersionMetrics,
4353 PostModelVersionMetricsRequest(model_id=_escape(model_id), version_id=version_id))
4354
4355
4356class pagination(object):
4357
4358 def __init__(self, page=1, per_page=20): # type: (int, int) -> None
4359 self.page = page
4360 self.per_page = per_page
4361
4362 def dict(self): # type: () -> dict
4363 return {'page': self.page, 'per_page': self.per_page}
4364
4365
4366class ApiStatus(object):
4367 """ Clarifai API Status Code """
4368
4369 def __init__(self, item): # type: (dict) -> None
4370 self.code = item['code']
4371 self.description = item['description']
4372
4373 def dict(self): # type: () -> dict
4374 d = {'status': {'code': self.code, 'description': self.description}}
4375
4376 return d
4377
4378
4379class ApiResponse(object):
4380 """ Clarifai API Response """
4381
4382 def __init__(self): # type: () -> None
4383 self.status = None
4384
4385
4386class InputCounts(object):
4387 """ input counts for upload status """
4388
4389 def __init__(self, item): # type: (dict) -> None
4390 if not item.get('counts'):
4391 raise UserError(
4392 'unable to initialize. need a dict with key=counts')
4393
4394 counts = item['counts']
4395
4396 # TODO(Rok) MEDIUM: Add the "processing" field here and in dict().
4397 self.processed = counts['processed']
4398 self.to_process = counts['to_process']
4399 self.errors = counts['errors']
4400
4401 def dict(self): # type: () -> dict
4402 d = {
4403 'counts': {
4404 'processed': self.processed,
4405 'to_process': self.to_process,
4406 'errors': self.errors
4407 }
4408 }
4409 return d
4410
4411
4412class ModelOutputInfo(object):
4413
4414 def __init__(self, concepts=None, output_config=None):
4415 # type: (typing.Optional[typing.List[Concept]], typing.Optional[ModelOutputConfig]) -> None
4416 self.concepts = concepts # type: typing.Optional[typing.List[Concept]]
4417 # type: typing.Optional[ModelOutputConfig]
4418 self.output_config = output_config
4419
4420 def dict(self): # type: () -> dict
4421 output_info = {}
4422 if self.output_config:
4423 output_info.update(self.output_config.dict())
4424 if self.concepts:
4425 output_info.update(
4426 {'data': {'concepts': [concept.dict() for concept in self.concepts]}})
4427
4428 if output_info:
4429 return {'output_info': output_info}
4430 else:
4431 return {}
4432
4433
4434class ModelOutputConfig(object):
4435
4436 def __init__(
4437 self, # type: ModelOutputConfig
4438 mutually_exclusive=None, # type: typing.Optional[bool]
4439 closed_environment=None, # type: typing.Optional[bool]
4440 language=None, # type: typing.Optional[str]
4441 min_value=None, # type: typing.Optional[float]
4442 max_concepts=None, # type: typing.Optional[int]
4443 select_concepts=None, # type: typing.Optional[typing.List[Concept]]
4444 sample_ms=None # type: typing.Optional[int]
4445 ):
4446 # type: (...) -> None
4447 self.concepts_mutually_exclusive = mutually_exclusive
4448 self.closed_environment = closed_environment
4449 self.language = language
4450 self.min_value = min_value
4451 self.max_concepts = max_concepts
4452 self.select_concepts = select_concepts
4453 self.sample_ms = sample_ms
4454
4455 def dict(self): # type: () -> dict
4456 output_config = {}
4457
4458 if self.concepts_mutually_exclusive is not None:
4459 output_config['concepts_mutually_exclusive'] = self.concepts_mutually_exclusive
4460
4461 if self.closed_environment is not None:
4462 output_config['closed_environment'] = self.closed_environment
4463
4464 if self.language is not None:
4465 output_config['language'] = self.language
4466
4467 if self.min_value is not None:
4468 output_config['min_value'] = self.min_value
4469
4470 if self.max_concepts is not None:
4471 output_config['max_concepts'] = self.max_concepts
4472
4473 if self.select_concepts is not None:
4474 output_config['select_concepts'] = [c.dict()
4475 for c in self.select_concepts]
4476
4477 if self.sample_ms is not None:
4478 output_config['sample_ms'] = self.sample_ms
4479
4480 if output_config:
4481 return {'output_config': output_config}
4482 else:
4483 return {}
4484
4485
4486class BoundingBox(object):
4487
4488 def __init__(self, top_row, left_col, bottom_row, right_col):
4489 # type: (float, float, float, float) -> None
4490 self.top_row = top_row
4491 self.left_col = left_col
4492 self.bottom_row = bottom_row
4493 self.right_col = right_col
4494
4495 def dict(self): # type: () -> dict
4496 data = {
4497 'bounding_box': {
4498 'top_row': self.top_row,
4499 'left_col': self.left_col,
4500 'bottom_row': self.bottom_row,
4501 'right_col': self.right_col
4502 }
4503 }
4504
4505 return data
4506
4507
4508class RegionInfo(object):
4509
4510 def __init__(self, bbox=None, feedback_type=None):
4511 # type: (typing.Optional[BoundingBox], typing.Optional[FeedbackType]) -> None
4512 self.bbox = bbox
4513 self.feedback_type = feedback_type
4514
4515 def dict(self): # type: () -> dict
4516
4517 data = {"region_info": {}}
4518
4519 if self.bbox:
4520 data['region_info'].update(self.bbox.dict())
4521
4522 if self.feedback_type:
4523 if isinstance(self.feedback_type, FeedbackType):
4524 data['region_info']['feedback'] = self.feedback_type.name
4525 else:
4526 data['region_info']['feedback'] = self.feedback_type
4527
4528 return data
4529
4530
4531class Region(object):
4532
4533 def __init__(
4534 self, # type: Region
4535 region_info=None, # type: typing.Optional[RegionInfo]
4536 concepts=None, # type: typing.Optional[typing.List[Concept]]
4537 face=None, # type: typing.Optional[Face]
4538 region_id=None # type: typing.Optional[str]
4539 ):
4540 # type: (...) -> None
4541
4542 self.region_info = region_info
4543 self.concepts = concepts
4544 self.face = face
4545 self.region_id = region_id
4546
4547 def dict(self): # type: () -> dict
4548 data = {}
4549 if self.concepts:
4550 data['concepts'] = [c.dict() for c in self.concepts]
4551 if self.face:
4552 data.update(self.face.dict())
4553
4554 region = {}
4555 if self.region_info:
4556 region.update(self.region_info.dict())
4557 if self.region_id:
4558 region['id'] = self.region_id
4559 if data:
4560 region['data'] = data
4561 return region
4562
4563
4564class Face(object):
4565
4566 def __init__(
4567 self, # type: Face
4568 identity=None, # type: typing.Optional[FaceIdentity]
4569 age_appearance=None, # type: typing.Optional[FaceAgeAppearance]
4570 gender_appearance=None, # type: typing.Optional[FaceGenderAppearance]
4571 # type: typing.Optional[FaceMulticulturalAppearance]
4572 multicultural_appearance=None
4573 ):
4574 # type: (...) -> None
4575
4576 self.identity = identity
4577 self.age_appearance = age_appearance
4578 self.gender_appearance = gender_appearance
4579 self.multicultural_appearance = multicultural_appearance
4580
4581 def dict(self): # type: () -> dict
4582
4583 data = {'face': {}}
4584
4585 if self.identity:
4586 data['face'].update(self.identity.dict())
4587
4588 if self.age_appearance:
4589 data['face'].update(self.age_appearance.dict())
4590
4591 if self.gender_appearance:
4592 data['face'].update(self.gender_appearance.dict())
4593
4594 if self.multicultural_appearance:
4595 data['face'].update(self.multicultural_appearance.dict())
4596
4597 return data
4598
4599
4600class FaceIdentity(object):
4601
4602 def __init__(self, concepts): # type: (typing.List[Concept]) -> None
4603 self.concepts = concepts
4604
4605 def dict(self): # type: () -> dict
4606 data = {'identity': {'concepts': [c.dict() for c in self.concepts]}}
4607 return data
4608
4609
4610class FaceAgeAppearance(object):
4611
4612 def __init__(self, concepts): # type: (typing.List[Concept]) -> None
4613 self.concepts = concepts
4614
4615 def dict(self): # type: () -> dict
4616 data = {'age_appearance': {'concepts': [
4617 c.dict() for c in self.concepts]}}
4618 return data
4619
4620
4621class FaceGenderAppearance(object):
4622
4623 def __init__(self, concepts): # type: (typing.List[Concept]) -> None
4624 self.concepts = concepts
4625
4626 def dict(self): # type: () -> dict
4627 data = {'gender_appearance': {
4628 'concepts': [c.dict() for c in self.concepts]}}
4629 return data
4630
4631
4632class FaceMulticulturalAppearance(object):
4633
4634 def __init__(self, concepts): # type: (typing.List[Concept]) -> None
4635 self.concepts = concepts
4636
4637 def dict(self): # type: () -> dict
4638 data = {'multicultural_appearance': {
4639 'concepts': [c.dict() for c in self.concepts]}}
4640 return data
4641