lists.arthurdejong.org
RSS feed

python-pskc branch master updated. 1.0-14-gce96e69

[Date Prev][Date Next] [Thread Prev][Thread Next]

python-pskc branch master updated. 1.0-14-gce96e69



This is an automated email from the git hooks/post-receive script. It was
generated because a ref change was pushed to the repository containing
the project "python-pskc".

The branch, master has been updated
       via  ce96e69e8aeb311fbae44c16a12986c98e092cc9 (commit)
       via  7a56eacb2bb9a4e9d81fe5e6f9168dcf63f2940a (commit)
      from  e6f2dd4695859da116e19e176da0f88a16e29367 (commit)

Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.

- Log -----------------------------------------------------------------
https://arthurdejong.org/git/python-pskc/commit/?id=ce96e69e8aeb311fbae44c16a12986c98e092cc9

commit ce96e69e8aeb311fbae44c16a12986c98e092cc9
Author: Arthur de Jong <arthur@arthurdejong.org>
Date:   Mon Apr 2 15:34:44 2018 +0200

    Ship the script as part of the pskc package
    
    This also installs pskc2csv and pskc2pskc console script entry points as
    part of the package installation.

diff --git a/pskc/scripts/__init__.py b/pskc/scripts/__init__.py
new file mode 100644
index 0000000..0d4006e
--- /dev/null
+++ b/pskc/scripts/__init__.py
@@ -0,0 +1,21 @@
+# __init__.py - collection of command-line scripts
+# coding: utf-8
+#
+# Copyright (C) 2018 Arthur de Jong
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301 USA
+
+"""Collection of command-line scripts."""
diff --git a/pskc2csv.py b/pskc/scripts/pskc2csv.py
old mode 100755
new mode 100644
similarity index 62%
copy from pskc2csv.py
copy to pskc/scripts/pskc2csv.py
index 57dee1c..f482d5b
--- a/pskc2csv.py
+++ b/pskc/scripts/pskc2csv.py
@@ -1,9 +1,6 @@
-#!/usr/bin/env python
-# coding: utf-8
-
 # pskc2csv.py - script to convert a PSKC file to CSV
 #
-# Copyright (C) 2014-2017 Arthur de Jong
+# Copyright (C) 2014-2018 Arthur de Jong
 #
 # This library is free software; you can redistribute it and/or
 # modify it under the terms of the GNU Lesser General Public
@@ -20,43 +17,19 @@
 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 # 02110-1301 USA
 
-from __future__ import print_function
+"""Script to convert a PSKC file to CSV."""
+
 import argparse
 import base64
 import csv
 import getpass
 import operator
-import os.path
 import sys
-from binascii import a2b_hex, b2a_hex
+from binascii import b2a_hex
 
 import pskc
