Source code for gpxity.accounts

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

# Copyright (c) 2019 Wolfgang Rohdewald <wolfgang@rohdewald.de>
# See LICENSE for details.
# The source in this file is inspired by and partially identical with paramiko.config


"""Configuration file for accounts in Backends."""

import os
import re
import copy
import tempfile

from gpxpy.geo import Location

__all__ = ['Fences', 'Account', 'DirectoryAccount', 'MemoryAccount']


[docs]class Fences: # pylint: disable=too-few-public-methods """ Defines circles. Args: config_str: The string from the accounts file Attributes: center (GPXTrackPoint): The center radius (meter): The radius in meters """ def __init__(self, config_str: str): """init.""" self.string = config_str or 'None' self.circles = list() if config_str is not None: for fence in config_str.split(' '): parts = fence.split('/') if len(parts) != 3: raise ValueError('fence needs 3 parts: {}'.format(fence)) try: parts = [x.strip() for x in parts] center = Location(float(parts[0]), float(parts[1])) radius = float(parts[2]) except Exception: raise ValueError('Fence definition is wrong: {}'.format(fence)) circle = (center, radius) self.circles.append(circle)
[docs] def outside(self, point) ->bool: """Determine if point is outside of all fences. Returns: True or False. """ return all(point.distance_2d(x[0]) > x[1] for x in self.circles)
def __str__(self): # noqa return self.string def __repr__(self): # noqa return 'Fences({})'.format(str(self)) def __bool__(self): """True if we actually fence. Returns: Result """ return bool(self.circles)
class Accounts: """Representation of config information as stored in the format used by Gpxity. Queries can be made via `lookup`. The keyword :literal:`Account` only allows one name. Keywords are case insensitive, arguments are not. Example for an entry in the accounts file: :: Account wp Backend WPTrackserver Username wordpress_username Url localhost Mysql wordpress_7@wordpress_7 Password xxxx Fences 53.7505,10.7445/750 """ # pylint: disable=too-few-public-methods __SETTINGS_REGEX = re.compile(r'(\w+)(?:\s*=\s*|\s+)(.+)') __account_files = dict() @classmethod def __parse(cls, path): """Parse an accounts file.""" if path not in cls.__account_files: if not os.path.exists(path): return cls.__account_files[path] = cls.__parse_accounts(path) @classmethod def __parse_accounts(cls, filename): """Parse all accounts from filename. Returns: dict with all accounts.filename """ result = dict() with open(filename) as file_obj: for _ in cls.__yield_accounts(file_obj): result[_['name']] = _ return result @staticmethod def __strip_whitespace(file_obj): """Filter out comments, strip lines.""" for line in file_obj: line = line.strip() if line and not line.startswith('#'): yield line @classmethod def __yield_matches(cls, file_obj): """Yield usable lines.""" for line in cls.__strip_whitespace(file_obj): match = re.match(cls.__SETTINGS_REGEX, line) if not match: raise Exception('Unparsable line {}'.format(line)) yield match @classmethod def __yield_accounts(cls, file_obj): """Generate all accounts.""" account = {'name': 'global'} for match in cls.__yield_matches(file_obj): key = match.group(1).lower() value = match.group(2) if key == 'account': if account is not None: yield account account = {'name': value.lower()} continue if value.startswith('"') and value.endswith('"'): value = value[1:-1] if key not in account: account[key] = value if account is not None: yield account @classmethod def lookup(cls, filename: str, wanted_account: str): """ Build an :class:`~gpxity.accounts.Account`. Args: filename: The name of the accounts file wanted_account: The name to look for in the accounts file Returns: dict """ cls.__parse(filename) return copy.deepcopy(cls.__account_files[filename][wanted_account.lower()])
[docs]class Account: """As parsed from the accounts file. Attributes can be referenced as account.xxxx where xxx is an arbitrary value in the account definition from the accounts file. Args: name: The name of the account. Must exist in the accounts file. filename: Name of the accounts file. Default is Account.path kwargs: Additional parameters added to the account. They have precedence. If both name and file are None, only :literal:`**kwargs` are used. Attributes: path: Default value for the accounts file name: The name of the account config: A dict with all config values backend: The name of the backend class fences: The backend will never write points within fences. You can define any number of fences separated by spaces. Every fence is a circle. It has the form Lat/Long/meter. Lat and Long are the center position in decimal degrees, meter is the radius. """ path = '~/.config/Gpxity/accounts' def __init__(self, name=None, filename=None, **kwargs): """Create an Account.""" if name is None: self.config = dict() for key, value in kwargs.items(): self.config[key.lower()] = value self.name = self.url or '.' if not self.backend: self.config['backend'] = 'Directory' self._resolve_fences() return self.name = name path = os.path.expanduser(filename or Account.path) lookup_name = name.split(':')[0] self.config = Accounts.lookup(path, lookup_name) if not self.backend: raise Exception('Account({}, {}, {}) defines no Backend'.format(name, filename, kwargs)) for key, value in kwargs.items(): self.config[key.lower()] = value self._resolve_fences() def _resolve_fences(self): """create self.fences as a Fences instance.""" if 'fences' in self.config: _ = Fences(self.config['fences']) del self.config['fences'] self.fences = _ else: self.fences = Fences(None) def __getattr__(self, key): """Only called if key is not an existing attribute. Returns: The value or None """ try: config = object.__getattribute__(self, 'config') except AttributeError: return None return config.get(key.lower()) def __repr__(self): """For debugging output. Returns: the str """ result = 'Account({}): backend={}'.format(self.account, self.backend) if 'url' in self.config: result += ' url={}'.format(self.url) if 'username' in self.config: result += ' username={}'.format(self.username) return result + ')' def __str__(self): """The account in a parseable form. Returns: The string """ return self.name + ':'
[docs]class DirectoryAccount(Account): """This will not use an acocunts file but the optional file :literal:`.config`. Args: url: The name of the directory. If it does not exist, create it. "" will translate into ".". A trailing "/" will raise an Exception. None will create a temporary directory. kwargs: Additional parameters added to the account. They have precedence. Attributes: path: Default value for the accounts file name: The name of the account config: A dict with all config values backend: The name of the backend class is_temporary: True for temporary directories. fences: The backend will never write points within fences. You can define any number of fences separated by spaces. Every fence is a circle. It has the form Lat/Long/meter. Lat and Long are the center position in decimal degrees, meter is the radius. prefix (str): Class attribute, may be changed. The default prefix for temporary directories. Default value is :literal:`gpxity.` """ path = None prefix = 'gpxity.' def __init__(self, url=None, **kwargs): # pylint: disable=super-init-not-called """Create an Account.""" self.is_temporary = url is None if self.is_temporary: url = tempfile.mkdtemp(prefix=self.__class__.prefix) if url == '': url = '.' if url == '/': raise Exception('Directory / is not allowed') if url.endswith('/') and url != '/': raise Exception('DirectoryAccount: url {} must not end with /'.format(url)) self.config = dict() if not os.path.exists(url): os.makedirs(url) path_parts = os.path.abspath(url).split('/') # TODO: should use os.path.separator for _ in range(1, len(path_parts) + 1): parts = path_parts[:_] dirname = os.path.join(*parts) config_name = '/' + os.path.join(dirname, '.gpxity_config') if os.path.exists(config_name): self.config.update(Accounts.lookup(config_name, 'global')) self.config['backend'] = 'Directory' self.config['url'] = url for key, value in kwargs.items(): self.config[key.lower()] = value self.name = url self._resolve_fences() def __repr__(self): """For debugging output. Returns: the str """ return 'DirectoryAccount({})'.format(self.name) def __str__(self): """The account in a parseable form. Returns: The string """ if self.name == '.': return '' if self.name == '/': return '/' return self.name + '/'
[docs]class MemoryAccount(Account): """This will only use kwargs for configuration. Args: kwargs: Additional parameters added to the account. They have precedence. Attributes: name: The name of the account config: A dict with all config values backend: The name of the backend class is_temporary: True for temporary directories. fences: The backend will never write points within fences. You can define any number of fences separated by spaces. Every fence is a circle. It has the form Lat/Long/meter. Lat and Long are the center position in decimal degrees, meter is the radius. prefix (str): Class attribute, may be changed. The default prefix for temporary directories. Default value is :literal:`gpxity.` """ # pylint: disable=too-few-public-methods counter = 0 def __init__(self, name=None, **kwargs): # pylint: disable=super-init-not-called """Create an Account.""" self.config = dict() self.config['backend'] = 'Memory' for key, value in kwargs.items(): self.config[key.lower()] = value if name is None: name = 'in_memory_{}'.format(MemoryAccount.counter) MemoryAccount.counter += 1 self.name = name self._resolve_fences() def __repr__(self): """For debugging output. Returns: the str """ return 'MemoryAccount({})'.format(self.name) + ':'