#!/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()