import json
import urllib
from yaml import safe_load
import types
import requests
import meteorpi_model as model
def _to_encoded_string(o):
"""
Build an encoded string suitable for use as a URL component. This includes double-escaping the string to
avoid issues with escaped backslash characters being automatically converted by WSGI or, in some cases
such as default Apache servers, blocked entirely.
:param o: an object of any kind, if it has an as_dict() method this will be used, otherwise uses __dict__
:return: an encoded string suitable for use as a URL component
:internal:
"""
_dict = o.__dict__
if o.as_dict:
_dict = o.as_dict()
return urllib.quote_plus(urllib.quote_plus(json.dumps(obj=_dict, separators=(',', ':'))))
[docs]class MeteorClient(object):
"""Client for the MeteorPi HTTP API. Use this to access a camera or central server."""
[docs] def __init__(self, base_url):
"""
Create a new MeteorPi client, use this to access the data in your MeteorPi server.
:param base_url:
the URL for the API. For a camera this will be the address of the camera with '/api/' added, so for example
if your camera website is at 'https://myhost.com/camera' you'd use 'https://myhost.com/camera/api/' here.
You might see a '#' symbol in your web browser address bar, ignore it and just use the bits of the URL
before that point.
:return: a configured instance of the MeteorPi client
"""
self.base_url = base_url
[docs] def list_cameras(self):
"""
Get the IDs of all cameras on this server with currently active status.
:return: a sequence of strings containing camera IDs
"""
response = requests.get(self.base_url + '/cameras').text
return safe_load(response)['cameras']
[docs] def get_camera_status(self, camera_id, status_time=None):
"""
Get details of the specified camera's status
:param string camera_id:
a cameraID, as returned by list_cameras()
:param datetime.datetime status_time:
optional, if specified attempts to get the status for the given camera at a particular point in time
specified as a datetime instance. This is useful if you want to retrieve the status of the camera at the
time a given event or file was produced. If this is None or not specified the time is 'now'.
:return:
a :class:`meteorpi_model.CameraStatus` object, or None if there was either no camera found or the camera
didn't have an active status at the specified time.
"""
if status_time is None:
response = requests.get(
self.base_url + '/cameras/{0}/status'.format(camera_id))
else:
response = requests.get(
self.base_url + '/cameras/{0}/status/{1}'.format(camera_id,
str(model.utc_datetime_to_milliseconds(status_time))))
if response.status_code == 200:
d = safe_load(response.text)
if 'status' in d:
return model.CameraStatus.from_dict(d['status'])
return None
[docs] def search_events(self, search=None):
"""
Search for files, returning a Event for each result. FileRecords within result Events have two additional
methods patched into them, get_url() and download_to(file_name), which will retrieve the URL for the file
content and download that content to a named file on disk, respectively.
:param search:
an instance of EventSearch - see the model docs for details on how to construct this
:return:
an object containing 'count' and 'events'. 'events' is a sequence of Event objects containing the results of
the search, and 'count' is the total number of results which would be returned if no result limit was in
place (i.e. if the number of Events in the 'events' part is less than 'count' you have more records which
weren't returned because of a query limit. Note that the default query limit is 100).
"""
if search is None:
search = model.EventSearch()
search_string = _to_encoded_string(search)
response = requests.get(self.base_url + '/events/{0}'.format(search_string))
response_object = safe_load(response.text)
event_dicts = response_object['events']
event_count = response_object['count']
return {'count': event_count,
'events': list((self._augment_event_files(e) for e in (model.Event.from_dict(d) for d in event_dicts)))}
[docs] def search_files(self, search=None):
"""
Search for files, returning a FileRecord for each result. FileRecords have two additional
methods patched into them, get_url() and download_to(file_name), which will retrieve the URL for the file
content and download that content to a named file on disk, respectively.
:param FileRecordSearch search:
an instance of :class:`meteorpi_model.FileRecordSearch` - see the model docs for details on how to construct
this
:return:
an object containing 'count' and 'files'. 'files' is a sequence of FileRecord objects containing the
results of the search, and 'count' is the total number of results which would be returned if no result limit
was in place (i.e. if the number of FileRecords in the 'files' part is less than 'count' you have more
records which weren't returned because of a query limit. Note that the default query limit is 100).
"""
if search is None:
search = model.FileRecordSearch()
search_string = _to_encoded_string(search)
response = requests.get(self.base_url + '/files/{0}'.format(search_string))
response_object = safe_load(response.text)
file_dicts = response_object['files']
file_count = response_object['count']
return {'count': file_count,
'files': list((self._augment_file(f) for f in (model.FileRecord.from_dict(d) for d in file_dicts)))}
def _augment_file(self, f):
"""
Augment a FileRecord with methods to get the data URL and to download, returning the updated file for use
in generator functions
:internal:
"""
def get_url(target):
if target.file_size is None:
return None
if target.file_name is not None:
return self.base_url + '/files/content/{0}/{1}'.format(target.file_id.hex, target.file_name)
else:
return self.base_url + '/files/content/{0}'.format(target.file_id.hex, )
f.get_url = types.MethodType(get_url, f)
def download_to(target, file_name):
url = target.get_url()
r = requests.get(url, stream=True)
with open(file_name, 'wb') as file_to_write:
for chunk in r.iter_content(chunk_size=1024):
if chunk: # filter out keep-alive new chunks
file_to_write.write(chunk)
file_to_write.flush()
return file_name
f.download_to = types.MethodType(download_to, f)
return f
def _augment_event_files(self, e):
"""
Augment all the file records in an event
:internal:
"""
e.file_records = list(self._augment_file(f) for f in e.file_records)
return e