-
-
-version_string = '''
-pskc2csv (python-pskc) %s
-Written by Arthur de Jong.
-
-Copyright (C) 2014-2017 Arthur de Jong
-This is free software; see the source for copying conditions.  There is NO
-warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
-'''.strip() % pskc.__version__
-
-
-class VersionAction(argparse.Action):
-
-    def __init__(self, option_strings, dest,
-                 help='output version information and exit'):
-        super(VersionAction, self).__init__(
-            option_strings=option_strings,
-            dest=argparse.SUPPRESS,
-            default=argparse.SUPPRESS,
-            nargs=0,
-            help=help)
-
-    def __call__(self, parser, namespace, values, option_string=None):
-        print(version_string)
-        parser.exit()
+from pskc.scripts.util import (
+    OutputFile, VersionAction, get_key, get_password)
 
 
 epilog = '''
@@ -111,23 +84,15 @@ def get_column(key, column, encoding):
 
 
 def main():
+    """Convert a PSKC file to CSV."""
     # parse command-line arguments
     args = parser.parse_args()
     # open and parse input PSKC file
     pskcfile = pskc.PSKC(args.input)
     if args.secret:
-        if os.path.isfile(args.secret):
-            with open(args.secret, 'rb') as keyfile:
-                pskcfile.encryption.key = keyfile.read()
-        else:
-            pskcfile.encryption.key = a2b_hex(args.secret)
+        pskcfile.encryption.key = get_key(args.secret)
     elif args.password:
-        if os.path.isfile(args.password):
-            with open(args.password, 'r') as passfile:
-                passwd = passfile.read().replace('\n', '')
-            pskcfile.encryption.derive_key(passwd)
-        else:
-            pskcfile.encryption.derive_key(args.password)
+        pskcfile.encryption.derive_key(get_password(args.password))
     elif sys.stdin.isatty() and pskcfile.encryption.is_encrypted:
         # prompt for a password
         prompt = 'Password: '
@@ -136,18 +101,10 @@ def main():
         passwd = getpass.getpass(prompt)
         pskcfile.encryption.derive_key(passwd)
     # open output CSV file, write header and keys
-    output = open(args.output, 'w') if args.output else sys.stdout
-    csvfile = csv.writer(output, quoting=csv.QUOTE_MINIMAL)
-    csvfile.writerow([column[-1] for column in args.columns])
-    for key in pskcfile.keys:
-        csvfile.writerow([
-            get_column(key, column[0], args.secret_encoding)
-            for column in args.columns])
-    if args.output:
-        output.close()
-    else:
-        output.flush()
-
-
-if __name__ == '__main__':  # pragma: no cover
-    main()
+    with OutputFile(args.output) as output:
+        csvfile = csv.writer(output, quoting=csv.QUOTE_MINIMAL)
+        csvfile.writerow([column[-1] for column in args.columns])
+        for key in pskcfile.keys:
+            csvfile.writerow([
+                get_column(key, column[0], args.secret_encoding)
+                for column in args.columns])
diff --git a/pskc2pskc.py b/pskc/scripts/pskc2pskc.py
old mode 100755
new mode 100644
similarity index 63%
copy from pskc2pskc.py
copy to pskc/scripts/pskc2pskc.py
index 282f4f9..0a80a44
--- a/pskc2pskc.py
+++ b/pskc/scripts/pskc2pskc.py
@@ -1,6 +1,3 @@
-#!/usr/bin/env python
-# coding: utf-8
-
 # pskc2pskc.py - script to convert a PSKC file to another PSKC file
 #
 # Copyright (C) 2018 Arthur de Jong
@@ -20,41 +17,13 @@
 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 # 02110-1301 USA
 
-from __future__ import print_function
+"""Script to convert a PSKC file to another PSKC file."""
+
 import argparse
-import getpass
-import operator
-import os.path
-import sys
-from binascii import a2b_hex, b2a_hex
 
 import pskc
-
-
-version_string = '''
-pskc2pskc (python-pskc) %s
-Written by Arthur de Jong.
-
-Copyright (C) 2018 Arthur de Jong
-This is free software; see the source for copying conditions.  There is NO
-warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
-'''.strip() % pskc.__version__
-
-
-class VersionAction(argparse.Action):
-
-    def __init__(self, option_strings, dest,
-                 help='output version information and exit'):
-        super(VersionAction, self).__init__(
-            option_strings=option_strings,
-            dest=argparse.SUPPRESS,
-            default=argparse.SUPPRESS,
-            nargs=0,
-            help=help)
-
-    def __call__(self, parser, namespace, values, option_string=None):
-        print(version_string)
-        parser.exit()
+from pskc.scripts.util import (
+    OutputFile, VersionAction, get_key, get_password)
 
 
 epilog = '''
@@ -91,25 +60,8 @@ parser.add_argument(
     help='hex encoded encryption key or a file containing the binary key')
 
 
-def get_key(argument):
-    """Get the key from a file or a hex-encoded string."""
-    if os.path.isfile(argument):
-        with open(argument, 'rb') as keyfile:
-            return keyfile.read()
-    else:
-        return a2b_hex(argument)
-
-
-def get_password(argument):
-    """Get the password from a file or as a string."""
-    if os.path.isfile(argument):
-        with open(argument, 'r') as passfile:
-            return passfile.read().replace('\n', '')
-    else:
-        return argument
-
-
 def main():
+    """Convert a PSKC file to another PSKC file."""
     # parse command-line arguments
     args = parser.parse_args()
     # open and parse input PSKC file
@@ -120,7 +72,7 @@ def main():
     elif args.password:
         pskcfile.encryption.derive_key(get_password(args.password))
         pskcfile.encryption.remove_encryption()
