megadetector 5.0.29__py3-none-any.whl → 10.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of megadetector might be problematic. Click here for more details.

Files changed (95) hide show
  1. megadetector/classification/efficientnet/model.py +8 -8
  2. megadetector/classification/efficientnet/utils.py +6 -5
  3. megadetector/classification/prepare_classification_script_mc.py +3 -3
  4. megadetector/data_management/annotations/annotation_constants.py +0 -1
  5. megadetector/data_management/camtrap_dp_to_coco.py +34 -1
  6. megadetector/data_management/cct_json_utils.py +2 -2
  7. megadetector/data_management/coco_to_yolo.py +22 -5
  8. megadetector/data_management/databases/add_width_and_height_to_db.py +85 -12
  9. megadetector/data_management/databases/combine_coco_camera_traps_files.py +2 -2
  10. megadetector/data_management/databases/integrity_check_json_db.py +29 -15
  11. megadetector/data_management/generate_crops_from_cct.py +50 -1
  12. megadetector/data_management/labelme_to_coco.py +4 -2
  13. megadetector/data_management/labelme_to_yolo.py +82 -2
  14. megadetector/data_management/lila/generate_lila_per_image_labels.py +276 -18
  15. megadetector/data_management/lila/get_lila_annotation_counts.py +5 -3
  16. megadetector/data_management/lila/lila_common.py +3 -0
  17. megadetector/data_management/lila/test_lila_metadata_urls.py +15 -5
  18. megadetector/data_management/mewc_to_md.py +5 -0
  19. megadetector/data_management/ocr_tools.py +4 -3
  20. megadetector/data_management/read_exif.py +20 -5
  21. megadetector/data_management/remap_coco_categories.py +66 -4
  22. megadetector/data_management/remove_exif.py +50 -1
  23. megadetector/data_management/rename_images.py +3 -3
  24. megadetector/data_management/resize_coco_dataset.py +563 -95
  25. megadetector/data_management/yolo_output_to_md_output.py +131 -2
  26. megadetector/data_management/yolo_to_coco.py +140 -5
  27. megadetector/detection/change_detection.py +4 -3
  28. megadetector/detection/pytorch_detector.py +60 -22
  29. megadetector/detection/run_detector.py +225 -25
  30. megadetector/detection/run_detector_batch.py +42 -16
  31. megadetector/detection/run_inference_with_yolov5_val.py +12 -2
  32. megadetector/detection/run_tiled_inference.py +1 -0
  33. megadetector/detection/video_utils.py +53 -24
  34. megadetector/postprocessing/add_max_conf.py +4 -0
  35. megadetector/postprocessing/categorize_detections_by_size.py +1 -1
  36. megadetector/postprocessing/classification_postprocessing.py +55 -20
  37. megadetector/postprocessing/combine_batch_outputs.py +3 -2
  38. megadetector/postprocessing/compare_batch_results.py +64 -10
  39. megadetector/postprocessing/convert_output_format.py +12 -8
  40. megadetector/postprocessing/create_crop_folder.py +137 -10
  41. megadetector/postprocessing/load_api_results.py +26 -8
  42. megadetector/postprocessing/md_to_coco.py +4 -4
  43. megadetector/postprocessing/md_to_labelme.py +18 -7
  44. megadetector/postprocessing/merge_detections.py +5 -0
  45. megadetector/postprocessing/postprocess_batch_results.py +6 -3
  46. megadetector/postprocessing/remap_detection_categories.py +55 -2
  47. megadetector/postprocessing/render_detection_confusion_matrix.py +9 -6
  48. megadetector/postprocessing/repeat_detection_elimination/repeat_detections_core.py +2 -2
  49. megadetector/taxonomy_mapping/map_new_lila_datasets.py +3 -4
  50. megadetector/taxonomy_mapping/prepare_lila_taxonomy_release.py +40 -19
  51. megadetector/taxonomy_mapping/preview_lila_taxonomy.py +1 -1
  52. megadetector/taxonomy_mapping/species_lookup.py +123 -41
  53. megadetector/utils/ct_utils.py +133 -113
  54. megadetector/utils/md_tests.py +93 -13
  55. megadetector/utils/path_utils.py +137 -107
  56. megadetector/utils/split_locations_into_train_val.py +2 -2
  57. megadetector/utils/string_utils.py +7 -7
  58. megadetector/utils/url_utils.py +81 -58
  59. megadetector/utils/wi_utils.py +46 -17
  60. megadetector/visualization/plot_utils.py +13 -9
  61. megadetector/visualization/render_images_with_thumbnails.py +2 -1
  62. megadetector/visualization/visualization_utils.py +94 -46
  63. megadetector/visualization/visualize_db.py +36 -9
  64. megadetector/visualization/visualize_detector_output.py +4 -4
  65. {megadetector-5.0.29.dist-info → megadetector-10.0.1.dist-info}/METADATA +135 -135
  66. megadetector-10.0.1.dist-info/RECORD +139 -0
  67. {megadetector-5.0.29.dist-info → megadetector-10.0.1.dist-info}/licenses/LICENSE +0 -0
  68. {megadetector-5.0.29.dist-info → megadetector-10.0.1.dist-info}/top_level.txt +0 -0
  69. megadetector/api/batch_processing/api_core/__init__.py +0 -0
  70. megadetector/api/batch_processing/api_core/batch_service/__init__.py +0 -0
  71. megadetector/api/batch_processing/api_core/batch_service/score.py +0 -438
  72. megadetector/api/batch_processing/api_core/server.py +0 -294
  73. megadetector/api/batch_processing/api_core/server_api_config.py +0 -97
  74. megadetector/api/batch_processing/api_core/server_app_config.py +0 -55
  75. megadetector/api/batch_processing/api_core/server_batch_job_manager.py +0 -220
  76. megadetector/api/batch_processing/api_core/server_job_status_table.py +0 -149
  77. megadetector/api/batch_processing/api_core/server_orchestration.py +0 -360
  78. megadetector/api/batch_processing/api_core/server_utils.py +0 -88
  79. megadetector/api/batch_processing/api_core_support/__init__.py +0 -0
  80. megadetector/api/batch_processing/api_core_support/aggregate_results_manually.py +0 -46
  81. megadetector/api/batch_processing/api_support/__init__.py +0 -0
  82. megadetector/api/batch_processing/api_support/summarize_daily_activity.py +0 -152
  83. megadetector/api/batch_processing/data_preparation/__init__.py +0 -0
  84. megadetector/api/synchronous/__init__.py +0 -0
  85. megadetector/api/synchronous/api_core/animal_detection_api/__init__.py +0 -0
  86. megadetector/api/synchronous/api_core/animal_detection_api/api_backend.py +0 -151
  87. megadetector/api/synchronous/api_core/animal_detection_api/api_frontend.py +0 -263
  88. megadetector/api/synchronous/api_core/animal_detection_api/config.py +0 -35
  89. megadetector/api/synchronous/api_core/tests/__init__.py +0 -0
  90. megadetector/api/synchronous/api_core/tests/load_test.py +0 -109
  91. megadetector/utils/azure_utils.py +0 -178
  92. megadetector/utils/sas_blob_utils.py +0 -513
  93. megadetector-5.0.29.dist-info/RECORD +0 -163
  94. /megadetector/{api/batch_processing/__init__.py → __init__.py} +0 -0
  95. {megadetector-5.0.29.dist-info → megadetector-10.0.1.dist-info}/WHEEL +0 -0
