Source code for pyqrcodeng.qrspecial

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright (c) 2016, Riccardo Metere
# Copyright (c) 2018 - 2019, Lars Heuer
# All rights reserved.
#
# License: BSD License
#
"""
Generation of special-purpose text for Qr codes.
"""
from __future__ import absolute_import, unicode_literals, print_function
import warnings

# <https://wiki.python.org/moin/PortingToPy3k/BilingualQuickRef#New_Style_Classes>
__metaclass__ = type


[docs]class QrSpecial: """ Special-purpose text for QR codes. Implements the special text generated by the ZXing project for QR codes. Likely, these are correctly handled by software using the this library. Of note: - the `Event` special text is not supported here, but it can be handled by using the `icalendar` package [https://pypi.python.org/pypi/icalendar]. - the `vCard` contact format is not supported here (only MeCard), but a number of packages for handling vCards are available in PyPI. """ _kv_sep = ':' _sep = ';' _token_fmt = '{{}}{kv_sep}{{}}'.format(kv_sep=_kv_sep) _end_tag = ';;' _start_tag = '' _fields = () _field_tags = {} _multi_fields = () _is_simple = True def __init__(self, **kws): """ Base class to encode/decode special-purpose text for QR codes. This class is not meant to be used directly, except for the 'parse' method, which can be used as a constructor for the base classes. Args: **kws (dict): Input info to generate the QrSpecial object. """ cls = type(self) cls._is_simple = len(cls._field_tags) == 0 # cls._fields = signature(cls).parameters.keys() # Py3-only # first argument is always `self` if not cls._fields: cls._fields = cls.__init__.__code__.co_varnames[ 1:cls.__init__.__code__.co_argcount] self.data = {} for field in cls._fields: if field in kws: if kws[field] is not None: self.data[field] = str(kws[field]) \ if cls._is_simple or field not in cls._multi_fields \ else cls._to_list(kws[field]) else: raise NameError('Invalid keyword argument ``'.format(field)) def __repr__(self): """ Generate the human-friendly representation of the object. Returns: text (str|unicode): The representation of the object. """ cls = type(self) if cls._is_simple: lines = [ '{}: {}'.format(field, str(self.data[field])) for field in cls._fields if field in self.data] else: lines = [] for field in cls._fields: if field in self.data: if isinstance(self.data[field], list): values = self.data[field] for value in values: lines.append('{}: {}'.format(field, str(value))) else: lines.append('{}: {}'.format( field, str(self.data[field]))) text = '<{}>'.format(cls.__name__) if lines: text += '\n' + '\n'.join(lines) else: text += ': <EMPTY>' return text def __str__(self): """ Generate QR-ready text, i.e. the text to be used in QR codes. Returns: text (str|unicode): The output text. """ cls = type(self) if cls._is_simple: values = [ str(self.data[field]) for field in cls._fields if field in self.data] text = cls._start_tag + cls._sep.join(values) else: tokens = [] for field in cls._fields: if field in self.data: if isinstance(self.data[field], list): tokens.extend( [cls._token_fmt.format( cls._field_tags[field], str(value)) for value in self.data[field]]) else: tokens.append(cls._token_fmt.format( cls._field_tags[field], str(self.data[field]))) text = ( cls._start_tag + cls._sep.join(tokens) + cls._end_tag) return text def __eq__(self, other): """ Generic implementation of equality. Args: other: The object to compare to. Returns: result (bool): True if contains the same data, False otherwise. """ if isinstance(other, type(self)): return self.__dict__ == other.__dict__ else: return False def __ne__(self, other): """ Generic implementation of inequality. Args: other: The object to compare to. Returns: result (bool): False if contains the same data, True otherwise. """ return not self.__eq__(other) def __bool__(self): """ Determines if the object contains data. Returns: result (bool): True if the object contains data, False otherwise. """ return len(self.data) > 0 def __nonzero__(self): return self.__bool__()
[docs] @classmethod def from_str(cls, text, strict=True, strip=True): """ Construct a QrSpecial object from its QR-ready text. This is conceptually the inverse operation of the 'to_str' method. Args: text (str|unicode): The input text. strict (bool): Raises an error if tags are missing. strip (bool): Strip from whitespaces before parsing. Returns: obj (QrSpecial): The QrSpecial object. """ if strip: text = text.strip() kws = {} if text: if text.startswith(cls._start_tag): text = text[len(cls._start_tag):] elif strict: raise ValueError( 'Starting tag `{}` not found'.format(cls._start_tag)) if text.endswith(cls._end_tag): text = text[:-len(cls._end_tag)] elif strict and not cls._is_simple: raise ValueError( 'Ending tag `{}` not found'.format(cls._end_tag)) tokens = text.split(cls._sep) if cls._is_simple: kws = {field: token for field, token in zip(cls._fields, tokens)} else: fields = {v: k for k, v in cls._field_tags.items()} for token in tokens: tag, value = token.split(cls._kv_sep, 1) if tag in fields: if fields[tag] not in kws: kws[fields[tag]] = value \ if fields[tag] not in cls._multi_fields \ else [value] elif fields[tag] in cls._multi_fields: kws[fields[tag]].append(value) else: raise KeyError( 'Field `{}` already used'.format(fields[tag])) else: raise ValueError( 'Unknown field identifier `{}`'.format(tag)) return cls(**kws)
@staticmethod def _to_list(obj): """ Ensure that an object is a list. If the object is already a list, nothing is done. Otherwise, if it is a tuple, it is converted to a list, else a list cointaining the object (with only 1 element) is returned. Args: obj: The input object. Returns: result (list): a list-ified version of the object. """ if isinstance(obj, list): return obj elif isinstance(obj, tuple): return list(obj) else: return [obj]
[docs] @staticmethod def parse(text): """ Construct a QrSpecial-derived object from a text. This can be useful for determining whether a given input is a valid QrSpecial-derived object. Args: text (str|unicode): The input text. Returns: obj (QrSpecial): The QrSpecial-derived object. """ # Construct a QrSpecial subclasses = (QrShortMessage, QrGeolocation, QrMeCard, QrWifi) obj = QrSpecial() for cls in subclasses: try: obj = cls.from_str(text) except ValueError: pass if obj: break return obj
[docs]class QrShortMessage(QrSpecial): """ QrSpecial-derived short message (SMS). """ _start_tag = 'smsto:' _sep = ':' def __init__(self, number=None, text=None): """ Generate the QrSpecial-derived short message (SMS). Args: number (str|unicode|int): The number to send the text to. text (str|unicode): The text to send. Examples: >>> qrs = QrShortMessage('+39070653263', 'I like your code!') >>> print(repr(qrs)) <QrShortMessage> number: +39070653263 text: I like your code! >>> print(qrs) smsto:+39070653263:I like your code! >>> qrs == QrShortMessage.from_str(str(qrs)) True """ # QrSpecial.__init__(**locals()) # Py3-only kws = locals() kws.pop('self') QrSpecial.__init__(self, **kws)
[docs]class QrGeolocation(QrSpecial): """ QrSpecial-derived geolocation. """ _start_tag = 'geo:' _query_tag = '?q=' _sep = ',' def __init__(self, lat=None, lon=None, query=None): """ Generate the QrSpecial-derived short message (SMS). Args: lat (str|unicode|float): The latitude coordinate. lon (str|unicode|float): The longitude coordinate. query (str|unicode): The associated query. It is present in ZXing QR implementation, but its usage is not well documented. Examples: >>> qrs = QrGeolocation(42.989, -71.465, 'www.python.org') >>> print(repr(qrs)) <QrGeolocation> lat: 42.989 lon: -71.465 query: www.python.org >>> print(qrs) geo:42.989,-71.465?q=www.python.org >>> qrs == QrGeolocation.from_str(str(qrs)) True >>> print(repr(QrGeolocation(47.68, -122.121))) <QrGeolocation> lat: 47.68 lon: -122.121 """ if lat is None: lat = 0.0 if lon is None: lon = 0.0 # QrSpecial.__init__(**locals()) # Py3-only kws = locals() kws.pop('self') QrSpecial.__init__(self, **kws) def __str__(self): cls = type(self) text = QrSpecial.__str__(self) first_sep_pos = text.find(cls._sep) last_sep_pos = text.rfind(cls._sep) if first_sep_pos != last_sep_pos: text = ( text[:last_sep_pos] + cls._query_tag + text[last_sep_pos + len(cls._sep):]) return text
[docs] @classmethod def from_str(cls, text, strict=True, strip=True): # replace only first occurrence of the query tag text = text.replace(cls._query_tag, cls._sep, 1) return super(QrGeolocation, cls).from_str(text, strict, strip)
[docs]class QrMeCard(QrSpecial): """ QrSpecial-derived contact information (MeCard). """ _start_tag = 'MECARD:' _field_tags = dict(( ('name', 'N'), ('reading', 'SOUND'), ('tel', 'TEL'), ('telav', 'TEL-AV'), ('email', 'EMAIL'), ('memo', 'NOTE'), ('birthday', 'BDAY'), ('address', 'ADR'), ('url', 'URL'), ('nickname', 'NICKNAME'), ('company', 'ORG'), )) _multi_fields = { 'tel', 'telav', 'email', 'address', 'url', 'nickname', 'company'} def __init__(self, name=None, reading=None, tel=None, telav=None, email=None, memo=None, birthday=None, address=None, url=None, nickname=None, company=None): """ Generate the QrSpecial-derived contact information (MeCard). This is implementation contains both elements of the NTT Docomo specification (1) and the ZXing implementation (2). In particular: - (1) allow for some fields to have multiple entries. - (2) defines some additional fields, in analogy to vCard. - (1) specify the i-mode compatible bar code recognition function version; this is indicated for each field. Args: name (str|unicode): The contact name. When a field is divided by a comma (,), the first half is treated as the last name and the second half is treated as the first name. This is not enforced by this implementation. NTT Docomo v.>=1. reading (str|unicode): The name pronunciation. When a field is divided by a comma (,), the first half is treated as the last name and the second half is treated as the first name. This is not enforced by this implementation. NTT Docomo v.>=1. tel (str|unicode|iterable): The contact phone number. Multiple entries are allowed and should be inserted as a list. Expects 1 to 24 digits, but this is not enforced. NTT Docomo v.>=1. telav (str|unicode|iterable): The contact video-phone number. This is meant to specify a preferental number for video calls. Multiple entries are allowed and should be inserted as a list. Expects 1 to 24 digits, but this is not enforced. NTT Docomo v.>=2. email (str|unicode|iterable): The contact e-mail address. Multiple entries are allowed and should be inserted as a list. NTT Docomo v.>=1. memo (str|unicode): A memo text. NTT Docomo v.>=1. birthday (str|unicode|int): The contact birthday. The ISO 8601 basic format (date only) is expected: YYYYMMDD. This comply with the NTT Docomo specifications. NTT Docomo v.>=3. address (str|unicode|iterable): The contact address. The fields divided by commas (,) denote PO box, room number, house number, city, prefecture, zip code and country, in order. This is not enforced by this implementation. NTT Docomo v.>=3. url (str|unicode|iterable): The contact website. NTT Docomo v.>=3. nickname (str|unicode|iterable): The contact nickname. NTT Docomo v.>=3. company (str|unicode|iterable): The contact company. NTT Docomo v.N/A. Examples: >>> qrs = QrMeCard('Py Thon', email=('py@py.org', 'thon@py.org')) >>> print(repr(qrs)) <QrMeCard> name: Py Thon email: py@py.org email: thon@py.org >>> print(str(qrs)) MECARD:N:Py Thon;EMAIL:py@py.org;EMAIL:thon@py.org;; >>> qrs == QrMeCard.from_str(str(qrs)) True >>> qrs = QrMeCard('QrSpecial', birthday=20160904) >>> print(repr(qrs)) <QrMeCard> name: QrSpecial birthday: 20160904 >>> print(str(qrs)) MECARD:N:QrSpecial;BDAY:20160904;; See Also: https://en.wikipedia.org/wiki/MeCard https://www.nttdocomo.co.jp/english/service/developer/make /content/barcode/function/application/addressbook/index.html https://zxingnet.codeplex.com/ https://zxing.appspot.com/generator """ if birthday: birthday = str(int(birthday))[:8] # QrSpecial.__init__(**locals()) # Py3-only kws = locals() kws.pop('self') QrSpecial.__init__(self, **kws)
[docs]class QrWifi(QrSpecial): """ QrSpecial-derived WiFi network. """ _start_tag = 'WIFI:' _field_tags = dict(( ('ssid', 'S'), ('security', 'T'), ('password', 'P'), ('hidden', 'H'), )) _modes = 'WEP', 'WPA', 'WPA2' def __init__(self, ssid=None, security=None, password=None, hidden=None): """ Generate the QrSpecial-derived WiFi network. This is useful for sharing WiFi network authentication information. Args: ssid (str|unicode): The SSID of the WiFi network. security (str|unicode|None): The security standard to use. Accepted values are: [WEP|WPA|WPA2] Note that `WPA2` is not defined in the standard but it is accepted by the ZXing implementation. If None, no encryption is used. password (str|unicode): The password of the WiFi network. If security is None, it is forced to be empty. hidden (bool): Determine the SSID broadcast mode. If True, the SSID is not expected to be broadcast. Examples: >>> qrs = QrWifi('Python', 'WEP', 'Monty', True) >>> print(repr(qrs)) <QrWifi> ssid: Python security: WEP password: Monty hidden: true >>> print(str(qrs)) WIFI:S:Python;T:WEP;P:Monty;H:true;; >>> qrs == QrWifi.from_str(str(qrs)) True >>> qrs = QrWifi('Python', 'WEP', 'Monty') >>> print(repr(qrs)) <QrWifi> ssid: Python security: WEP password: Monty >>> print(str(qrs)) WIFI:S:Python;T:WEP;P:Monty;; >>> with warnings.catch_warnings(record=True) as w: ... qrs = QrWifi('Python', 'X', 'Monty') ... assert(len(w) == 1) ... 'Unknown WiFi security' in str(w[-1].message) True >>> print(repr(qrs)) <QrWifi> ssid: Python >>> print(str(qrs)) WIFI:S:Python;; """ if hidden: hidden = 'true' else: hidden = None if security: security = security.upper() if security not in type(self)._modes: warnings.warn('Unknown WiFi security `{}`'.format(security)) security = None if password and not security: password = None # QrSpecial.__init__(**locals()) # Py3-only kws = locals() kws.pop('self') QrSpecial.__init__(self, **kws)
if __name__ == '__main__': import doctest doctest.testmod()