-    # encrypt the file if needed
+    # encrypt the output file if needed
     if args.new_secret:
         assert not pskcfile.encryption.is_encrypted
         pskcfile.encryption.setup_preshared_key(key=get_key(args.new_secret))
@@ -128,13 +80,5 @@ def main():
         assert not pskcfile.encryption.is_encrypted
         pskcfile.encryption.setup_pbkdf2(get_password(args.new_password))
     # write output PSKC file
-    output = open(args.output, 'w') if args.output else sys.stdout
-    pskcfile.write(output)
-    if args.output:
-        output.close()
-    else:
-        output.flush()
-
-
-if __name__ == '__main__':  # pragma: no cover
-    main()
+    with OutputFile(args.output) as output:
+        pskcfile.write(output)
diff --git a/pskc/scripts/util.py b/pskc/scripts/util.py
new file mode 100644
index 0000000..c3adf54
--- /dev/null
+++ b/pskc/scripts/util.py
@@ -0,0 +1,90 @@
+# util.py - utility functions for command-line scripts
+#
+# Copyright (C) 2014-2018 Arthur de Jong
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301 USA
+
+"""Utility functions for command-line scripts."""
+
+import argparse
+import os.path
+import sys
+from binascii import a2b_hex
+
+import pskc
+
+
+version_string = '''
+%s (python-pskc) %s
+Written by Arthur de Jong.
+
+Copyright (C) 2014-2018 Arthur de Jong
+This is free software; see the source for copying conditions.  There is NO
+warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+'''.lstrip()
+
+
+class VersionAction(argparse.Action):
+    """Define --version argparse action."""
+
+    def __init__(self, option_strings, dest,
+                 help='output version information and exit'):
+        super(VersionAction, self).__init__(
+            option_strings=option_strings,
+            dest=argparse.SUPPRESS,
+            default=argparse.SUPPRESS,
+            nargs=0,
+            help=help)
+
+    def __call__(self, parser, namespace, values, option_string=None):
+        """Output version information and exit."""
+        sys.stdout.write(version_string % (parser.prog, pskc.__version__))
+        parser.exit()
+
+
+def get_key(argument):
+    """Get the key from a file or a hex-encoded string."""
+    if os.path.isfile(argument):
+        with open(argument, 'rb') as keyfile:
+            return keyfile.read()
+    else:
+        return a2b_hex(argument)
+
+
+def get_password(argument):
+    """Get the password from a file or as a string."""
+    if os.path.isfile(argument):
+        with open(argument, 'r') as passfile:
+            return passfile.read().replace('\n', '')
+    else:
+        return argument
+
+
+class OutputFile(object):
+    """Wrapper around output file to also fall back to stdout."""
+
+    def __init__(self, output):
+        self.output = output
+
+    def __enter__(self):
+        self.file = open(self.output, 'w') if self.output else sys.stdout
+        return self.file
+
+    def __exit__(self, *args):
+        if self.output:
+            self.file.close()
+        else:  # we are using stdout
+            self.file.flush()
diff --git a/pskc2csv.py b/pskc2csv.py
index 57dee1c..33cae99 100755
--- a/pskc2csv.py
+++ b/pskc2csv.py
@@ -3,7 +3,7 @@
 
 # pskc2csv.py - script to convert a PSKC file to CSV
 #
-# Copyright (C) 2014-2017 Arthur de Jong
+# Copyright (C) 2014-2018 Arthur de Jong
 #
 # This library is free software; you can redistribute it and/or
 # modify it under the terms of the GNU Lesser General Public
@@ -20,134 +20,8 @@
 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 # 02110-1301 USA
 