@@ -1,149 +0,0 @@
1
- # Copyright (c) Microsoft Corporation. All rights reserved.
2
- # Licensed under the MIT License.
3
-
4
- """
5
- A class to manage updating the status of an API request / Azure Batch Job using
6
- the Cosmos DB table "batch_api_jobs".
7
- """
8
-
9
- import logging
10
- import os
11
- import unittest
12
- import uuid
13
- from typing import Union, Optional
14
-
15
- from azure.cosmos.cosmos_client import CosmosClient
16
- from azure.cosmos.exceptions import CosmosResourceNotFoundError
17
-
18
- from server_api_config import API_INSTANCE_NAME, COSMOS_ENDPOINT, COSMOS_WRITE_KEY
19
- from server_utils import get_utc_time
20
-
21
-
22
- log = logging.getLogger(os.environ['FLASK_APP'])
23
-
24
-
25
- class JobStatusTable:
26
- """
27
- A wrapper around the Cosmos DB client. Each item in the table "batch_api_jobs" represents
28
- a request/Batch Job, and should have the following fields:
29
- - id: this is the job_id
30
- - api_instance
31
- - status
32
- - last_updated
33
- - call_params: the dict representing the body of the POST request from the user
34
- The 'status' field is a dict with the following fields:
35
- - request_status
36
- - message
37
- - num_tasks (present after Batch Job created)
38
- - num_images (present after Batch Job created)
39
- """
40
- # a job moves from created to running/problem after the Batch Job has been submitted
41
- allowed_statuses = ['created', 'running', 'failed', 'problem', 'completed', 'canceled']
42
-
43
- def __init__(self, api_instance=None):
44
- self.api_instance = api_instance if api_instance is not None else API_INSTANCE_NAME
45
- cosmos_client = CosmosClient(COSMOS_ENDPOINT, credential=COSMOS_WRITE_KEY)
46
- db_client = cosmos_client.get_database_client('camera-trap')
47
- self.db_jobs_client = db_client.get_container_client('batch_api_jobs')
48
-
49
- def create_job_status(self, job_id: str, status: Union[dict, str], call_params: dict) -> dict:
50
- assert 'request_status' in status and 'message' in status
51
- assert status['request_status'] in JobStatusTable.allowed_statuses
52
-
53
- # job_id should be unique across all instances, and is also the partition key
54
- cur_time = get_utc_time()
55
- item = {
56
- 'id': job_id,
57
- 'api_instance': self.api_instance,
58
- 'status': status,
59
- 'job_submission_time': cur_time,
60
- 'last_updated': cur_time,
61
- 'call_params': call_params
62
- }
63
- created_item = self.db_jobs_client.create_item(item)
64
- return created_item
65
-
66
- def update_job_status(self, job_id: str, status: Union[dict, str]) -> dict:
67
- assert 'request_status' in status and 'message' in status
68
- assert status['request_status'] in JobStatusTable.allowed_statuses
69
-
70
- item_old = self.read_job_status(job_id)
71
- if item_old is None:
72
- raise ValueError
73
-
74
- # need to retain other fields in 'status' to be able to restart monitoring thread
75
- if 'status' in item_old and isinstance(item_old['status'], dict):
76
- # retain existing fields; update as needed
77
- for k, v in item_old['status'].items():
78
- if k not in status:
79
- status[k] = v
80
- item = {
81
- 'id': job_id,
82
- 'api_instance': self.api_instance,
83
- 'status': status,
84
- 'job_submission_time': item_old['job_submission_time'],
85
- 'last_updated': get_utc_time(),
86
- 'call_params': item_old['call_params']
87
- }
88
- replaced_item = self.db_jobs_client.replace_item(job_id, item)
89
- return replaced_item
90
-
91
- def read_job_status(self, job_id) -> Optional[dict]:
92
- """
93
- Read the status of the job from the Cosmos DB table of job status.
94
- Note that it does not check the actual status of the job on Batch, and just returns what
95
- the monitoring thread wrote to the database.
96
- job_id is also the partition key
97
- """
98
- try:
99
- read_item = self.db_jobs_client.read_item(job_id, partition_key=job_id)
100
- assert read_item['api_instance'] == self.api_instance, 'Job does not belong to this API instance'
101
- except CosmosResourceNotFoundError:
102
- return None # job_id not a key
103
- except Exception as e:
104
- logging.error(f'server_job_status_table, read_job_status, exception: {e}')
105
- raise
106
- else:
107
- item = {k: v for k, v in read_item.items() if not k.startswith('_')}
108
- return item
109
-
110
-
111
- class TestJobStatusTable(unittest.TestCase):
112
- api_instance = 'api_test'
113
-
114
- def test_insert(self):
115
- table = JobStatusTable(TestJobStatusTable.api_instance)
116
- status = {
117
- 'request_status': 'running',
118
- 'message': 'this is a test'
119
- }
120
- job_id = uuid.uuid4().hex
121
- item = table.create_job_status(job_id, status, {'container_sas': 'random_string'})
122
- self.assertTrue(job_id == item['id'], 'Expect job_id to be the id of the item')
123
- self.assertTrue(item['status']['request_status'] == 'running', 'Expect fields to be inserted correctly')
124
-
125
- def test_update_and_read(self):
126
- table = JobStatusTable(TestJobStatusTable.api_instance)
127
- status = {
128
- 'request_status': 'running',
129
- 'message': 'this is a test'
130
- }
131
- job_id = uuid.uuid4().hex
132
- res = table.create_job_status(job_id, status, {'container_sas': 'random_string'})
133
-
134
- status = {
135
- 'request_status': 'completed',
136
- 'message': 'this is a test again'
137
- }
138
- res = table.update_job_status(job_id, status)
139
- item_read = table.read_job_status(job_id)
140
- self.assertTrue(item_read['status']['request_status'] == 'completed', 'Expect field to have updated')
141
-
142
- def test_read_invalid_id(self):
143
- table = JobStatusTable(TestJobStatusTable.api_instance)
144
- job_id = uuid.uuid4().hex # should not be in the database
145
- item_read = table.read_job_status(job_id)
146
- self.assertIsNone(item_read)
147
-
148
- if __name__ == '__main__':
149
- unittest.main()
@@ -1,360 +0,0 @@
1
- # Copyright (c) Microsoft Corporation. All rights reserved.
2
- # Licensed under the MIT License.
3
-
4
- """
5
- Functions to submit images to the Azure Batch node pool for processing, monitor
6
- the Job and fetch results when completed.
7
- """
8
-
9
- import io
10
- import json
11
- import threading
12
- import time
13
- import logging
14
- import os
15
- import urllib.parse
16
- from datetime import timedelta
17
- from random import shuffle
18
-
19
- import sas_blob_utils
20
- import requests
21
- from azure.storage.blob import ContainerClient, BlobSasPermissions, generate_blob_sas
22
- from tqdm import tqdm
23
-
24
- from server_utils import *
25
- import server_api_config as api_config
26
- from server_batch_job_manager import BatchJobManager
27
- from server_job_status_table import JobStatusTable
28
-
29
-
30
- # Gunicorn logger handler will get attached if needed in server.py
31
- log = logging.getLogger(os.environ['FLASK_APP'])
32
-
33
-
34
- def create_batch_job(job_id: str, body: dict):
35
- """
36
- This is the target to be run in a thread to submit a batch processing job and monitor progress
37
- """
38
- job_status_table = JobStatusTable()
39
- try:
40
- log.info(f'server_job, create_batch_job, job_id {job_id}, {body}')
41
-
42
- input_container_sas = body.get('input_container_sas', None)
43
-
44
- use_url = body.get('use_url', False)
45
-
46
- images_requested_json_sas = body.get('images_requested_json_sas', None)
47
-
48
- image_path_prefix = body.get('image_path_prefix', None)
49
-
50
- first_n = body.get('first_n', None)
51
- first_n = int(first_n) if first_n else None
52
-
53
- sample_n = body.get('sample_n', None)
54
- sample_n = int(sample_n) if sample_n else None
55
-
56
- model_version = body.get('model_version', '')
57
- if model_version == '':
58
- model_version = api_config.DEFAULT_MD_VERSION
59
-
60
- # request_name and request_submission_timestamp are for appending to
61
- # output file names
62
- job_name = body.get('request_name', '') # in earlier versions we used "request" to mean a "job"
63
- job_submission_timestamp = get_utc_time()
64
-
65
- # image_paths can be a list of strings (Azure blob names or public URLs)
66
- # or a list of length-2 lists where each is a [image_id, metadata] pair
67
-
68
- # Case 1: listing all images in the container
69
- # - not possible to have attached metadata if listing images in a blob
70
- if images_requested_json_sas is None:
71
- log.info('server_job, create_batch_job, listing all images to process.')
72
-
73
- # list all images to process
74
- image_paths = sas_blob_utils.list_blobs_in_container(
75
- container_uri=input_container_sas,
76
- blob_prefix=image_path_prefix, # check will be case-sensitive
77
- blob_suffix=api_config.IMAGE_SUFFIXES_ACCEPTED, # check will be case-insensitive
78
- limit=api_config.MAX_NUMBER_IMAGES_ACCEPTED_PER_JOB + 1
79
- # + 1 so if the number of images listed > MAX_NUMBER_IMAGES_ACCEPTED_PER_JOB
80
- # we will know and not proceed
81
- )
82
-
83
- # Case 2: user supplied a list of images to process; can include metadata
84
- else:
85
- log.info('server_job, create_batch_job, using provided list of images.')
86
-
87
- response = requests.get(images_requested_json_sas) # could be a file hosted anywhere
88
- image_paths = response.json()
89
-
90
- log.info('server_job, create_batch_job, length of image_paths provided by the user: {}'.format(
91
- len(image_paths)))
92
- if len(image_paths) == 0:
93
- job_status = get_job_status(
94
- 'completed', '0 images found in provided list of images.')
95
- job_status_table.update_job_status(job_id, job_status)
96
- return
97
-
98
- error, metadata_available = validate_provided_image_paths(image_paths)
99
- if error is not None:
100
- msg = 'image paths provided in the json are not valid: {}'.format(error)
101
- raise ValueError(msg)
102
-
103
- # filter down to those conforming to the provided prefix and accepted suffixes (image file types)
104
- valid_image_paths = []
105
- for p in image_paths:
106
- locator = p[0] if metadata_available else p
107
-
108
- # prefix is case-sensitive; suffix is not
109
- if image_path_prefix is not None and not locator.startswith(image_path_prefix):
110
- continue
111
-
112
- # Although urlparse(p).path preserves the extension on local paths, it will not work for
113
- # blob file names that contains "#", which will be treated as indication of a query.
114
- # If the URL is generated via Azure Blob Storage, the "#" char will be properly encoded
115
- path = urllib.parse.urlparse(locator).path if use_url else locator
116
-
117
- if path.lower().endswith(api_config.IMAGE_SUFFIXES_ACCEPTED):
118
- valid_image_paths.append(p)
119
- image_paths = valid_image_paths
120
- log.info(('server_job, create_batch_job, length of image_paths provided by user, '
121
- f'after filtering to jpg: {len(image_paths)}'))
122
-
123
- # apply the first_n and sample_n filters
124
- if first_n:
125
- assert first_n > 0, 'parameter first_n is 0.'
126
- # OK if first_n > total number of images
127
- image_paths = image_paths[:first_n]
128
-
129
- if sample_n:
130
- assert sample_n > 0, 'parameter sample_n is 0.'
131
- if sample_n > len(image_paths):
132
- msg = ('parameter sample_n specifies more images than '
133
- 'available (after filtering by other provided params).')
134
- raise ValueError(msg)
135
-
136
- # sample by shuffling image paths and take the first sample_n images
137
- log.info('First path before shuffling:', image_paths[0])
138
- shuffle(image_paths)
139
- log.info('First path after shuffling:', image_paths[0])
140
- image_paths = image_paths[:sample_n]
141
-
142
- num_images = len(image_paths)
143
- log.info(f'server_job, create_batch_job, num_images after applying all filters: {num_images}')
144
-
145
- if num_images < 1:
146
- job_status = get_job_status('completed', (
147
- 'Zero images found in container or in provided list of images '
148
- 'after filtering with the provided parameters.'))
149
- job_status_table.update_job_status(job_id, job_status)
150
- return
151
- if num_images > api_config.MAX_NUMBER_IMAGES_ACCEPTED_PER_JOB:
152
- job_status = get_job_status(
153
- 'failed',
154
- (f'The number of images ({num_images}) requested for processing exceeds the maximum '
155
- f'accepted {api_config.MAX_NUMBER_IMAGES_ACCEPTED_PER_JOB} in one call'))
156
- job_status_table.update_job_status(job_id, job_status)
157
- return
158
-
159
- # upload the image list to the container, which is also mounted on all nodes
160
- # all sharding and scoring use the uploaded list
161
- images_list_str_as_bytes = bytes(json.dumps(image_paths, ensure_ascii=False), encoding='utf-8')
162
-
163
- container_url = sas_blob_utils.build_azure_storage_uri(account=api_config.STORAGE_ACCOUNT_NAME,
164
- container=api_config.STORAGE_CONTAINER_API)
165
- with ContainerClient.from_container_url(container_url,
166
- credential=api_config.STORAGE_ACCOUNT_KEY) as api_container_client:
167
- _ = api_container_client.upload_blob(
168
- name=f'api_{api_config.API_INSTANCE_NAME}/job_{job_id}/{job_id}_images.json',
169
- data=images_list_str_as_bytes)
170
-
171
- job_status = get_job_status('created', f'{num_images} images listed; submitting the job...')
172
- job_status_table.update_job_status(job_id, job_status)
173
-
174
- except Exception as e:
175
- job_status = get_job_status('failed', f'Error occurred while preparing the Batch job: {e}')
176
- job_status_table.update_job_status(job_id, job_status)
177
- log.error(f'server_job, create_batch_job, Error occurred while preparing the Batch job: {e}')
178
- return # do not start monitoring
179
-
180
- try:
181
- batch_job_manager = BatchJobManager()
182
-
183
- model_rel_path = api_config.MD_VERSIONS_TO_REL_PATH[model_version]
184
- batch_job_manager.create_job(job_id,
185
- model_rel_path,
186
- input_container_sas,
187
- use_url)
188
-
189
- num_tasks, task_ids_failed_to_submit = batch_job_manager.submit_tasks(job_id, num_images)
190
-
191
- # now request_status moves from created to running
192
- job_status = get_job_status('running',
193
- (f'Submitted {num_images} images to cluster in {num_tasks} shards. '
194
- f'Number of shards failed to be submitted: {len(task_ids_failed_to_submit)}'))
195
-
196
- # an extra field to allow the monitoring thread to restart after an API restart: total number of tasks
197
- job_status['num_tasks'] = num_tasks
198
- # also record the number of images to process for reporting
199
- job_status['num_images'] = num_images
200
-
201
- job_status_table.update_job_status(job_id, job_status)
202
- except Exception as e:
203
- job_status = get_job_status('problem', f'Please contact us. Error occurred while submitting the Batch job: {e}')
204
- job_status_table.update_job_status(job_id, job_status)
205
- log.error(f'server_job, create_batch_job, Error occurred while submitting the Batch job: {e}')
206
- return
207
-
208
- # start the monitor thread with the same name
209
- try:
210
- thread = threading.Thread(
211
- target=monitor_batch_job,
212
- name=f'job_{job_id}',
213
- kwargs={
214
- 'job_id': job_id,
215
- 'num_tasks': num_tasks,
216
- 'model_version': model_version,
217
- 'job_name': job_name,
218
- 'job_submission_timestamp': job_submission_timestamp
219
- }
220
- )
221
- thread.start()
222
- except Exception as e:
223
- job_status = get_job_status('problem', f'Error occurred while starting the monitoring thread: {e}')
224
- job_status_table.update_job_status(job_id, job_status)
225
- log.error(f'server_job, create_batch_job, Error occurred while starting the monitoring thread: {e}')
226
- return
227
-
228
-
229
- def monitor_batch_job(job_id: str,
230
- num_tasks: int,
231
- model_version: str,
232
- job_name: str,
233
- job_submission_timestamp: str):
234
-
235
- job_status_table = JobStatusTable()
236
- batch_job_manager = BatchJobManager()
237
-
238
- try:
239
- num_checks = 0
240
-
241
- while True:
242
- time.sleep(api_config.MONITOR_PERIOD_MINUTES * 60)
243
- num_checks += 1
244
-
245
- # both succeeded and failed tasks are marked "completed" on Batch
246
- num_tasks_succeeded, num_tasks_failed = batch_job_manager.get_num_completed_tasks(job_id)
247
- job_status = get_job_status('running',
248
- (f'Check number {num_checks}, '
249
- f'{num_tasks_succeeded} out of {num_tasks} shards have completed '
250
- f'successfully, {num_tasks_failed} shards have failed.'))
251
- job_status_table.update_job_status(job_id, job_status)
252
- log.info(f'job_id {job_id}. '
253
- f'Check number {num_checks}, {num_tasks_succeeded} out of {num_tasks} shards completed, '
254
- f'{num_tasks_failed} shards failed.')
255
-
256
- if (num_tasks_succeeded + num_tasks_failed) >= num_tasks:
257
- break
258
-
259
- if num_checks > api_config.MAX_MONITOR_CYCLES:
260
- job_status = get_job_status('problem',
261
- (
262
- f'Job unfinished after {num_checks} x {api_config.MONITOR_PERIOD_MINUTES} minutes, '
263
- f'please contact us to retrieve the results. Number of succeeded shards: {num_tasks_succeeded}')
264
- )
265
- job_status_table.update_job_status(job_id, job_status)
266
- log.warning(f'server_job, create_batch_job, MAX_MONITOR_CYCLES reached, ending thread')
267
- break # still aggregate the Tasks' outputs
268
-
269
- except Exception as e:
270
- job_status = get_job_status('problem', f'Error occurred while monitoring the Batch job: {e}')
271
- job_status_table.update_job_status(job_id, job_status)
272
- log.error(f'server_job, create_batch_job, Error occurred while monitoring the Batch job: {e}')
273
- return
274
-
275
- try:
276
- output_sas_url = aggregate_results(job_id, model_version, job_name, job_submission_timestamp)
277
- # preserving format from before, but SAS URL to 'failed_images' and 'images' are no longer provided
278
- # failures should be contained in the output entries, indicated by an 'error' field
279
- msg = {
280
- 'num_failed_shards': num_tasks_failed,
281
- 'output_file_urls': {
282
- 'detections': output_sas_url
283
- }
284
- }
285
- job_status = get_job_status('completed', msg)
286
- job_status_table.update_job_status(job_id, job_status)
287
-
288
- except Exception as e:
289
- job_status = get_job_status('problem',
290
- f'Please contact us to retrieve the results. Error occurred while aggregating results: {e}')
291
- job_status_table.update_job_status(job_id, job_status)
292
- log.error(f'server_job, create_batch_job, Error occurred while aggregating results: {e}')
293
- return
294
-
295
-
296
- def aggregate_results(job_id: str,
297
- model_version: str,
298
- job_name: str,
299
- job_submission_timestamp: str) -> str:
300
- log.info(f'server_job, aggregate_results starting, job_id: {job_id}')
301
-
302
- container_url = sas_blob_utils.build_azure_storage_uri(account=api_config.STORAGE_ACCOUNT_NAME,
303
- container=api_config.STORAGE_CONTAINER_API)
304
- # when people download this, the timestamp will have : replaced by _
305
- output_file_path = f'api_{api_config.API_INSTANCE_NAME}/job_{job_id}/{job_id}_detections_{job_name}_{job_submission_timestamp}.json'
306
-
307
- with ContainerClient.from_container_url(container_url,
308
- credential=api_config.STORAGE_ACCOUNT_KEY) as container_client:
309
- # check if the result blob has already been written (could be another instance of the API / worker thread)
310
- # and if so, skip aggregating and uploading the results, and just generate the SAS URL, which
311
- # could be needed still if the previous request_status was `problem`.
312
- blob_client = container_client.get_blob_client(output_file_path)
313
- if blob_client.exists():
314
- log.warning(f'The output file already exists, likely because another monitoring thread already wrote it.')
315
- else:
316
- task_outputs_dir = f'api_{api_config.API_INSTANCE_NAME}/job_{job_id}/task_outputs/'
317
- generator = container_client.list_blobs(name_starts_with=task_outputs_dir)
318
-
319
- blobs = [i for i in generator if i.name.endswith('.json')]
320
-
321
- all_results = []
322
- for blob_props in tqdm(blobs):
323
- with container_client.get_blob_client(blob_props) as blob_client:
324
- stream = io.BytesIO()
325
- blob_client.download_blob().readinto(stream)
326
- stream.seek(0)
327
- task_results = json.load(stream)
328
- all_results.extend(task_results)
329
-
330
- api_output = {
331
- 'info': {
332
- 'detector': f'megadetector_v{model_version}',
333
- 'detection_completion_time': get_utc_time(),
334
- 'format_version': api_config.OUTPUT_FORMAT_VERSION
335
- },
336
- 'detection_categories': api_config.DETECTOR_LABEL_MAP,
337
- 'images': all_results
338
- }
339
-
340
- # upload the output JSON to the Job folder
341
- api_output_as_bytes = bytes(json.dumps(api_output, ensure_ascii=False, indent=1), encoding='utf-8')
342
- _ = container_client.upload_blob(name=output_file_path, data=api_output_as_bytes)
343
-
344
- output_sas = generate_blob_sas(
345
- account_name=api_config.STORAGE_ACCOUNT_NAME,
346
- container_name=api_config.STORAGE_CONTAINER_API,
347
- blob_name=output_file_path,
348
- account_key=api_config.STORAGE_ACCOUNT_KEY,
349
- permission=BlobSasPermissions(read=True, write=False),
350
- expiry=datetime.utcnow() + timedelta(days=api_config.OUTPUT_SAS_EXPIRATION_DAYS)
351
- )
352
- output_sas_url = sas_blob_utils.build_azure_storage_uri(
353
- account=api_config.STORAGE_ACCOUNT_NAME,
354
- container=api_config.STORAGE_CONTAINER_API,
355
- blob=output_file_path,
356
- sas_token=output_sas
357
- )
358
- log.info(f'server_job, aggregate_results done, job_id: {job_id}')
359
- log.info(f'output_sas_url: {output_sas_url}')
360
- return output_sas_url
@@ -1,88 +0,0 @@
1
- # Copyright (c) Microsoft Corporation. All rights reserved.
2
- # Licensed under the MIT License.
3
-
4
- """
5
- Helper functions for the batch processing API.
6
- """
7
-
8
- import logging
9
- import os
10
- from datetime import datetime
11
- from typing import Tuple, Any, Sequence, Optional
12
-
13
- import sas_blob_utils
14
-
15
-
16
- log = logging.getLogger(os.environ['FLASK_APP'])
17
-
18
-
19
- #%% helper classes and functions
20
-
21
- def make_error(error_code: int, error_message: str) -> Tuple[dict, int]:
22
- log.error(f'Error {error_code} - {error_message}')
23
- return {'error': error_message}, error_code
24
-
25
-
26
- def check_data_container_sas(input_container_sas: str) -> Optional[Tuple[int, str]]:
27
- """
28
- Returns a tuple (error_code, msg) if not a usable SAS URL, else returns None
29
- """
30
- permissions = sas_blob_utils.get_permissions_from_uri(input_container_sas)
31
- data = sas_blob_utils.get_all_query_parts(input_container_sas)
32
-
33
- msg = ('input_container_sas provided does not have both read and list '
34
- 'permissions.')
35
- if 'read' not in permissions or 'list' not in permissions:
36
- if 'si' in data:
37
- # if no permission specified explicitly but has an access policy, assumes okay
38
- return None
39
-
40
- return 400, msg
41
-
42
- return None
43
-
44
-
45
- def get_utc_time() -> str:
46
- # return current UTC time as a string in the ISO 8601 format (so we can query by
47
- # timestamp in the Cosmos DB job status table.
48
- # example: '2021-02-08T20:02:05.699689Z'
49
- return datetime.utcnow().isoformat(timespec='microseconds') + 'Z'
50
-
51
-
52
- def get_job_status(request_status: str, message: Any) -> dict:
53
- return {
54
- 'request_status': request_status,
55
- 'message': message
56
- }
57
-
58
-
59
- def validate_provided_image_paths(image_paths: Sequence[Any]) -> Tuple[Optional[str], bool]:
60
- """Given a list of image_paths (list length at least 1), validate them and
61
- determine if metadata is available.
62
- Args:
63
- image_paths: a list of string (image_id) or a list of 2-item lists
64
- ([image_id, image_metadata])
65
- Returns:
66
- error: None if checks passed, otherwise a string error message
67
- metadata_available: bool, True if available
68
- """
69
- # image_paths will have length at least 1, otherwise would have ended before this step
70
- first_item = image_paths[0]
71
- metadata_available = False
72
- if isinstance(first_item, str):
73
- for i in image_paths:
74
- if not isinstance(i, str):
75
- error = 'Not all items in image_paths are of type string.'
76
- return error, metadata_available
77
- return None, metadata_available
78
- elif isinstance(first_item, list):
79
- metadata_available = True
80
- for i in image_paths:
81
- if len(i) != 2: # i should be [image_id, metadata_string]
82
- error = ('Items in image_paths are lists, but not all lists '
83
- 'are of length 2 [image locator, metadata].')
84
- return error, metadata_available
85
- return None, metadata_available
86
- else:
87
- error = 'image_paths contain items that are not strings nor lists.'
88
- return error, metadata_available
@@ -1,46 +0,0 @@
1
- #
2
- # If a request has been sent to AML for batch scoring but the monitoring thread of the API was
3
- # interrupted (uncaught exception or having to re-start the API container), we could manually
4
- # aggregate results from each shard using this script, assuming all jobs submitted to AML have finished.
5
- #
6
- # Need to have set environment variables STORAGE_ACCOUNT_NAME and STORAGE_ACCOUNT_KEY to those of the
7
- # storage account backing the API. Also need to adjust the INTERNAL_CONTAINER, AML_CONTAINER and
8
- # AML_CONFIG fields in api_core/orchestrator_api/api_config.py to match the instance of the API that this
9
- # request was submitted to.
10
- #
11
- # May need to change the import statement in api_core/orchestrator_api/orchestrator.py
12
- # "from sas_blob_utils import SasBlob" to
13
- # "from .sas_blob_utils import SasBlob" to not confuse with the module in AI4Eutils;
14
- # and change "import api_config" to
15
- # "from api.batch_processing.api_core.orchestrator_api import api_config"
16
-
17
- # Execute this script from the root of the repository. You may need to add the repository to PYTHONPATH.
18
-
19
- import argparse
20
- import json
21
-
22
- from api.batch_processing.api_core.orchestrator_api.orchestrator import AMLMonitor
23
-
24
-
25
- def main(): # noqa
26
- parser = argparse.ArgumentParser()
27
- parser.add_argument('shortened_request_id', type=str,
28
- help='the request ID to restart monitoring')
29
- parser.add_argument('model_version', type=str, help='version of megadetector used; this is used to fill in the meta info section of the output file')
30
- parser.add_argument('request_name', type=str, help='easy to remember name for that job, optional', default='')
31
- args = parser.parse_args()
32
-
33
-
34
- # list_jobs_submitted cannot be serialized ("can't pickle _thread.RLock objects "), but
35
- # do not need it for aggregating results
36
- aml_monitor = AMLMonitor(request_id=args.request_id,
37
- list_jobs_submitted=None,
38
- request_name=args.request_name,
39
- request_submission_timestamp='',
40
- model_version=args.model_version)
41
- output_file_urls = aml_monitor.aggregate_results()
42
- output_file_urls_str = json.dumps(output_file_urls)
43
- print(output_file_urls_str)
44
-
45
- if __name__ == '__main__':
46
- main()
File without changes