smartconnect-client 1.11.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.
- smartconnect/__init__.py +552 -0
- smartconnect/async_client.py +619 -0
- smartconnect/cache.py +23 -0
- smartconnect/data.py +72 -0
- smartconnect/er_sync_utils.py +264 -0
- smartconnect/exceptions.py +16 -0
- smartconnect/models.py +489 -0
- smartconnect/smart_settings.py +14 -0
- smartconnect/utils.py +28 -0
- smartconnect_client-1.11.1.dist-info/METADATA +42 -0
- smartconnect_client-1.11.1.dist-info/RECORD +13 -0
- smartconnect_client-1.11.1.dist-info/WHEEL +4 -0
- smartconnect_client-1.11.1.dist-info/licenses/LICENSE +201 -0
smartconnect/__init__.py
ADDED
|
@@ -0,0 +1,552 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
import uuid
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from typing import List, Optional
|
|
6
|
+
from functools import wraps
|
|
7
|
+
|
|
8
|
+
import pytz
|
|
9
|
+
import httpx
|
|
10
|
+
from pydantic import parse_obj_as
|
|
11
|
+
from pydantic.main import BaseModel
|
|
12
|
+
|
|
13
|
+
from smartconnect import models, cache, smart_settings, data
|
|
14
|
+
from .exceptions import SMARTClientException, SMARTClientServerError, SMARTClientClientError, SMARTClientServerUnreachableError, SMARTClientUnauthorizedError
|
|
15
|
+
from .async_client import AsyncSmartClient
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
from smartconnect.models import SMARTRequest, SMARTResponse, Patrol, PatrolDataModel, DataModel, ConservationArea, \
|
|
20
|
+
ConfigurableDataModel, SmartConnectApiInfo
|
|
21
|
+
|
|
22
|
+
# Manually bump this.
|
|
23
|
+
__version__ = '1.11.0'
|
|
24
|
+
|
|
25
|
+
DEFAULT_TIMEOUT = (smart_settings.SMART_DEFAULT_CONNECT_TIMEOUT, smart_settings.SMART_DEFAULT_TIMEOUT)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def with_login_session():
|
|
29
|
+
|
|
30
|
+
def decorator(func):
|
|
31
|
+
@wraps(func)
|
|
32
|
+
def wrapper(self, *args, **kwargs):
|
|
33
|
+
self.ensure_login()
|
|
34
|
+
return func(self, *args, **kwargs)
|
|
35
|
+
return wrapper
|
|
36
|
+
return decorator
|
|
37
|
+
class SmartClient:
|
|
38
|
+
|
|
39
|
+
# TODO: Figure out how to specify timezone.
|
|
40
|
+
SMARTCONNECT_DATFORMAT = '%Y-%m-%dT%H:%M:%S'
|
|
41
|
+
|
|
42
|
+
def __init__(self, *, api=None, username=None, password=None, use_language_code='en', version="7.5",
|
|
43
|
+
read_timeout=None, connect_timeout=None):
|
|
44
|
+
|
|
45
|
+
self.api = api.rstrip('/') # trim trailing slash in case configured into portal with one
|
|
46
|
+
self.username = username
|
|
47
|
+
self.password = password
|
|
48
|
+
self.use_language_code=use_language_code
|
|
49
|
+
self.version=version
|
|
50
|
+
|
|
51
|
+
self.logger = logging.getLogger(SmartClient.__name__)
|
|
52
|
+
self.verify_ssl = smart_settings.SMART_SSL_VERIFY
|
|
53
|
+
|
|
54
|
+
# Configure httpx client with timeout and retries
|
|
55
|
+
self.max_retries = smart_settings.SMART_DEFAULT_CONNECT_RETRIES
|
|
56
|
+
transport = httpx.HTTPTransport(retries=self.max_retries)
|
|
57
|
+
# Use provided timeout values or fall back to environment variables
|
|
58
|
+
connect_timeout = connect_timeout if connect_timeout is not None else smart_settings.SMART_DEFAULT_CONNECT_TIMEOUT
|
|
59
|
+
data_timeout = read_timeout if read_timeout is not None else smart_settings.SMART_DEFAULT_TIMEOUT
|
|
60
|
+
timeout = httpx.Timeout(data_timeout, connect=connect_timeout, pool=connect_timeout)
|
|
61
|
+
|
|
62
|
+
self._session = httpx.Client(transport=transport, timeout=timeout, verify=self.verify_ssl)
|
|
63
|
+
|
|
64
|
+
def ensure_login(self):
|
|
65
|
+
'''
|
|
66
|
+
Login flow for SMART Connect. If the session already has a JSESSIONID cookie, it is assumed to be logged in.
|
|
67
|
+
'''
|
|
68
|
+
for k, v in self._session.cookies.items():
|
|
69
|
+
if k == 'JSESSIONID' and v:
|
|
70
|
+
return self._session
|
|
71
|
+
|
|
72
|
+
# Request the landing page to prime the session.
|
|
73
|
+
landing_page = self._session.get(f'{self.api}/connect/home')
|
|
74
|
+
if not landing_page.is_success:
|
|
75
|
+
raise SMARTClientServerUnreachableError(f"Failed to retrieve landing page {landing_page.url}. Status code: {landing_page.status_code}")
|
|
76
|
+
|
|
77
|
+
login_result = self._session.post(f'{self.api}/j_security_check',
|
|
78
|
+
data={"j_username": self.username, "j_password": self.password})
|
|
79
|
+
|
|
80
|
+
if login_result.is_success or login_result.is_redirect:
|
|
81
|
+
return self._session
|
|
82
|
+
|
|
83
|
+
if login_result.status_code == 401:
|
|
84
|
+
raise SMARTClientUnauthorizedError(f"Failed to login to SMART Connect {login_result.url}. Status code: {login_result.status_code}")
|
|
85
|
+
|
|
86
|
+
logger.error(f"Failed to login to SMART Connect {login_result.url}. Status code: {login_result.status_code}, {login_result.text[:250]}")
|
|
87
|
+
exception_class = SMARTClientClientError if login_result.is_client_error else SMARTClientServerError
|
|
88
|
+
raise exception_class(f"Failed to login to SMART Connect {login_result.url}. Status code: {login_result.status_code}")
|
|
89
|
+
|
|
90
|
+
@with_login_session()
|
|
91
|
+
def get_server_api_info(self):
|
|
92
|
+
cas = self._session.get(f'{self.api}/api/info',
|
|
93
|
+
headers={
|
|
94
|
+
'accept': 'application/json',
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
if cas.is_success:
|
|
98
|
+
return SmartConnectApiInfo.parse_obj(cas.json())
|
|
99
|
+
|
|
100
|
+
@with_login_session()
|
|
101
|
+
def get_conservation_area(self, *, ca_uuid: str = None, force: bool = False):
|
|
102
|
+
|
|
103
|
+
cache_key = f"cache:smart-ca:{ca_uuid}:metadata"
|
|
104
|
+
if not force:
|
|
105
|
+
self.logger.info(f"Looking up CA cached at {cache_key}.")
|
|
106
|
+
try:
|
|
107
|
+
cached_data = cache.cache.get(cache_key)
|
|
108
|
+
if cached_data:
|
|
109
|
+
self.logger.info(f"Found CA cached at {cache_key}.")
|
|
110
|
+
conservation_area = ConservationArea.parse_raw(cached_data)
|
|
111
|
+
return conservation_area
|
|
112
|
+
|
|
113
|
+
self.logger.info(f"Cache miss for {cache_key}")
|
|
114
|
+
except:
|
|
115
|
+
self.logger.info(f"Cache miss/error for {cache_key}")
|
|
116
|
+
pass
|
|
117
|
+
|
|
118
|
+
try:
|
|
119
|
+
self.logger.info(
|
|
120
|
+
"Querying Smart Connect for CAs at endpoint: %s, username: %s",
|
|
121
|
+
self.api,
|
|
122
|
+
self.username,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
for ca in self.get_conservation_areas():
|
|
126
|
+
if ca.uuid == uuid.UUID(ca_uuid):
|
|
127
|
+
conservation_area = ca
|
|
128
|
+
break
|
|
129
|
+
else:
|
|
130
|
+
logger.error(
|
|
131
|
+
f"Can't find a Conservation Area with UUID: {ca_uuid}"
|
|
132
|
+
)
|
|
133
|
+
conservation_area = None
|
|
134
|
+
|
|
135
|
+
if conservation_area:
|
|
136
|
+
self.logger.info(f"Caching CA metadata at {cache_key}")
|
|
137
|
+
cache.cache.set(
|
|
138
|
+
name=cache_key,
|
|
139
|
+
value=json.dumps(dict(conservation_area), default=str),
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
return conservation_area
|
|
143
|
+
|
|
144
|
+
except Exception as ex:
|
|
145
|
+
self.logger.exception(
|
|
146
|
+
f"Failed to get Conservation Areas", extra=dict(ca_uuid=ca_uuid)
|
|
147
|
+
)
|
|
148
|
+
raise SMARTClientException(f"Failed to get SMART Conservation Areas") from ex
|
|
149
|
+
|
|
150
|
+
@with_login_session()
|
|
151
|
+
def get_conservation_areas(self) -> List[models.ConservationArea]:
|
|
152
|
+
cas = self._session.get(f'{self.api}/api/conservationarea',
|
|
153
|
+
headers={
|
|
154
|
+
'accept': 'application/json',
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
if cas.is_success:
|
|
158
|
+
cas = cas.json()
|
|
159
|
+
|
|
160
|
+
return [models.ConservationArea.parse_obj(ca) for ca in cas]
|
|
161
|
+
|
|
162
|
+
@with_login_session()
|
|
163
|
+
def list_configurable_datamodels(self, *, ca_uuid: str):
|
|
164
|
+
|
|
165
|
+
assert ca_uuid, "ca_uuid is required"
|
|
166
|
+
'''
|
|
167
|
+
Get metadata about the configurable data models for a given CA.
|
|
168
|
+
:param ca_uuid: The UUID of the CA to get the configurable data models for.
|
|
169
|
+
:return: Smart Connect response that contains the configurable data models.
|
|
170
|
+
'''
|
|
171
|
+
extra_dict = dict(ca_uuid=ca_uuid,
|
|
172
|
+
url=f'{self.api}/metadata/configurablemodel')
|
|
173
|
+
|
|
174
|
+
config_datamodels = self._session.get(f'{self.api}/api/metadata/configurablemodel',
|
|
175
|
+
params={"ca_uuid": ca_uuid},
|
|
176
|
+
headers={
|
|
177
|
+
'accept': 'application/json',
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
self.logger.info(f'Get configurable data models for CA {ca_uuid} data model took {config_datamodels.elapsed.total_seconds()} seconds',
|
|
181
|
+
extra=dict(**extra_dict,
|
|
182
|
+
status_code=config_datamodels.status_code))
|
|
183
|
+
|
|
184
|
+
if not config_datamodels.is_success:
|
|
185
|
+
self.logger.error(
|
|
186
|
+
f'Failed to download configurable datamodels for CA {ca_uuid}. Status_code is: {config_datamodels.status_code}',
|
|
187
|
+
extra=dict(**extra_dict,
|
|
188
|
+
status_code=config_datamodels.status_code))
|
|
189
|
+
raise Exception('Failed to download Configurable Data Models')
|
|
190
|
+
|
|
191
|
+
cdms = config_datamodels.json()
|
|
192
|
+
return cdms
|
|
193
|
+
|
|
194
|
+
@with_login_session()
|
|
195
|
+
def download_configurable_datamodel(self, *, cm_uuid: str = None):
|
|
196
|
+
|
|
197
|
+
extra_dict = dict(cm_uuid=cm_uuid,
|
|
198
|
+
url=f'{self.api}/api/metadata/configurablemodel/{cm_uuid}')
|
|
199
|
+
|
|
200
|
+
config_datamodel = self._session.get(f'{self.api}/api/metadata/configurablemodel/{cm_uuid}',
|
|
201
|
+
headers={
|
|
202
|
+
'accept': 'application/xml',
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
self.logger.info(f'Download configurable model {cm_uuid} data model took {config_datamodel.elapsed.total_seconds()} seconds',
|
|
206
|
+
extra=dict(**extra_dict,
|
|
207
|
+
status_code=config_datamodel.status_code))
|
|
208
|
+
|
|
209
|
+
if not config_datamodel.is_success:
|
|
210
|
+
self.logger.error(
|
|
211
|
+
f'Failed to download data model for configurable model {cm_uuid}. Status_code is: {config_datamodel.status_code}',
|
|
212
|
+
extra=dict(**extra_dict,
|
|
213
|
+
status_code=config_datamodel.status_code))
|
|
214
|
+
raise Exception('Failed to download Data Model')
|
|
215
|
+
|
|
216
|
+
cdm = ConfigurableDataModel(cm_uuid=cm_uuid, use_language_code=self.use_language_code)
|
|
217
|
+
cdm.load(config_datamodel.text)
|
|
218
|
+
|
|
219
|
+
return cdm
|
|
220
|
+
|
|
221
|
+
@with_login_session()
|
|
222
|
+
def get_configurable_data_model(self, *, cm_uuid: str = None, force: bool = False):
|
|
223
|
+
# TODO: version consideration
|
|
224
|
+
# TODO: Implement caching
|
|
225
|
+
|
|
226
|
+
ca_uuid = 'na'
|
|
227
|
+
cache_key = f'cache:smart-ca:{ca_uuid}:cdm:{cm_uuid}'
|
|
228
|
+
|
|
229
|
+
if not force:
|
|
230
|
+
try:
|
|
231
|
+
cached_data = cache.cache.get(cache_key)
|
|
232
|
+
if cached_data:
|
|
233
|
+
|
|
234
|
+
cm = ConfigurableDataModel(use_language_code=self.use_language_code)
|
|
235
|
+
cm.import_from_dict(json.loads(cached_data))
|
|
236
|
+
|
|
237
|
+
self.logger.debug(
|
|
238
|
+
f"Using cached SMART Configurable Data Model", extra={"cached_key": cache_key}
|
|
239
|
+
)
|
|
240
|
+
return cm
|
|
241
|
+
|
|
242
|
+
except Exception as ex:
|
|
243
|
+
logger.exception('Failed on reading configurable model from cache.', extra={'cache_key': cache_key})
|
|
244
|
+
|
|
245
|
+
# Re-download and cache.
|
|
246
|
+
ca_config_datamodel = self.download_configurable_datamodel(
|
|
247
|
+
cm_uuid=cm_uuid
|
|
248
|
+
)
|
|
249
|
+
cache.cache.set(cache_key, json.dumps(ca_config_datamodel.export_as_dict()))
|
|
250
|
+
return ca_config_datamodel
|
|
251
|
+
|
|
252
|
+
def get_data_model(self, *, ca_uuid: str = None, force: bool = False):
|
|
253
|
+
|
|
254
|
+
# CA Data Model is not available for versions below 7. Use a blank data model.
|
|
255
|
+
if self.version.startswith("6"):
|
|
256
|
+
blank_datamodel = DataModel()
|
|
257
|
+
blank_datamodel.load(data.BLANK_DATAMODEL_CONTENT)
|
|
258
|
+
return blank_datamodel
|
|
259
|
+
|
|
260
|
+
cache_key = f"cache:smart-ca:{ca_uuid}:datamodel"
|
|
261
|
+
if not force:
|
|
262
|
+
try:
|
|
263
|
+
cached_data = cache.cache.get(cache_key)
|
|
264
|
+
if cached_data:
|
|
265
|
+
dm = DataModel(use_language_code=self.use_language_code)
|
|
266
|
+
dm.import_from_dict(json.loads(cached_data))
|
|
267
|
+
self.logger.debug(
|
|
268
|
+
f"Using cached SMART Datamodel", extra={"cached_key": cache_key}
|
|
269
|
+
)
|
|
270
|
+
return dm
|
|
271
|
+
|
|
272
|
+
except Exception as ex:
|
|
273
|
+
logger.exception('Failed on reading data model from cache.', extra={'cache_key': cache_key})
|
|
274
|
+
|
|
275
|
+
logger.debug(f"Cache miss for SMART Datamodel", extra={"cached_key": cache_key})
|
|
276
|
+
|
|
277
|
+
try:
|
|
278
|
+
ca_datamodel = self.download_datamodel(
|
|
279
|
+
ca_uuid=ca_uuid
|
|
280
|
+
)
|
|
281
|
+
except Exception as e:
|
|
282
|
+
logger.exception(e)
|
|
283
|
+
raise SMARTClientException(f"Failed downloading SMART Datamodel for CA {ca_uuid}") from e
|
|
284
|
+
|
|
285
|
+
if ca_datamodel:
|
|
286
|
+
cache.cache.set(
|
|
287
|
+
name=cache_key,
|
|
288
|
+
value=json.dumps(ca_datamodel.export_as_dict()),
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
return ca_datamodel
|
|
292
|
+
|
|
293
|
+
@with_login_session()
|
|
294
|
+
def download_datamodel(self, *, ca_uuid: str = None):
|
|
295
|
+
|
|
296
|
+
extra_dict = dict(ca_uuid=ca_uuid,
|
|
297
|
+
url=f'{self.api}/api/metadata/datamodel/{ca_uuid}')
|
|
298
|
+
|
|
299
|
+
ca_datamodel = self._session.get(f'{self.api}/api/metadata/datamodel/{ca_uuid}',
|
|
300
|
+
headers={
|
|
301
|
+
'accept': 'application/xml',
|
|
302
|
+
})
|
|
303
|
+
|
|
304
|
+
self.logger.info(f'Download CA {ca_uuid} data model took {ca_datamodel.elapsed.total_seconds()} seconds',
|
|
305
|
+
extra=dict(**extra_dict,
|
|
306
|
+
status_code=ca_datamodel.status_code))
|
|
307
|
+
|
|
308
|
+
if not ca_datamodel.is_success:
|
|
309
|
+
self.logger.error(
|
|
310
|
+
f'Failed to download data model for CA {ca_uuid}. Status_code is: {ca_datamodel.status_code}',
|
|
311
|
+
extra=dict(**extra_dict,
|
|
312
|
+
status_code=ca_datamodel.status_code))
|
|
313
|
+
raise Exception('Failed to download Data Model')
|
|
314
|
+
|
|
315
|
+
dm = DataModel(use_language_code=self.use_language_code)
|
|
316
|
+
dm.load(ca_datamodel.text)
|
|
317
|
+
return dm
|
|
318
|
+
|
|
319
|
+
def load_datamodel(self, *, filename=None):
|
|
320
|
+
|
|
321
|
+
with open(filename, 'r') as fi:
|
|
322
|
+
contents = fi.read()
|
|
323
|
+
|
|
324
|
+
dm = DataModel(use_language_code=self.use_language_code)
|
|
325
|
+
dm.load(contents)
|
|
326
|
+
return dm
|
|
327
|
+
|
|
328
|
+
@with_login_session()
|
|
329
|
+
def download_patrolmodel(self, *, ca_uuid: str = None):
|
|
330
|
+
|
|
331
|
+
ca_patrolmodel = self._session.get(f'{self.api}/api/metadata/patrol/{ca_uuid}',
|
|
332
|
+
headers={
|
|
333
|
+
'accept': 'application/json',
|
|
334
|
+
})
|
|
335
|
+
|
|
336
|
+
self.logger.info('Downloaded CA Patrol Model. Status code is: %s', ca_patrolmodel.status_code)
|
|
337
|
+
|
|
338
|
+
if not ca_patrolmodel.is_success:
|
|
339
|
+
self.logger.error('Failed to download CA Patrol Model. Status_code is: %s', ca_patrolmodel.status_code)
|
|
340
|
+
raise Exception('Failed to download Patrol Model.')
|
|
341
|
+
|
|
342
|
+
pm = PatrolDataModel.parse_obj(ca_patrolmodel.json())
|
|
343
|
+
return pm
|
|
344
|
+
|
|
345
|
+
@with_login_session()
|
|
346
|
+
def download_missionmodel(self, *, ca_uuid: str = None):
|
|
347
|
+
|
|
348
|
+
ca_missionmodel = self._session.get(f'{self.api}/api/metadata/mission/{ca_uuid}',
|
|
349
|
+
headers={
|
|
350
|
+
'accept': 'application/json',
|
|
351
|
+
})
|
|
352
|
+
|
|
353
|
+
self.logger.info('Downloaded CA Mission Model. Status code is: %s', ca_missionmodel.status_code)
|
|
354
|
+
|
|
355
|
+
if not ca_missionmodel.is_success:
|
|
356
|
+
self.logger.error('Failed to download CA Mission Model. Status_code is: %s', ca_missionmodel.status_code)
|
|
357
|
+
raise Exception('Failed to download Mission Model.')
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
with open('_missionmodel.json', 'w') as fo:
|
|
361
|
+
fo.write(ca_missionmodel.text)
|
|
362
|
+
|
|
363
|
+
@with_login_session()
|
|
364
|
+
def get_patrol(self, *, patrol_id=None):
|
|
365
|
+
|
|
366
|
+
response = self._session.get(f'{self.api}/api/query/custom/patrol',
|
|
367
|
+
params= {"client_patrol_uuid": patrol_id})
|
|
368
|
+
|
|
369
|
+
if response.is_success and len(response.json()) > 0:
|
|
370
|
+
patrol = parse_obj_as(List[Patrol],response.json())[0]
|
|
371
|
+
return patrol
|
|
372
|
+
|
|
373
|
+
@with_login_session()
|
|
374
|
+
def get_patrol_waypoints(self, *, patrol_id=None):
|
|
375
|
+
|
|
376
|
+
response = self._session.get(f'{self.api}/api/query/custom/waypoint/patrol',
|
|
377
|
+
params= {"client_patrol_uuid": patrol_id})
|
|
378
|
+
|
|
379
|
+
if response.is_success and len(response.json()) > 0:
|
|
380
|
+
smart_response = parse_obj_as(List[SMARTResponse],response.json())
|
|
381
|
+
return [item.properties.waypoint for item in smart_response]
|
|
382
|
+
|
|
383
|
+
@with_login_session()
|
|
384
|
+
def get_incident(self, *, incident_uuid=None):
|
|
385
|
+
|
|
386
|
+
url = f'{self.api}/api/query/custom/waypoint/incident'
|
|
387
|
+
response = self._session.get(url,
|
|
388
|
+
params={"client_incident_uuid": incident_uuid})
|
|
389
|
+
|
|
390
|
+
if not response.is_success:
|
|
391
|
+
logger.error("Failed lookup for incident %s", incident_uuid,
|
|
392
|
+
extra=dict(url=url,
|
|
393
|
+
response_content=response.content)
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
response_data = response.json()
|
|
397
|
+
if response_data and isinstance(response_data, list):
|
|
398
|
+
return parse_obj_as(List[SMARTResponse], response_data)
|
|
399
|
+
|
|
400
|
+
def generate_patrol_label(self, *, device_id=None, prefix='wildlife', ts=None):
|
|
401
|
+
|
|
402
|
+
ts = ts or datetime.now(tz=pytz.utc)
|
|
403
|
+
|
|
404
|
+
return '/'.join( (prefix, device_id, ts.strftime('%Y/%m')) )
|
|
405
|
+
|
|
406
|
+
@with_login_session()
|
|
407
|
+
def post_smart_request(self, *, json: str, ca_uuid: str = None):
|
|
408
|
+
|
|
409
|
+
url = f'{self.api}/api/data/{ca_uuid}'
|
|
410
|
+
response = self._session.post(url,
|
|
411
|
+
headers={'content-type': 'application/json'},
|
|
412
|
+
content=json)
|
|
413
|
+
if response.is_success:
|
|
414
|
+
logger.info("Posted request to SMART successfully")
|
|
415
|
+
if logger.isEnabledFor(logging.DEBUG):
|
|
416
|
+
logger.debug("SMART request succeeded", extra=dict(url=url,
|
|
417
|
+
ca_uuid=ca_uuid,
|
|
418
|
+
request=json,
|
|
419
|
+
response=response.content),
|
|
420
|
+
response_code=response.status_code)
|
|
421
|
+
else:
|
|
422
|
+
message = f"SMART request failed for {url} with response code {response.status_code}"
|
|
423
|
+
logger.exception(message, extra=dict(url=url,
|
|
424
|
+
ca_uuid=ca_uuid,
|
|
425
|
+
request=json,
|
|
426
|
+
response=response.content,
|
|
427
|
+
response_code=response.status_code)
|
|
428
|
+
)
|
|
429
|
+
raise SMARTClientException(message)
|
|
430
|
+
|
|
431
|
+
# Functions for quick testing
|
|
432
|
+
@with_login_session()
|
|
433
|
+
def add_patrol_trackpoint(self, *, ca_uuid: str = None, patrol_uuid: str = None, patrol_leg_uuid: str = None, x=None, y=None, timestamp=None):
|
|
434
|
+
|
|
435
|
+
track_point = {
|
|
436
|
+
"type": "Feature",
|
|
437
|
+
"geometry": {
|
|
438
|
+
"coordinates": [x, y],
|
|
439
|
+
"type": "Point"
|
|
440
|
+
},
|
|
441
|
+
"properties": {
|
|
442
|
+
"dateTime": timestamp.strftime(self.SMARTCONNECT_DATFORMAT),
|
|
443
|
+
|
|
444
|
+
"smartDataType": "patrol",
|
|
445
|
+
"smartFeatureType": "trackpoint/new",
|
|
446
|
+
"smartAttributes": {
|
|
447
|
+
"patrolUuid": patrol_uuid, # required
|
|
448
|
+
"patrolLegUuid": patrol_leg_uuid, # required
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
response = self._session.post(f'{self.api}/api/data/{ca_uuid}',
|
|
454
|
+
json=track_point)
|
|
455
|
+
|
|
456
|
+
if response.is_success:
|
|
457
|
+
print('All good mate!')
|
|
458
|
+
print(response.status_code, response.content)
|
|
459
|
+
|
|
460
|
+
@with_login_session()
|
|
461
|
+
def add_patrol_waypoint(self, *, ca_uuid: str = None, patrol_uuid: str = None, patrol_leg_uuid: str = None,
|
|
462
|
+
x=None, y=None, timestamp=None):
|
|
463
|
+
|
|
464
|
+
way_point = {
|
|
465
|
+
"type": "Feature",
|
|
466
|
+
"geometry": {
|
|
467
|
+
"coordinates": [x, y],
|
|
468
|
+
"type": "Point"
|
|
469
|
+
},
|
|
470
|
+
"properties": {
|
|
471
|
+
"dateTime": timestamp.strftime(self.SMARTCONNECT_DATFORMAT),
|
|
472
|
+
|
|
473
|
+
"smartDataType": "patrol",
|
|
474
|
+
"smartFeatureType": "waypoint/new",
|
|
475
|
+
"smartAttributes": {
|
|
476
|
+
"patrolUuid": patrol_uuid, # required
|
|
477
|
+
"patrolLegUuid": patrol_leg_uuid, # required
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
response = self._session.post(f'{self.api}/api/data/{ca_uuid}',
|
|
483
|
+
json=way_point)
|
|
484
|
+
|
|
485
|
+
if response.is_success:
|
|
486
|
+
print('All good mate!')
|
|
487
|
+
print(response.status_code, response.content)
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
# TODO: Depreciate can just use post_smart_request
|
|
491
|
+
@with_login_session()
|
|
492
|
+
def add_independent_incident(self, *, incident: models.SMARTRequest, ca_uuid: str = None):
|
|
493
|
+
|
|
494
|
+
response = self._session.post(f'{self.api}/api/data/{ca_uuid}', headers={'content-type': 'application/json'},
|
|
495
|
+
content=incident.json())
|
|
496
|
+
|
|
497
|
+
if response.is_success:
|
|
498
|
+
print('All good mate!')
|
|
499
|
+
print(response.content)
|
|
500
|
+
else:
|
|
501
|
+
logger.error('Failed posting independent incident. %s', response.text)
|
|
502
|
+
|
|
503
|
+
logger.debug(response.status_code, response.content)
|
|
504
|
+
|
|
505
|
+
@with_login_session()
|
|
506
|
+
def add_mission(self, *, ca_uuid: str = None):
|
|
507
|
+
|
|
508
|
+
present = datetime.now(tz=pytz.utc)
|
|
509
|
+
|
|
510
|
+
mission_uuid = str(uuid.uuid4())
|
|
511
|
+
|
|
512
|
+
print(f'Using Mission id: {mission_uuid}')
|
|
513
|
+
track_point = {
|
|
514
|
+
"type": "Feature",
|
|
515
|
+
"geometry": {
|
|
516
|
+
"coordinates": [11.41, -9.2],
|
|
517
|
+
"type": "Point"
|
|
518
|
+
},
|
|
519
|
+
"properties": {
|
|
520
|
+
"dateTime": present.strftime(self.SMARTCONNECT_DATFORMAT),
|
|
521
|
+
|
|
522
|
+
"smartDataType": "mission",
|
|
523
|
+
"smartFeatureType": "start",
|
|
524
|
+
"smartAttributes": {
|
|
525
|
+
'missionId': 'wildilfe/2021/07/mission-00004',
|
|
526
|
+
"missionUuid": mission_uuid, # required
|
|
527
|
+
"survey": '3e6efdfdb3fe4929816ab1f735a80fad',
|
|
528
|
+
# "team": "communityteam1",
|
|
529
|
+
"objective": "Tracking wildlife", #required
|
|
530
|
+
"comment": "Tracking wildlife", #required
|
|
531
|
+
"isArmed": "false",
|
|
532
|
+
"transportType": "wildlife", #required
|
|
533
|
+
"mandate": "animaltracking", #required
|
|
534
|
+
# "number": -999,
|
|
535
|
+
# "test": "test attribute",
|
|
536
|
+
# "members": ['9b07b0d7155d44de93e8361fede22297'], # Required
|
|
537
|
+
# "leader": '9b07b0d7155d44de93e8361fede22297', # required
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
response = self._session.post(f'{self.api}/api/data/{ca_uuid}',
|
|
544
|
+
json=track_point)
|
|
545
|
+
|
|
546
|
+
if response.is_success:
|
|
547
|
+
print('All good mate!')
|
|
548
|
+
print(response.status_code, response.content)
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
|