-from __future__ import print_function
-import argparse
-import base64
-import csv
-import getpass
-import operator
-import os.path
-import sys
-from binascii import a2b_hex, b2a_hex
-
-import pskc
-
-
-version_string = '''
-pskc2csv (python-pskc) %s
-Written by Arthur de Jong.
-
-Copyright (C) 2014-2017 Arthur de Jong
-This is free software; see the source for copying conditions.  There is NO
-warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
-'''.strip() % pskc.__version__
-
-
-class VersionAction(argparse.Action):
-
-    def __init__(self, option_strings, dest,
-                 help='output version information and exit'):
-        super(VersionAction, self).__init__(
-            option_strings=option_strings,
-            dest=argparse.SUPPRESS,
-            default=argparse.SUPPRESS,
-            nargs=0,
-            help=help)
-
-    def __call__(self, parser, namespace, values, option_string=None):
-        print(version_string)
-        parser.exit()
-
-
-epilog = '''
-supported columns:
-  id, serial, secret, counter, time_offset, time_interval, interval,
-  time_drift, issuer, manufacturer, response_length, algorithm
-And any other properties of pskc.key.Key instances.
-
-Report bugs to <python-pskc-users@lists.arthurdejong.org>.
-'''.strip()
-
-# set up command line parser
-parser = argparse.ArgumentParser(
-    formatter_class=argparse.RawDescriptionHelpFormatter,
-    description='Convert a PSKC file to CSV.', epilog=epilog)
-parser.add_argument(
-    'input', metavar='FILE', help='the PSKC file to read')
-parser.add_argument(
-    '-V', '--version', action=VersionAction)
-parser.add_argument(
-    '-o', '--output', metavar='FILE',
-    help='write CSV to file instead of stdout')
-parser.add_argument(
-    '-c', '--columns', metavar='COL:LABEL,COL,..',
-    type=lambda x: [column.split(':', 1) for column in x.split(',')],
-    help='list of columns with optional labels to export',
-    default='serial,secret,algorithm,response_length,time_interval')
-parser.add_argument(
-    '-p', '--password', '--passwd', metavar='PASS/FILE',
-    help='password to use for decryption (or read from a file)')
-parser.add_argument(
-    '-s', '--secret', metavar='KEY/FILE',
-    help='hex encoded encryption key or file containing the binary key')
-encodings = {
-    'hex': b2a_hex,
-    'base32': base64.b32encode,
-    'base64': base64.b64encode,
-}
-parser.add_argument(
-    '-e', '--secret-encoding', choices=sorted(encodings.keys()),
-    help='encoding used for outputting key material',
-    default='hex')
-
-
-def get_column(key, column, encoding):
-    """Return a string value for the given column."""
-    value = operator.attrgetter(column)(key)
-    if column == 'secret':
-        # Python 3 compatible construct for outputting a string
-        return str(encodings[encoding](value).decode())
-    return value
-
-
-def main():
-    # parse command-line arguments
-    args = parser.parse_args()
-    # open and parse input PSKC file
-    pskcfile = pskc.PSKC(args.input)
-    if args.secret:
-        if os.path.isfile(args.secret):
-            with open(args.secret, 'rb') as keyfile:
-                pskcfile.encryption.key = keyfile.read()
-        else:
-            pskcfile.encryption.key = a2b_hex(args.secret)
-    elif args.password:
-        if os.path.isfile(args.password):
-            with open(args.password, 'r') as passfile:
-                passwd = passfile.read().replace('\n', '')
-            pskcfile.encryption.derive_key(passwd)
-        else:
-            pskcfile.encryption.derive_key(args.password)
-    elif sys.stdin.isatty() and pskcfile.encryption.is_encrypted:
-        # prompt for a password
-        prompt = 'Password: '
-        if pskcfile.encryption.key_name:
-            prompt = '%s: ' % pskcfile.encryption.key_name
-        passwd = getpass.getpass(prompt)
-        pskcfile.encryption.derive_key(passwd)
-    # open output CSV file, write header and keys
-    output = open(args.output, 'w') if args.output else sys.stdout
-    csvfile = csv.writer(output, quoting=csv.QUOTE_MINIMAL)
-    csvfile.writerow([column[-1] for column in args.columns])
-    for key in pskcfile.keys:
-        csvfile.writerow([
-            get_column(key, column[0], args.secret_encoding)
-            for column in args.columns])
-    if args.output:
-        output.close()
-    else:
-        output.flush()
+from pskc.scripts import pskc2csv
 
 
 if __name__ == '__main__':  # pragma: no cover
-    main()
+    pskc2csv.main()
diff --git a/pskc2pskc.py b/pskc2pskc.py
index 282f4f9..d882a0c 100755
--- a/pskc2pskc.py
+++ b/pskc2pskc.py
@@ -20,121 +20,8 @@
 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 # 02110-1301 USA
 
