import enum import ipaddress import json import locale from collections import Iterable from datetime import datetime, timedelta from decimal import Decimal from distutils.version import StrictVersion from functools import wraps from typing import Generator, Union from uuid import UUID class JSONEncoder(json.JSONEncoder): """An overloaded JSON Encoder with extra type support.""" def default(self, obj): if isinstance(obj, enum.Enum): return obj.name elif isinstance(obj, datetime): return obj.isoformat() elif isinstance(obj, timedelta): return round(obj.total_seconds()) elif isinstance(obj, UUID): return str(obj) elif isinstance(obj, StrictVersion): return str(obj) elif isinstance(obj, set): return list(obj) elif isinstance(obj, Decimal): return float(obj) elif isinstance(obj, Dumpeable): return obj.dump() elif isinstance(obj, ipaddress._BaseAddress): return str(obj) # Instead of failing, return the string representation by default return str(obj) class Dumpeable: """Dumps dictionaries and jsons for Devicehub. A base class to allow subclasses to generate dictionaries and json suitable for sending to a Devicehub, i.e. preventing private and constants to be in the JSON and camelCases field names. """ ENCODER = JSONEncoder def dump(self): """ Creates a dictionary consisting of the non-private fields of this instance with camelCase field names. """ import inflection return { inflection.camelize(name, uppercase_first_letter=False): getattr(self, name) for name in self._field_names() if not name.startswith('_') and not name[0].isupper() } def _field_names(self): """An iterable of the names to dump.""" # Feel free to override this return vars(self).keys() def to_json(self): """ Creates a JSON representation of the non-private fields of this class. """ return json.dumps(self, cls=self.ENCODER, indent=2) class DumpeableModel(Dumpeable): """A dumpeable for SQLAlchemy models. Note that this does not avoid recursive relations. """ def _field_names(self): from sqlalchemy import inspect return (a.key for a in inspect(self).attrs) def ensure_utf8(app_name_to_show_on_error: str): """ Python3 uses by default the system set, but it expects it to be ‘utf-8’ to work correctly. This can generate problems in reading and writing files and in ``.decode()`` method. An example how to 'fix' it:: echo 'export LC_CTYPE=en_US.UTF-8' > .bash_profile echo 'export LC_ALL=en_US.UTF-8' > .bash_profile """ encoding = locale.getpreferredencoding() if encoding.lower() != 'utf-8': raise OSError( '{} works only in UTF-8, but yours is set at {}' ''.format(app_name_to_show_on_error, encoding) ) def now() -> datetime: """ Returns a compatible 'now' with DeviceHub's API, this is as UTC and without microseconds. """ return datetime.utcnow().replace(microsecond=0) def flatten_mixed(values: Iterable) -> Generator: """ Flatten a list containing lists and other elements. This is not deep. >>> list(flatten_mixed([1, 2, [3, 4]])) [1, 2, 3, 4] """ for x in values: if isinstance(x, list): for y in x: yield y else: yield x def if_none_return_none(f): """If the first value is None return None, otherwise execute f.""" @wraps(f) def wrapper(self, value, *args, **kwargs): if value is None: return None return f(self, value, *args, **kwargs) return wrapper def local_ip( dest='109.69.8.152', ) -> Union[ipaddress.IPv4Address, ipaddress.IPv6Address]: """Gets the local IP of the interface that has access to the Internet. This is a reliable way to test if a device has an active connection to the Internet. This method works by connecting, by default, to the IP of ereuse01.ereuse.org. >>> local_ip() :raise OSError: The device cannot connect to the Internet. """ import socket, ipaddress s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) s.connect((dest, 80)) ip = s.getsockname()[0] s.close() return ipaddress.ip_address(ip) def version(package_name: str) -> StrictVersion: """Returns the version of a package name installed with pip.""" # From https://stackoverflow.com/a/2073599 import pkg_resources return StrictVersion(pkg_resources.require(package_name)[0].version)