From 009889300c7e5a9ed6a4f40cf61514133cf76679 Mon Sep 17 00:00:00 2001 From: Tony Flury Date: Sat, 21 May 2016 11:07:20 +0100 Subject: [PATCH 1/6] Initial implementation of cui.DateTime --- nhspy/cui.py | 95 +++++++++++++++++++++++++++ tests/test_cui.py | 159 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 254 insertions(+) create mode 100644 nhspy/cui.py create mode 100755 tests/test_cui.py diff --git a/nhspy/cui.py b/nhspy/cui.py new file mode 100644 index 0000000..0a63101 --- /dev/null +++ b/nhspy/cui.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python +# coding=utf-8 +""" +# nhspy : Implementation of cui + +Summary : + +Use Case : + As a I want So that + +Testable Statements : + Can I + .... +""" + +__version__ = "0.1" +__author__ = 'Tony Flury : anthony.flury@btinternet.com' +__created__ = '21 May 2016' + +from datetime import datetime +from numbers import Real +from collections import Callable + +class _Core(object): + """ _Core mixin - a place for common cui functionality + All Cui Data types must inherit from _Core + """ + pass + + def __format__(self, format_spec): + raise NotImplemented("All cui data types must implement their own __format__ method if their other baseclass does not support it") + + +class DateTime(datetime, _Core): + """Date class - supports all the normal date/tme functions, and nhs cui formatting""" + + def __new__(cls, initial=None): + """ Create a CUI compliant DateTime Object, from the initial value + initial : Either + numeric - a timestamps of seconds since 1970-01-01 00:00 + datetime - a value derived from the datetime module + string - a text value in nhs standard format (e.g. 01-Jan-1970 01:20) + callable - a function to be called on construction (so a date time can be used as a default) + The Callable can return None, numeric, String, or datetime as above + if initial is not provided - defaults to now() + """ + if isinstance(initial, Callable): + initial = initial() + + if initial is None: + initial_date = datetime.now() + + if isinstance(initial, Real): + initial_date = datetime.utcfromtimestamp(initial) + + if isinstance(initial, basestring): + initial_date = datetime.strptime( initial, '%d-%b-%Y %H:%M') + + if isinstance(initial, datetime): + initial_date = initial + + return datetime.__new__(cls, initial_date.year, initial_date.month, initial_date.day, + initial_date.hour, initial_date.minute, initial_date.second, initial_date.microsecond, + initial_date.tzinfo) + + def __format__(self, format_spec): + """ Magic method to implement customised formatting""" + pass + + +class NHSNumber(str, _Core): + def __init__(self, number): + """ Create a CUI compliant NHSNumber - basically a string with customised formatting + + :param number: The intial number - with or without separators + """ + + def __format__(self, format_spec): + """ Magic method to implement customised formatting""" + pass + + +class Name(_Core): + def __init__(self, last_name='', first_name=''): + """ Create a CUI compliant NHSNumber - basically a string with customised formatting + + :param last_name : The person's last name + :param first_name : The person's first name + """ + pass + + + def __format__(self, format_spec): + """ Magic method to implement customised formatting""" + pass diff --git a/tests/test_cui.py b/tests/test_cui.py new file mode 100755 index 0000000..bdb2f00 --- /dev/null +++ b/tests/test_cui.py @@ -0,0 +1,159 @@ +#!/usr/bin/env python +# coding=utf-8 +""" +# nhspy : Test Suite for test_cui.py + +Summary : + +Use Case : + As a I want So that + +Testable Statements : + Can I + .... +""" + +import unittest +from datetime import datetime, timedelta + +import nhspy.cui as cui + +__version__ = "0.1" +__author__ = 'Tony Flury : anthony.flury@btinternet.com' +__created__ = '21 May 2016' + + +class TestCUIDate(unittest.TestCase): + def setUp(self): + pass + + def tearDown(self): + pass + + def test_000_001_DefaultCreation(self): + """DateTime object with default parameters is created correctly""" + now = datetime.now() + cui_datetime = cui.DateTime() + self.assertIsInstance(cui_datetime, cui.DateTime) + self.assertLessEqual(cui_datetime-now, timedelta(milliseconds=100)) + + def test_010_001_CreateFromTimeStamp(self): + """DateTime object can be created from a valid timestamp""" + ts = 1000.0 + cui_datetime = cui.DateTime( initial=ts) + ts_date = datetime.utcfromtimestamp(ts) + self.assertEqual(cui_datetime, ts_date) + + def test_010_002_CreateFromInvalidTimeStamp(self): + """DateTime object cannot be created from a invalid timestamp""" + ts = -62135596800 # 00:00 1st Jan 1 AD + ts = ts - 86400 + with self.assertRaises(ValueError): + cui_datetime = cui.DateTime(initial=ts) + print cui_datetime + + def test_020_001_CreateFromStringNoLeadingZero(self): + """DateTime Object created from a string in CUI format - day of month > 10""" + date_str = '17-Aug-1966 08:40' + dt = datetime(year=1966, month=8, day=17, hour=8, minute=40, second=0, microsecond=0, tzinfo=None) + cui_datetime = cui.DateTime(initial=date_str) + self.assertEqual(cui_datetime, dt) + + def test_020_002_CreateFromStringLeadingZero(self): + """DateTime Object created from a string in CUI format - Leading zero day of month""" + date_str = '07-Aug-1966 08:40' + dt = datetime(year=1966, month=8, day=7, hour=8, minute=40, second=0, microsecond=0, tzinfo=None) + cui_datetime = cui.DateTime(initial=date_str) + self.assertEqual(cui_datetime, dt) + + def test_020_003_CreateFromStringInvalidDay(self): + """DateTime Object created from a string in CUI format - Invalid day of month""" + date_str = '32-Aug-1966 08:40' + with self.assertRaises(ValueError): + cui_datetime = cui.DateTime(initial=date_str) + + def test_020_004_CreateFromStringInvalidMonth(self): + """DateTime Object created from a string in CUI format - Invalid month""" + date_str = '01-Bed-1966 08:40' + with self.assertRaises(ValueError): + cui_datetime = cui.DateTime(initial=date_str) + + def test_020_005_CreateFromStringInvalidYear(self): + """DateTime Object created from a string in CUI format - Invalid year""" + date_str = '01-Aug-ABCD 08:40' + with self.assertRaises(ValueError): + cui_datetime = cui.DateTime(initial=date_str) + + def test_020_006_CreateFromStringInvalidHour(self): + """DateTime Object created from a string in CUI format - Invalid Hour""" + date_str = '01-Aug-1966 25:40' + with self.assertRaises(ValueError): + cui_datetime = cui.DateTime(initial=date_str) + + def test_020_007_CreateFromStringInvalidHour(self): + """DateTime Object created from a string in CUI format - Invalid Minute""" + date_str = '01-Aug-1966 08:64' + with self.assertRaises(ValueError): + cui_datetime = cui.DateTime(initial=date_str) + + def test_030_001_CreateFromStandardDateTime(self): + """DateTime Object created from a datetime.datetime object from the standard Libary""" + then = datetime.now() + timedelta(days=1) + cui_datetime = cui.DateTime(initial=then) + self.assertEqual(cui_datetime, then) + + def test_040_001_CreateFromCallableReturningNone(self): + """DateTime object can be created from a callable returning None""" + def callable(): + return None + + now = datetime.now() + cui_datetime = cui.DateTime(initial=callable) + self.assertIsInstance(cui_datetime, cui.DateTime) + self.assertLessEqual(cui_datetime - now, timedelta(milliseconds=100)) + + def test_040_002_CreateFromCallableReturningTimestamp(self): + """DateTime object can be created from a callable returning a Timestamp""" + + def callable(): + return 86460 # 02-Jan-1970 00:01 (UTC) + + dt = datetime(year=1970, month=1, day=2, hour=0, minute=1, second=0, microsecond=0,tzinfo=None) + cui_datetime = cui.DateTime(initial=callable) + self.assertEqual(cui_datetime, dt) + + def test_040_003_CreateFromCallableReturningString(self): + """DateTime object can be created from a callable returning a String""" + + def callable(): + return '17-Aug-1966 08:40' + + dt = datetime(year=1966, month=8, day=17, hour=8, minute=40, second=0, microsecond=0, tzinfo=None) + cui_datetime = cui.DateTime(initial=callable) + self.assertEqual(cui_datetime, dt) + + def test_040_004_CreateFromCallableReturningDateTime(self): + """DateTime object can be created from a callable returning a standard Library Date Time""" + + def callable(): + return datetime.now() + timedelta(hours=3) + + dt = callable() + cui_datetime = cui.DateTime(initial=callable) + self.assertLessEqual(cui_datetime - dt, timedelta(microseconds=100)) + +def load_tests(loader, tests=None, pattern=None): + classes = [TestCUIDate] + suite = unittest.TestSuite() + for test_class in classes: + tests = loader.loadTestsFromTestCase(test_class) + suite.addTests(tests) + return suite + + +if __name__ == '__main__': + ldr = unittest.TestLoader() + + test_suite = load_tests(ldr) + + unittest.TextTestRunner(verbosity=2).run(test_suite) From 7d0c2bb36a7de1e0c3983d78cf8fb5cad22d3c95 Mon Sep 17 00:00:00 2001 From: Tony Flury Date: Sat, 21 May 2016 13:03:51 +0100 Subject: [PATCH 2/6] Initial implementation of cui.DateTime - now with formatting. --- nhspy/cui.py | 34 +++++++++++++++++++++++++++++++--- tests/test_cui.py | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 3 deletions(-) diff --git a/nhspy/cui.py b/nhspy/cui.py index 0a63101..8985ba6 100644 --- a/nhspy/cui.py +++ b/nhspy/cui.py @@ -21,16 +21,30 @@ from numbers import Real from collections import Callable +import re + class _Core(object): """ _Core mixin - a place for common cui functionality All Cui Data types must inherit from _Core """ - pass + fmt_spec = re.compile( + r""" + (?x) # Allow Verbose + ( + (?P.?) # Optional Fill Character + (?P[<>^]?) # Optional Align Character + ) + (?P\d*?) # Optional Width specifier + v # Format type is v + """) def __format__(self, format_spec): raise NotImplemented("All cui data types must implement their own __format__ method if their other baseclass does not support it") + def _split_format_spec(self, format_spec): + pass + class DateTime(datetime, _Core): """Date class - supports all the normal date/tme functions, and nhs cui formatting""" @@ -44,6 +58,9 @@ def __new__(cls, initial=None): The Callable can return None, numeric, String, or datetime as above if initial is not provided - defaults to now() """ + + initial_date = None + if isinstance(initial, Callable): initial = initial() @@ -59,14 +76,25 @@ def __new__(cls, initial=None): if isinstance(initial, datetime): initial_date = initial + if initial_date is None: + raise ValueError('Invalid value for initial argument - must be a numeric, string, datetime, Callable or None') + return datetime.__new__(cls, initial_date.year, initial_date.month, initial_date.day, initial_date.hour, initial_date.minute, initial_date.second, initial_date.microsecond, initial_date.tzinfo) def __format__(self, format_spec): """ Magic method to implement customised formatting""" - pass - + if not format_spec: # No format spec - always return the ISO format + return str(self) + + fmt_match = DateTime.fmt_spec.match(format_spec) + if fmt_match: + val, fmt = self.strftime('%d-%b-%Y %H:%M'), format_spec[:-1] + "s" + return "{val:{fmt}}".format(fmt = fmt, val=val) + else: + val, fmt = self, format_spec + return "{val:{fmt}}".format(fmt = fmt, val=datetime(val)) class NHSNumber(str, _Core): def __init__(self, number): diff --git a/tests/test_cui.py b/tests/test_cui.py index bdb2f00..bf03e7b 100755 --- a/tests/test_cui.py +++ b/tests/test_cui.py @@ -142,6 +142,45 @@ def callable(): cui_datetime = cui.DateTime(initial=callable) self.assertLessEqual(cui_datetime - dt, timedelta(microseconds=100)) + def test_050_001_CreationInvalidValueTypeList(self): + """DateTime object cannot be created an Invalid Type - for instance a list""" + with self.assertRaises(ValueError): + cui_datetime = cui.DateTime(initial = []) + + def test_050_002_CreationInvalidValueTypeTuple(self): + """DateTime object cannot be created an Invalid Type - for instance a tuple""" + with self.assertRaises(ValueError): + cui_datetime = cui.DateTime(initial=(0,)) + + def test_050_003_CreationInvalidValueTypeDictionary(self): + """DateTime object cannot be created an Invalid Type - for instance a dictionary""" + with self.assertRaises(ValueError): + cui_datetime = cui.DateTime(initial=dict()) + + def test_060_001_FormattingISOFormat(self): + """DateTime object can be formatted as a ISO standard time""" + dt = datetime(day=17, month=8, year=1966, hour=8, minute=40, tzinfo=None) + dt_str = "{}".format(cui.DateTime(initial=dt)) + self.assertEqual(dt_str, '1966-08-17 08:40:00') + + def test_060_002_FormattingCUIFormat(self): + """DateTime object can be formatted as a CUI standard format""" + dt = datetime(day=17, month=8, year=1966, hour=8, minute=40, tzinfo=None) + dt_str = "{0:v}".format(cui.DateTime(initial=dt)) + self.assertEqual(dt_str, '17-Aug-1966 08:40') + + def test_060_003_FormattingCUIFormatPaddingAlignWidth(self): + """DateTime object can be formatted as a CUI standard format with Padding, width and alignment""" + dt = datetime(day=17, month=8, year=1966, hour=8, minute=40, tzinfo=None) + dt_str = "{0:#>20v}".format(cui.DateTime(initial=dt)) + self.assertEqual(dt_str, '###17-Aug-1966 08:40') + + def test_060_004_FormattingCUIFormatCenter(self): + """DateTime object can be formatted as a CUI standard format centered""" + dt = datetime(day=17, month=8, year=1966, hour=8, minute=40, tzinfo=None) + dt_str = "{0: ^20v}".format(cui.DateTime(initial=dt)) + self.assertEqual(dt_str, ' 17-Aug-1966 08:40 ') + def load_tests(loader, tests=None, pattern=None): classes = [TestCUIDate] suite = unittest.TestSuite() From 204d113b9099fc478351b8df025a9d8c546a01ac Mon Sep 17 00:00:00 2001 From: Tony Flury Date: Sun, 22 May 2016 01:11:55 +0100 Subject: [PATCH 3/6] Initial implementation of cui.DateTime - now with formatting. --- docs/CUI/Date_Time.rst | 84 ++++++++++++++++++++++++++++++++++++++++++ docs/usage.rst | 19 +++++++++- nhspy/cui.py | 21 ++++++----- nhspy/nhspy.py | 15 ++++++++ tests/test_cui.py | 67 ++++++++++----------------------- 5 files changed, 147 insertions(+), 59 deletions(-) create mode 100644 docs/CUI/Date_Time.rst diff --git a/docs/CUI/Date_Time.rst b/docs/CUI/Date_Time.rst new file mode 100644 index 0000000..aba723a --- /dev/null +++ b/docs/CUI/Date_Time.rst @@ -0,0 +1,84 @@ +======================== +CUI Date Time Processing +======================== + +The CUI defines the standard date time format as dd-mmm-YYYY HH:MM (eg. 08-Aug-1970 08:40). + +The CUI date/time is a class which allows dates to be manipulated as if they were python standard library datetimes, with the addition of an easy way of inputing and outputing the datetime in the standard CUI format above. + +Recognising that not every interface will produce date/times in the CUI format, it is also posible to easily create CUI standard dates/times from other input formats, such as POSIX timestamps and standard library datetime instances. + +.. warning:: + + The current implementation of the CUI Date/time class is not fully timezone aware. This **WILL** be resolved in a upcoming update. + + +DateTime Class +-------------- + +.. code-block:: python + + nhspy.cui.DateTime( initial=None ) + +The ``initial`` argument is as follows : + + - ``initial`` is None : The DateTime instance is created from the current local date/time. + - ``initial`` is a number (integer, floating point or a Decimal), then this number is a POSIX timestamp (count of seconds since 01-Jan-1970 00:00. The DateTime class will accept and process fractional timestamps correctly, although the standard CUI format displays data with a resolution of 1 minute only. + - ``initial`` is a string, then it is expected to be formatted in a CUI Date format (dd-mmm-YYYY HH:MM). If the string is not formatted in the expected format, a ValueError exception will be raised. + - ``initial`` is a Python standard library datetime, the DateTime instance will represent the same date/time as the datetime instance. + +Manipulating DateTime instances +------------------------------- + +DateTime instances can be manipulated the same as any standard library datetime. Elements can be extracted by using the standard datetime attributes : + ++-----------------+-----------------------------------------+---------------------------------+ +| Attribute | Definition | Range | ++=================+=========================================+=================================+ +| year | The year | From ``MINYEAR`` to ``MAXYEAR`` | ++-----------------+-----------------------------------------+---------------------------------+ +| month | The month number (from 1) | From 1 to 12 inclusive | ++-----------------+-----------------------------------------+---------------------------------+ +| day | The day of the month | From 1 to 31 inclusive | ++-----------------+-----------------------------------------+---------------------------------+ +| hour | The hour on the 24 hour clock | From 0 to 23 inclusive | ++-----------------+-----------------------------------------+---------------------------------+ +| minute | Minutes past the current hour | From 0 to 59 inclusive | ++-----------------+-----------------------------------------+---------------------------------+ +| second | Seconds within the current minute | From 0 to 59 inclusive | ++-----------------+-----------------------------------------+---------------------------------+ +| microsecond | microseconds with the current second | From 0 to 999999 inclusive | ++-----------------+-----------------------------------------+---------------------------------+ + +DateTime instances can be compared to standard library datetimes, and can have timedelta instances added and subtracted from them. In all cases they behave exactly the same as standard library datetimes. See the `Python Standard Library datetime module`_ for more details. + +.. _`Python Standard Library datetime module`: https://docs.python.org/2.7/library/datetime.html + +Output for CUI DateTime +----------------------- + +The DateTime class is intended to work well with the standard ``.format`` output method to allow +control over the formatting of how the information is output. + +A format type of `v` will always output the DateTime in the CUI DateTime format : + +.. code-block:: Python + + >>> from nhspy.cui import DateTime + >>> "{0:v}".format(DateTime(0)) # Create CUI Date format for 1970/01/01 00:00 (timestamp=0) + '01-Jan-1970 00:00' + +With the `v` format type the full format specifier is : + + **[[fill]align][width]v** where : + + - ``fill`` : A single fill character which is used when the output is aligned within a fixed width. Defaults to a space. + - ``align`` : One of ``>`` (right align - default) ``<`` (left align) or ``^`` (centered). + - ``width`` : An integer given the the minimum text width for the output. Note the standard format is always 17 characters long. + +If the ``v`` format is not used, then the following other output formats can be used : + + - an empty format string (i.e. no type, alignment, width or other fill character) will generate a string containing the date in ISO format : 'YYYY-mm-dd HH:MM:SS. + - The format specifier might also contain one or more datetime part format specifiers, as would be passed to ``.strftime``. See also See the `Python Standard Library datetime module - strftime & strptime behaviour`_ for more details. + +.. _`Python Standard Library datetime module - strftime & strptime behaviour`: https://docs.python.org/2.7/library/datetime.html?highlight=datetime.__format__#strftime-strptime-behavior diff --git a/docs/usage.rst b/docs/usage.rst index 155807a..2db0113 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -2,6 +2,23 @@ Usage ===== -To use NHSPy in a project:: +To use NHSPy in a project + +.. code-block::python import nhspy + +To use the CUI formats directly + +.. code-block::python + + import nhspy.cui + +or +.. code-block::python + + from nhspy import cui + +For specific information on the classes see + + - CUI Date/Time : :doc:'CUI/Date_Time` diff --git a/nhspy/cui.py b/nhspy/cui.py index 8985ba6..0a7a1d2 100644 --- a/nhspy/cui.py +++ b/nhspy/cui.py @@ -4,12 +4,15 @@ # nhspy : Implementation of cui Summary : - + Implements Common User Interface formatting for key functionality Use Case : - As a I want So that + As a Developer I want standard common user Interface Library So that I can develop applications which will + display data in a consistent manner Testable Statements : - Can I + Can I input date/time information in the CUI date format (dd-mmm-YYYY HH:MM) + Can I manipulate date/time information is manner compliant with python standard libraries + Can I output date/time information in the CUI date format (dd-mmm-YYYY HH:MM) .... """ @@ -54,16 +57,11 @@ def __new__(cls, initial=None): numeric - a timestamps of seconds since 1970-01-01 00:00 datetime - a value derived from the datetime module string - a text value in nhs standard format (e.g. 01-Jan-1970 01:20) - callable - a function to be called on construction (so a date time can be used as a default) - The Callable can return None, numeric, String, or datetime as above if initial is not provided - defaults to now() """ initial_date = None - if isinstance(initial, Callable): - initial = initial() - if initial is None: initial_date = datetime.now() @@ -86,7 +84,7 @@ def __new__(cls, initial=None): def __format__(self, format_spec): """ Magic method to implement customised formatting""" if not format_spec: # No format spec - always return the ISO format - return str(self) + return super(DateTime, self).__str__() fmt_match = DateTime.fmt_spec.match(format_spec) if fmt_match: @@ -94,7 +92,10 @@ def __format__(self, format_spec): return "{val:{fmt}}".format(fmt = fmt, val=val) else: val, fmt = self, format_spec - return "{val:{fmt}}".format(fmt = fmt, val=datetime(val)) + return super(DateTime, self).__format__(fmt) + + def __str__(self): + return "{:v}".format(self) class NHSNumber(str, _Core): def __init__(self, number): diff --git a/nhspy/nhspy.py b/nhspy/nhspy.py index 40a96af..2e8c253 100644 --- a/nhspy/nhspy.py +++ b/nhspy/nhspy.py @@ -1 +1,16 @@ # -*- coding: utf-8 -*- +""" +# nhspy : Implementation of nhspy + +Summary : + Implements a common core of functionality for use on NHS projects +Use Case : + As a Developer I want a standard common core of functionality So that I can develop applications which behave in a + consistent manner. + +Testable Statements : + Can I Input, Manipulate and Output date/times in a form that is + compliant with the CUI Date format (dd-mm-YYYY HH:MM) - See cui.py + .... +""" + diff --git a/tests/test_cui.py b/tests/test_cui.py index bf03e7b..d16428a 100755 --- a/tests/test_cui.py +++ b/tests/test_cui.py @@ -102,85 +102,57 @@ def test_030_001_CreateFromStandardDateTime(self): cui_datetime = cui.DateTime(initial=then) self.assertEqual(cui_datetime, then) - def test_040_001_CreateFromCallableReturningNone(self): - """DateTime object can be created from a callable returning None""" - def callable(): - return None - - now = datetime.now() - cui_datetime = cui.DateTime(initial=callable) - self.assertIsInstance(cui_datetime, cui.DateTime) - self.assertLessEqual(cui_datetime - now, timedelta(milliseconds=100)) - - def test_040_002_CreateFromCallableReturningTimestamp(self): - """DateTime object can be created from a callable returning a Timestamp""" - - def callable(): - return 86460 # 02-Jan-1970 00:01 (UTC) - - dt = datetime(year=1970, month=1, day=2, hour=0, minute=1, second=0, microsecond=0,tzinfo=None) - cui_datetime = cui.DateTime(initial=callable) - self.assertEqual(cui_datetime, dt) - - def test_040_003_CreateFromCallableReturningString(self): - """DateTime object can be created from a callable returning a String""" - - def callable(): - return '17-Aug-1966 08:40' - - dt = datetime(year=1966, month=8, day=17, hour=8, minute=40, second=0, microsecond=0, tzinfo=None) - cui_datetime = cui.DateTime(initial=callable) - self.assertEqual(cui_datetime, dt) - - def test_040_004_CreateFromCallableReturningDateTime(self): - """DateTime object can be created from a callable returning a standard Library Date Time""" - - def callable(): - return datetime.now() + timedelta(hours=3) - - dt = callable() - cui_datetime = cui.DateTime(initial=callable) - self.assertLessEqual(cui_datetime - dt, timedelta(microseconds=100)) - - def test_050_001_CreationInvalidValueTypeList(self): + def test_040_001_CreationInvalidValueTypeList(self): """DateTime object cannot be created an Invalid Type - for instance a list""" with self.assertRaises(ValueError): cui_datetime = cui.DateTime(initial = []) - def test_050_002_CreationInvalidValueTypeTuple(self): + def test_040_002_CreationInvalidValueTypeTuple(self): """DateTime object cannot be created an Invalid Type - for instance a tuple""" with self.assertRaises(ValueError): cui_datetime = cui.DateTime(initial=(0,)) - def test_050_003_CreationInvalidValueTypeDictionary(self): + def test_040_003_CreationInvalidValueTypeDictionary(self): """DateTime object cannot be created an Invalid Type - for instance a dictionary""" with self.assertRaises(ValueError): cui_datetime = cui.DateTime(initial=dict()) - def test_060_001_FormattingISOFormat(self): + def test_050_001_FormattingISOFormat(self): """DateTime object can be formatted as a ISO standard time""" dt = datetime(day=17, month=8, year=1966, hour=8, minute=40, tzinfo=None) dt_str = "{}".format(cui.DateTime(initial=dt)) self.assertEqual(dt_str, '1966-08-17 08:40:00') - def test_060_002_FormattingCUIFormat(self): + def test_050_002_FormattingCUIFormat(self): """DateTime object can be formatted as a CUI standard format""" dt = datetime(day=17, month=8, year=1966, hour=8, minute=40, tzinfo=None) dt_str = "{0:v}".format(cui.DateTime(initial=dt)) self.assertEqual(dt_str, '17-Aug-1966 08:40') - def test_060_003_FormattingCUIFormatPaddingAlignWidth(self): + def test_050_003_FormattingCUIFormatPaddingAlignWidth(self): """DateTime object can be formatted as a CUI standard format with Padding, width and alignment""" dt = datetime(day=17, month=8, year=1966, hour=8, minute=40, tzinfo=None) dt_str = "{0:#>20v}".format(cui.DateTime(initial=dt)) self.assertEqual(dt_str, '###17-Aug-1966 08:40') - def test_060_004_FormattingCUIFormatCenter(self): + def test_050_004_FormattingCUIFormatCenter(self): """DateTime object can be formatted as a CUI standard format centered""" dt = datetime(day=17, month=8, year=1966, hour=8, minute=40, tzinfo=None) dt_str = "{0: ^20v}".format(cui.DateTime(initial=dt)) self.assertEqual(dt_str, ' 17-Aug-1966 08:40 ') + def test_050_005_FormattingCUIDateFormatting(self): + """DateTime object can be formatted using the normal datetime format strings""" + dt = datetime(day=17, month=8, year=1966, hour=8, minute=40, tzinfo=None) + dt_str = "{0:%Y/%m/%d}".format(cui.DateTime(initial=dt)) + self.assertEqual(dt_str, '1966/08/17') + + def test_060_001_strMethod(self): + """DateTime object formatted using the str() method""" + dt = datetime(day=17, month=8, year=1966, hour=8, minute=40, tzinfo=None) + dt_str = str(cui.DateTime(initial=dt)) + self.assertEqual(dt_str, '17-Aug-1966 08:40') + def load_tests(loader, tests=None, pattern=None): classes = [TestCUIDate] suite = unittest.TestSuite() @@ -189,7 +161,6 @@ def load_tests(loader, tests=None, pattern=None): suite.addTests(tests) return suite - if __name__ == '__main__': ldr = unittest.TestLoader() From 6cf3268f72c43de700d14f0cc3a8ac8f98c309aa Mon Sep 17 00:00:00 2001 From: Tony Flury Date: Sun, 22 May 2016 10:08:24 +0100 Subject: [PATCH 4/6] PEP8 corrections, changes for Python3.4 compatibility & coverage tests --- AUTHORS.rst | 2 +- HISTORY.rst | 8 ++++++++ README.rst | 8 ++++++-- docs/CUI/Date_Time.rst | 8 +++----- docs/conf.py | 2 +- docs/history.rst | 2 ++ docs/usage.rst | 15 +++++++++----- nhspy/cui.py | 44 +++++++++++++++++++++--------------------- setup.py | 2 +- tests/test_cui.py | 17 +++++++++------- 10 files changed, 64 insertions(+), 44 deletions(-) diff --git a/AUTHORS.rst b/AUTHORS.rst index c97b4cb..b032309 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -10,4 +10,4 @@ Development Lead Contributors ------------ -None yet. Why not be the first? +* Tony Flury diff --git a/HISTORY.rst b/HISTORY.rst index fb7588d..a5fe249 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -6,3 +6,11 @@ History ------------------ * First release on PyPI. + + +0.2.0 (2016-05-22) +------------------ + +* Implementation of the `cui.DateTime`_ class. + +.. _`cui.DateTime`: CUI/Date_Time diff --git a/README.rst b/README.rst index 1383695..68bdde1 100644 --- a/README.rst +++ b/README.rst @@ -26,12 +26,16 @@ Features * CUI Formatting * NHS Number (e.g. 123 456 7890) - * Dates (e.g. 01-Jan-1970 01:20) + * Dates (e.g. 01-Jan-1970 01:20) : Initial Implementation in 0.2.0 * Patient Name (e.g. BLOGGS Joe) Credits ---------- +------- + +Software and documentation developed by : + +Matt Stibbs and Tony Flury. This package was created with Cookiecutter_ and the `audreyr/cookiecutter-pypackage`_ project template. diff --git a/docs/CUI/Date_Time.rst b/docs/CUI/Date_Time.rst index aba723a..57c8639 100644 --- a/docs/CUI/Date_Time.rst +++ b/docs/CUI/Date_Time.rst @@ -16,9 +16,7 @@ Recognising that not every interface will produce date/times in the CUI format, DateTime Class -------------- -.. code-block:: python - - nhspy.cui.DateTime( initial=None ) +.. py:function:: nhspy.cui.DateTime( initial=None ) The ``initial`` argument is as follows : @@ -70,9 +68,9 @@ A format type of `v` will always output the DateTime in the CUI DateTime format With the `v` format type the full format specifier is : - **[[fill]align][width]v** where : + ``[[fill]align][width]v`` where : - - ``fill`` : A single fill character which is used when the output is aligned within a fixed width. Defaults to a space. + - ``fill`` : A single fill character which is used when the output is aligned within a fixed width. Defaults to a space if ommitted. - ``align`` : One of ``>`` (right align - default) ``<`` (left align) or ``^`` (centered). - ``width`` : An integer given the the minimum text width for the output. Note the standard format is always 17 characters long. diff --git a/docs/conf.py b/docs/conf.py index 5341b79..b7460f8 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -56,7 +56,7 @@ # General information about the project. project = u'NHSPy' -copyright = u'2016, Matt Stibbs' +copyright = u'2016, Matt Stibbs & Tony Flury' # The version info for the project you're documenting, acts as replacement # for |version| and |release|, also used in various other places throughout diff --git a/docs/history.rst b/docs/history.rst index 2506499..dd9ea74 100644 --- a/docs/history.rst +++ b/docs/history.rst @@ -1 +1,3 @@ .. include:: ../HISTORY.rst + + diff --git a/docs/usage.rst b/docs/usage.rst index 2db0113..97f725c 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -4,21 +4,26 @@ Usage To use NHSPy in a project -.. code-block::python +.. code-block:: Python import nhspy + To use the CUI formats directly -.. code-block::python +.. code-block:: Python import nhspy.cui + or -.. code-block::python + +.. code-block:: Python from nhspy import cui -For specific information on the classes see - - CUI Date/Time : :doc:'CUI/Date_Time` +For specific information on the modules and classes see + +.. toctree:: + CUI/Date_Time diff --git a/nhspy/cui.py b/nhspy/cui.py index 0a7a1d2..3b587ca 100644 --- a/nhspy/cui.py +++ b/nhspy/cui.py @@ -16,15 +16,13 @@ .... """ -__version__ = "0.1" -__author__ = 'Tony Flury : anthony.flury@btinternet.com' -__created__ = '21 May 2016' - +import re from datetime import datetime from numbers import Real -from collections import Callable -import re +__author__ = 'Tony Flury : anthony.flury@btinternet.com' +__created__ = '21 May 2016' + class _Core(object): """ _Core mixin - a place for common cui functionality @@ -42,12 +40,11 @@ class _Core(object): """) def __format__(self, format_spec): - raise NotImplemented("All cui data types must implement their own __format__ method if their other baseclass does not support it") - - - def _split_format_spec(self, format_spec): - pass + raise NotImplemented( + "All cui data types must implement their own __format__ method if their " + "other baseclass does not support it") +# noinspection PyInitNewSignature class DateTime(datetime, _Core): """Date class - supports all the normal date/tme functions, and nhs cui formatting""" @@ -68,41 +65,45 @@ def __new__(cls, initial=None): if isinstance(initial, Real): initial_date = datetime.utcfromtimestamp(initial) - if isinstance(initial, basestring): - initial_date = datetime.strptime( initial, '%d-%b-%Y %H:%M') + if isinstance(initial, str): + initial_date = datetime.strptime(initial, '%d-%b-%Y %H:%M') if isinstance(initial, datetime): initial_date = initial if initial_date is None: - raise ValueError('Invalid value for initial argument - must be a numeric, string, datetime, Callable or None') + raise ValueError( + 'Invalid value for initial argument - must be a numeric, string, datetime, Callable or None') return datetime.__new__(cls, initial_date.year, initial_date.month, initial_date.day, - initial_date.hour, initial_date.minute, initial_date.second, initial_date.microsecond, - initial_date.tzinfo) + initial_date.hour, initial_date.minute, initial_date.second, initial_date.microsecond, + initial_date.tzinfo) def __format__(self, format_spec): """ Magic method to implement customised formatting""" - if not format_spec: # No format spec - always return the ISO format + if not format_spec: # No format spec - always return the ISO format return super(DateTime, self).__str__() fmt_match = DateTime.fmt_spec.match(format_spec) if fmt_match: - val, fmt = self.strftime('%d-%b-%Y %H:%M'), format_spec[:-1] + "s" - return "{val:{fmt}}".format(fmt = fmt, val=val) + val, fmt = self.strftime('%d-%b-%Y %H:%M'), format_spec[:-1] + "s" + return "{val:{fmt}}".format(fmt=fmt, val=val) else: - val, fmt = self, format_spec + val, fmt = self, format_spec return super(DateTime, self).__format__(fmt) def __str__(self): return "{:v}".format(self) + class NHSNumber(str, _Core): + # noinspection PyMissingConstructor def __init__(self, number): """ Create a CUI compliant NHSNumber - basically a string with customised formatting - :param number: The intial number - with or without separators + :param number: The initial number - with or without separators """ + pass def __format__(self, format_spec): """ Magic method to implement customised formatting""" @@ -118,7 +119,6 @@ def __init__(self, last_name='', first_name=''): """ pass - def __format__(self, format_spec): """ Magic method to implement customised formatting""" pass diff --git a/setup.py b/setup.py index 8591649..bcb4208 100644 --- a/setup.py +++ b/setup.py @@ -19,7 +19,7 @@ ] test_requirements = [ - # TODO: put package test requirements here + 'flake8' ] setup( diff --git a/tests/test_cui.py b/tests/test_cui.py index d16428a..6020e9b 100755 --- a/tests/test_cui.py +++ b/tests/test_cui.py @@ -23,6 +23,7 @@ __created__ = '21 May 2016' +# noinspection PyUnusedLocal class TestCUIDate(unittest.TestCase): def setUp(self): pass @@ -35,22 +36,21 @@ def test_000_001_DefaultCreation(self): now = datetime.now() cui_datetime = cui.DateTime() self.assertIsInstance(cui_datetime, cui.DateTime) - self.assertLessEqual(cui_datetime-now, timedelta(milliseconds=100)) + self.assertLessEqual(cui_datetime - now, timedelta(milliseconds=100)) def test_010_001_CreateFromTimeStamp(self): """DateTime object can be created from a valid timestamp""" ts = 1000.0 - cui_datetime = cui.DateTime( initial=ts) + cui_datetime = cui.DateTime(initial=ts) ts_date = datetime.utcfromtimestamp(ts) self.assertEqual(cui_datetime, ts_date) def test_010_002_CreateFromInvalidTimeStamp(self): """DateTime object cannot be created from a invalid timestamp""" - ts = -62135596800 # 00:00 1st Jan 1 AD - ts = ts - 86400 + ts = -62135596800 # 00:00 1st Jan 1 AD + ts -= 86400 # this should be 00:00 31st Dec 1 BC - which is outside the valid range. with self.assertRaises(ValueError): cui_datetime = cui.DateTime(initial=ts) - print cui_datetime def test_020_001_CreateFromStringNoLeadingZero(self): """DateTime Object created from a string in CUI format - day of month > 10""" @@ -97,7 +97,7 @@ def test_020_007_CreateFromStringInvalidHour(self): cui_datetime = cui.DateTime(initial=date_str) def test_030_001_CreateFromStandardDateTime(self): - """DateTime Object created from a datetime.datetime object from the standard Libary""" + """DateTime Object created from a datetime.datetime object from the standard Library""" then = datetime.now() + timedelta(days=1) cui_datetime = cui.DateTime(initial=then) self.assertEqual(cui_datetime, then) @@ -105,7 +105,7 @@ def test_030_001_CreateFromStandardDateTime(self): def test_040_001_CreationInvalidValueTypeList(self): """DateTime object cannot be created an Invalid Type - for instance a list""" with self.assertRaises(ValueError): - cui_datetime = cui.DateTime(initial = []) + cui_datetime = cui.DateTime(initial=[]) def test_040_002_CreationInvalidValueTypeTuple(self): """DateTime object cannot be created an Invalid Type - for instance a tuple""" @@ -153,6 +153,8 @@ def test_060_001_strMethod(self): dt_str = str(cui.DateTime(initial=dt)) self.assertEqual(dt_str, '17-Aug-1966 08:40') + +# noinspection PyUnusedLocal def load_tests(loader, tests=None, pattern=None): classes = [TestCUIDate] suite = unittest.TestSuite() @@ -161,6 +163,7 @@ def load_tests(loader, tests=None, pattern=None): suite.addTests(tests) return suite + if __name__ == '__main__': ldr = unittest.TestLoader() From a10a6fd4dd7a3791442ae853281e8326db525153 Mon Sep 17 00:00:00 2001 From: Tony Flury Date: Sun, 22 May 2016 10:48:53 +0100 Subject: [PATCH 5/6] Changes to testing and code to ensure compatibility with Python2.6 --- nhspy/cui.py | 2 +- tests/test_cui.py | 38 ++++++++++++++++---------------------- 2 files changed, 17 insertions(+), 23 deletions(-) diff --git a/nhspy/cui.py b/nhspy/cui.py index 3b587ca..5e2b582 100644 --- a/nhspy/cui.py +++ b/nhspy/cui.py @@ -93,7 +93,7 @@ def __format__(self, format_spec): return super(DateTime, self).__format__(fmt) def __str__(self): - return "{:v}".format(self) + return "{me:v}".format(me=self) class NHSNumber(str, _Core): diff --git a/tests/test_cui.py b/tests/test_cui.py index 6020e9b..1e590ee 100755 --- a/tests/test_cui.py +++ b/tests/test_cui.py @@ -22,6 +22,8 @@ __author__ = 'Tony Flury : anthony.flury@btinternet.com' __created__ = '21 May 2016' +# For compatibility of Python2.6 cannot use context manager version of assertRaises + # noinspection PyUnusedLocal class TestCUIDate(unittest.TestCase): @@ -35,8 +37,9 @@ def test_000_001_DefaultCreation(self): """DateTime object with default parameters is created correctly""" now = datetime.now() cui_datetime = cui.DateTime() - self.assertIsInstance(cui_datetime, cui.DateTime) - self.assertLessEqual(cui_datetime - now, timedelta(milliseconds=100)) + self.assertTrue( isinstance(cui_datetime, cui.DateTime)) # Don't use assertIsInstance due to Python 2.6 tests + self.assertTrue( isinstance(cui_datetime, datetime)) + self.assertTrue( (cui_datetime - now) <= timedelta(milliseconds=100)) def test_010_001_CreateFromTimeStamp(self): """DateTime object can be created from a valid timestamp""" @@ -49,8 +52,7 @@ def test_010_002_CreateFromInvalidTimeStamp(self): """DateTime object cannot be created from a invalid timestamp""" ts = -62135596800 # 00:00 1st Jan 1 AD ts -= 86400 # this should be 00:00 31st Dec 1 BC - which is outside the valid range. - with self.assertRaises(ValueError): - cui_datetime = cui.DateTime(initial=ts) + self.assertRaises(ValueError, cui.DateTime, initial=ts) def test_020_001_CreateFromStringNoLeadingZero(self): """DateTime Object created from a string in CUI format - day of month > 10""" @@ -69,32 +71,27 @@ def test_020_002_CreateFromStringLeadingZero(self): def test_020_003_CreateFromStringInvalidDay(self): """DateTime Object created from a string in CUI format - Invalid day of month""" date_str = '32-Aug-1966 08:40' - with self.assertRaises(ValueError): - cui_datetime = cui.DateTime(initial=date_str) + self.assertRaises(ValueError, cui.DateTime, initial=date_str) def test_020_004_CreateFromStringInvalidMonth(self): """DateTime Object created from a string in CUI format - Invalid month""" date_str = '01-Bed-1966 08:40' - with self.assertRaises(ValueError): - cui_datetime = cui.DateTime(initial=date_str) + self.assertRaises(ValueError, cui.DateTime, initial=date_str ) def test_020_005_CreateFromStringInvalidYear(self): """DateTime Object created from a string in CUI format - Invalid year""" date_str = '01-Aug-ABCD 08:40' - with self.assertRaises(ValueError): - cui_datetime = cui.DateTime(initial=date_str) + self.assertRaises(ValueError, cui.DateTime, initial=date_str ) def test_020_006_CreateFromStringInvalidHour(self): """DateTime Object created from a string in CUI format - Invalid Hour""" date_str = '01-Aug-1966 25:40' - with self.assertRaises(ValueError): - cui_datetime = cui.DateTime(initial=date_str) + self.assertRaises(ValueError, cui.DateTime, initial=date_str ) def test_020_007_CreateFromStringInvalidHour(self): """DateTime Object created from a string in CUI format - Invalid Minute""" date_str = '01-Aug-1966 08:64' - with self.assertRaises(ValueError): - cui_datetime = cui.DateTime(initial=date_str) + self.assertRaises(ValueError, cui.DateTime, initial=date_str ) def test_030_001_CreateFromStandardDateTime(self): """DateTime Object created from a datetime.datetime object from the standard Library""" @@ -104,23 +101,20 @@ def test_030_001_CreateFromStandardDateTime(self): def test_040_001_CreationInvalidValueTypeList(self): """DateTime object cannot be created an Invalid Type - for instance a list""" - with self.assertRaises(ValueError): - cui_datetime = cui.DateTime(initial=[]) + self.assertRaises(ValueError, cui.DateTime, initial=[] ) def test_040_002_CreationInvalidValueTypeTuple(self): """DateTime object cannot be created an Invalid Type - for instance a tuple""" - with self.assertRaises(ValueError): - cui_datetime = cui.DateTime(initial=(0,)) + self.assertRaises(ValueError, cui.DateTime, initial=(0,) ) def test_040_003_CreationInvalidValueTypeDictionary(self): """DateTime object cannot be created an Invalid Type - for instance a dictionary""" - with self.assertRaises(ValueError): - cui_datetime = cui.DateTime(initial=dict()) + self.assertRaises(ValueError, cui.DateTime, initial=dict() ) def test_050_001_FormattingISOFormat(self): """DateTime object can be formatted as a ISO standard time""" dt = datetime(day=17, month=8, year=1966, hour=8, minute=40, tzinfo=None) - dt_str = "{}".format(cui.DateTime(initial=dt)) + dt_str = "{0}".format(cui.DateTime(initial=dt)) self.assertEqual(dt_str, '1966-08-17 08:40:00') def test_050_002_FormattingCUIFormat(self): @@ -169,4 +163,4 @@ def load_tests(loader, tests=None, pattern=None): test_suite = load_tests(ldr) - unittest.TextTestRunner(verbosity=2).run(test_suite) + unittest.TextTestRunner(verbosity=0).run(test_suite) From 1f95a03c2d4f4c41ac80bf90c548ea128aa03881 Mon Sep 17 00:00:00 2001 From: Tony Flury Date: Sun, 22 May 2016 10:51:03 +0100 Subject: [PATCH 6/6] Changes to setup.py to give correct version number --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index bcb4208..f9ec5bd 100644 --- a/setup.py +++ b/setup.py @@ -24,7 +24,7 @@ setup( name='nhspy', - version='0.1.0', + version='0.2.0', description="A python package to help with NHS-specific functions.", long_description=readme + '\n\n' + history, author="Matt Stibbs",