-from __future__ import print_function
-import argparse
-import getpass
-import operator
-import os.path
-import sys
-from binascii import a2b_hex, b2a_hex
-
-import pskc
-
-
-version_string = '''
-pskc2pskc (python-pskc) %s
-Written by Arthur de Jong.
-
-Copyright (C) 2018 Arthur de Jong
-This is free software; see the source for copying conditions.  There is NO
-warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
-'''.strip() % pskc.__version__
-
-
-class VersionAction(argparse.Action):
-
-    def __init__(self, option_strings, dest,
-                 help='output version information and exit'):
-        super(VersionAction, self).__init__(
-            option_strings=option_strings,
-            dest=argparse.SUPPRESS,
-            default=argparse.SUPPRESS,
-            nargs=0,
-            help=help)
-
-    def __call__(self, parser, namespace, values, option_string=None):
-        print(version_string)
-        parser.exit()
-
-
-epilog = '''
-supported columns:
-  id, serial, secret, counter, time_offset, time_interval, interval,
-  time_drift, issuer, manufacturer, response_length, algorithm
-And any other properties of pskc.key.Key instances.
-
-Report bugs to <python-pskc-users@lists.arthurdejong.org>.
-'''.strip()
-
-# set up command line parser
-parser = argparse.ArgumentParser(
-    formatter_class=argparse.RawDescriptionHelpFormatter,
-    description='Convert a PSKC file to another PSKC file.', epilog=epilog)
-parser.add_argument(
-    'input', metavar='FILE', help='the PSKC file to read')
-parser.add_argument(
-    '-V', '--version', action=VersionAction)
-parser.add_argument(
-    '-o', '--output', metavar='FILE',
-    help='write PSKC to file instead of stdout')
-parser.add_argument(
-    '-p', '--password', '--passwd', metavar='PASS/FILE',
-    help='password to use for decryption (or read from a file)')
-parser.add_argument(
-    '-s', '--secret', metavar='KEY/FILE',
-    help='hex encoded encryption key or a file containing the binary key')
-parser.add_argument(
-    '--new-password', '--new-passwd', metavar='PASS/FILE',
-    help='password to use for encryption (or read from a file)')
-parser.add_argument(
-    '--new-secret', metavar='KEY/FILE',
-    help='hex encoded encryption key or a file containing the binary key')
-
-
-def get_key(argument):
-    """Get the key from a file or a hex-encoded string."""
-    if os.path.isfile(argument):
-        with open(argument, 'rb') as keyfile:
-            return keyfile.read()
-    else:
-        return a2b_hex(argument)
-
-
-def get_password(argument):
-    """Get the password from a file or as a string."""
-    if os.path.isfile(argument):
-        with open(argument, 'r') as passfile:
-            return passfile.read().replace('\n', '')
-    else:
-        return argument
-
-
-def main():
-    # parse command-line arguments
-    args = parser.parse_args()
-    # open and parse input PSKC file
-    pskcfile = pskc.PSKC(args.input)
-    if args.secret:
-        pskcfile.encryption.key = get_key(args.secret)
-        pskcfile.encryption.remove_encryption()
-    elif args.password:
-        pskcfile.encryption.derive_key(get_password(args.password))
-        pskcfile.encryption.remove_encryption()
-    # encrypt the file if needed
-    if args.new_secret:
-        assert not pskcfile.encryption.is_encrypted
-        pskcfile.encryption.setup_preshared_key(key=get_key(args.new_secret))
-    elif args.new_password:
-        assert not pskcfile.encryption.is_encrypted
-        pskcfile.encryption.setup_pbkdf2(get_password(args.new_password))
-    # write output PSKC file
-    output = open(args.output, 'w') if args.output else sys.stdout
-    pskcfile.write(output)
-    if args.output:
-        output.close()
-    else:
-        output.flush()
+from pskc.scripts import pskc2pskc
 
 
 if __name__ == '__main__':  # pragma: no cover
-    main()
+    pskc2pskc.main()
diff --git a/setup.cfg b/setup.cfg
index d1d7b30..77d3c1f 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -14,7 +14,7 @@ doctest-extension=doctest
 doctest-options=+IGNORE_EXCEPTION_DETAIL
 with-coverage=true
 cover-branches=true
-cover-package=pskc,pskc2csv,pskc2pskc
+cover-package=pskc
 cover-inclusive=true
 cover-erase=true
 cover-html=true
@@ -29,5 +29,6 @@ builder = html man
 ignore =
   D105  # Missing docstring in magic method
   D107  # Missing docstring in __init__
+  Q001  # Use of ''' multiline strings
 max-complexity = 14
 max-line-length = 78
diff --git a/setup.py b/setup.py
index dfb71c7..261db78 100755
--- a/setup.py
+++ b/setup.py
@@ -2,7 +2,7 @@
 
 # setup.py - python-pskc installation script
 #
-# Copyright (C) 2014-2017 Arthur de Jong
+# Copyright (C) 2014-2018 Arthur de Jong
 #
 # This library is free software; you can redistribute it and/or
 # modify it under the terms of the GNU Lesser General Public
@@ -70,8 +70,14 @@ setup(
     packages=find_packages(),
     install_requires=['cryptography', 'python-dateutil'],
     extras_require={
-        'lxml':  ['lxml'],
-        'defuse':  ['defusedxml'],
-        'signature': ['signxml']
+        'lxml': ['lxml'],
+        'defuse': ['defusedxml'],
+        'signature': ['signxml'],
+    },
+    entry_points={
+        'console_scripts': [
+            'pskc2csv = pskc.scripts.pskc2csv:main',
+            'pskc2pskc = pskc.scripts.pskc2pskc:main',
+        ],
     },
 )
diff --git a/tests/test_pskc2csv.doctest b/tests/test_pskc2csv.doctest
index a97e297..8c049ad 100644
--- a/tests/test_pskc2csv.doctest
+++ b/tests/test_pskc2csv.doctest
@@ -25,7 +25,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 >>> import tempfile
 
 >>> from pskc import PSKC
->>> from pskc2csv import main
+>>> from pskc.scripts.pskc2csv import main
 
 
 Sadly we cannot test --help and --version properly because argparse calls
diff --git a/tests/test_pskc2pskc.doctest b/tests/test_pskc2pskc.doctest
index f404469..c1a91bb 100644
--- a/tests/test_pskc2pskc.doctest
+++ b/tests/test_pskc2pskc.doctest
@@ -24,7 +24,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 >>> import tempfile
 
 >>> from pskc import PSKC
->>> from pskc2pskc import main
+>>> from pskc.scripts.pskc2pskc import main
 
 
 Sadly we cannot test --help and --version properly because argparse calls
diff --git a/tox.ini b/tox.ini
index 7a3e049..a3e48eb 100644
--- a/tox.ini
+++ b/tox.ini
@@ -32,7 +32,7 @@ deps = flake8
        flake8-tidy-imports
        flake8-tuple
        pep8-naming
-commands = flake8 pskc *.py
+commands = flake8 pskc
 
 [testenv:docs]
 basepython = python

https://arthurdejong.org/git/python-pskc/commit/?id=7a56eacb2bb9a4e9d81fe5e6f9168dcf63f2940a

commit 7a56eacb2bb9a4e9d81fe5e6f9168dcf63f2940a
Author: Arthur de Jong <arthur@arthurdejong.org>
Date:   Sat Mar 3 16:56:33 2018 +0100

    Support setting key sub-properties via add_key()

diff --git a/pskc/__init__.py b/pskc/__init__.py
index d2e8868..358dbbd 100644
--- a/pskc/__init__.py
+++ b/pskc/__init__.py
@@ -97,14 +97,10 @@ class PSKC(object):
         The device is initialised with properties from the provided keyword
         arguments if any.
         """
-        from pskc.device import Device
+        from pskc.device import Device, update_attributes
         device = Device(self)
         self.devices.append(device)
-        # assign the kwargs as key properties
-        for k, v in kwargs.items():
-            if not hasattr(device, k):
-                raise AttributeError()
-            setattr(device, k, v)
+        update_attributes(device, **kwargs)
         return device
 
     def add_key(self, **kwargs):
@@ -113,13 +109,10 @@ class PSKC(object):
         The new key is initialised with properties from the provided keyword
         arguments if any.
         """
+        from pskc.device import update_attributes
         device = self.add_device()
         key = device.add_key()
-        # assign the kwargs as key properties
-        for k, v in kwargs.items():
-            if not hasattr(key, k):
-                raise AttributeError()
-            setattr(key, k, v)
+        update_attributes(key, **kwargs)
         return key
 
     def write(self, filename):
diff --git a/pskc/device.py b/pskc/device.py
index ec2eb64..fc29413 100644
--- a/pskc/device.py
+++ b/pskc/device.py
@@ -1,7 +1,7 @@
 # device.py - module for handling device info from pskc files
 # coding: utf-8
 #
-# Copyright (C) 2016 Arthur de Jong
+# Copyright (C) 2016-2018 Arthur de Jong
 #
 # This library is free software; you can redistribute it and/or
 # modify it under the terms of the GNU Lesser General Public
@@ -21,6 +21,17 @@
 """Module that handles device information stored in PSKC files."""
 
 
+def update_attributes(obj, **kwargs):
+    """Update object with provided properties."""
+    for k, v in kwargs.items():
+        k = k.split('.') if '.' in k else k.split('__')
+        o = obj
+        for name in k[:-1]:
+            o = getattr(o, name)
+        getattr(o, k[-1])  # raise exception for non-existing properties
+        setattr(o, k[-1], v)
+
+
 class Device(object):
     """Representation of a single key from a PSKC file.
 
@@ -62,9 +73,5 @@ class Device(object):
         from pskc.key import Key
         key = Key(self)
         self.keys.append(key)
-        # assign the kwargs as key properties
-        for k, v in kwargs.items():
-            if not hasattr(key, k):
-                raise AttributeError()
-            setattr(key, k, v)
+        update_attributes(key, **kwargs)
         return key
diff --git a/tests/test_misc.doctest b/tests/test_misc.doctest
index 246ece1..a036712 100644
--- a/tests/test_misc.doctest
+++ b/tests/test_misc.doctest
@@ -104,6 +104,20 @@ Setting secret, counter, etc. also works
 10
 
 
+We can set policy properties in two ways: policy.property or
+policy__property:
+
+>>> data = {
+...     'policy__start_date': datetime.datetime(2018, 3, 2, 10, 12, 16),
+...     'policy.expiry_date': datetime.datetime(2018, 3, 3, 16, 37, 21),
+... }
+>>> key = pskc.add_key(**data)
+>>> key.policy.start_date
+datetime.datetime(2018, 3, 2, 10, 12, 16)
+>>> key.policy.expiry_date
+datetime.datetime(2018, 3, 3, 16, 37, 21)
+
+
 Setting encryption key name and algorithm also works.
 
 >>> pskc.encryption.key_name = 'Test encryption key'

-----------------------------------------------------------------------

Summary of changes:
 pskc/__init__.py                          |  15 +---
 pskc/device.py                            |  19 +++--
 pskc/{crypto => scripts}/__init__.py      |   6 +-
 pskc2csv.py => pskc/scripts/pskc2csv.py   |  75 ++++-------------
 pskc2pskc.py => pskc/scripts/pskc2pskc.py |  72 ++--------------
 pskc/scripts/util.py                      |  90 ++++++++++++++++++++
 pskc2csv.py                               | 132 +-----------------------------
 pskc2pskc.py                              | 117 +-------------------------
 setup.cfg                                 |   3 +-
 setup.py                                  |  14 +++-
 tests/test_misc.doctest                   |  14 ++++
 tests/test_pskc2csv.doctest               |   2 +-
 tests/test_pskc2pskc.doctest              |   2 +-
 tox.ini                                   |   2 +-
 14 files changed, 168 insertions(+), 395 deletions(-)
 copy pskc/{crypto => scripts}/__init__.py (85%)
 copy pskc2csv.py => pskc/scripts/pskc2csv.py (62%)
 mode change 100755 => 100644
 copy pskc2pskc.py => pskc/scripts/pskc2pskc.py (63%)
 mode change 100755 => 100644
 create mode 100644 pskc/scripts/util.py


hooks/post-receive
-- 
python-pskc
-- 
To unsubscribe send an email to
python-pskc-commits-unsubscribe@lists.arthurdejong.org or see
https://lists.arthurdejong.org/python-pskc-commits/