diff --git a/CHANGELOG.md b/CHANGELOG.md index af9ccf5..1c8a089 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,5 @@ This project no longer uses `CHANGELOG.md` to track release changes. See the project repository release history at: https://github.com/cfpb/retirement/releases -To see the legacy version of this file, visit: https://github.com/cfpb/retirement/blob/0.7.2/CHANGELOG.md +To see the legacy version of this file, visit: https://github.com/cfpb/retirement/blob/0.11.0/CHANGELOG.md -or, from the command line: `git show 0.7.2:CHANGELOG.md` +or, from the command line: `git show 0.11.0:CHANGELOG.md` diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b7f6434..58480d5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -89,3 +89,22 @@ information visit the [Autoprefixer documentation site] - Icons: We currently use icon fonts to deliver scalable icons. Browsers that do not support icon fonts unfortunately do not receive backups but we try to always pair icons with text. + +## Style + +This project uses [`black`](https://github.com/psf/black) to format code, +[`isort`](https://github.com/timothycrosley/isort) to format imports, +and [`flake8`](https://gitlab.com/pycqa/flake8). + +You can format code and imports by calling: + +``` +black retirement_api +isort --recursive retirement_api +``` + +And you can check for style, import order, and other linting by using: + +``` +tox -e lint +``` diff --git a/README.md b/README.md index 9822d59..e56ca26 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ Tú puedes ver este app en español por poner `/es` al parte final del url. * [Bower](https://bower.io/) ### Code dependencies - * [Django 1.11](https://docs.djangoproject.com/en/1.11/) + * [Django 2.2](https://docs.djangoproject.com/en/2.2/) * [BeautifulSoup4](http://www.crummy.com/software/BeautifulSoup/bs4/doc/) * [Python-dateutil](https://dateutil.readthedocs.org/en/latest/) * [Requests](http://docs.python-requests.org/en/latest/) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..70364bd --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,24 @@ +[tool.black] +line-length = 79 +target-version = ['py36', 'py38'] +include = '\.pyi?$' +exclude = ''' +( + /( + \.eggs + | \.git + | \.tox + | \*.egg-info + | _build + | build + | dist + | migrations + | site + | \*.json + | \*.csv + )/ +) +''' + +[build-system] +requires = ["setuptools", "wheel"] diff --git a/retirement_api/admin.py b/retirement_api/admin.py index 3069ec0..537b391 100644 --- a/retirement_api/admin.py +++ b/retirement_api/admin.py @@ -1,23 +1,31 @@ #!/usr/bin/env python from django.contrib import admin -from retirement_api.models import (Page, Question, Step, - Calibration, AgeChoice, Tooltip) + +from retirement_api.models import ( + AgeChoice, + Calibration, + Page, + Question, + Step, + Tooltip, +) class PageAdmin(admin.ModelAdmin): - list_display = ('title', 'h1', 'intro') + list_display = ("title", "h1", "intro") class AgeChoiceAdmin(admin.ModelAdmin): - list_display = ('age', 'aside') + list_display = ("age", "aside") class QuestionAdmin(admin.ModelAdmin): - list_display = ('title', 'question', 'workflow_state') + list_display = ("title", "question", "workflow_state") class TooltipAdmin(admin.ModelAdmin): - list_display = ('title', 'text') + list_display = ("title", "text") + admin.site.register(Page, PageAdmin) admin.site.register(Question, QuestionAdmin) diff --git a/retirement_api/data/awi_series_2020.csv b/retirement_api/data/awi_series_2020.csv new file mode 100644 index 0000000..8b70445 --- /dev/null +++ b/retirement_api/data/awi_series_2020.csv @@ -0,0 +1,69 @@ +Year,Index +1951,2799.16 +1952,2973.32 +1953,3139.44 +1954,3155.64 +1955,3301.44 +1956,3532.36 +1957,3641.72 +1958,3673.80 +1959,3855.80 +1960,4007.12 +1961,4086.76 +1962,4291.40 +1963,4396.64 +1964,4576.32 +1965,4658.72 +1966,4938.36 +1967,5213.44 +1968,5571.76 +1969,5893.76 +1970,6186.24 +1971,6497.08 +1972,7133.80 +1973,7580.16 +1974,8030.76 +1975,8630.92 +1976,9226.48 +1977,9779.44 +1978,10556.03 +1979,11479.46 +1980,12513.46 +1981,13773.10 +1982,14531.34 +1983,15239.24 +1984,16135.07 +1985,16822.51 +1986,17321.82 +1987,18426.51 +1988,19334.04 +1989,20099.55 +1990,21027.98 +1991,21811.60 +1992,22935.42 +1993,23132.67 +1994,23753.53 +1995,24705.66 +1996,25913.90 +1997,27426.00 +1998,28861.44 +1999,30469.84 +2000,32154.82 +2001,32921.92 +2002,33252.09 +2003,34064.95 +2004,35648.55 +2005,36952.94 +2006,38651.41 +2007,40405.48 +2008,41334.97 +2009,40711.61 +2010,41673.83 +2011,42979.61 +2012,44321.67 +2013,44888.16 +2014,46481.52 +2015,48098.63 +2016,48642.15 +2017,50321.89 +2018,52145.80 diff --git a/retirement_api/data/awi_series_2020.json b/retirement_api/data/awi_series_2020.json new file mode 100644 index 0000000..da10771 --- /dev/null +++ b/retirement_api/data/awi_series_2020.json @@ -0,0 +1 @@ +{"1951": "2799.16", "1952": "2973.32", "1953": "3139.44", "1954": "3155.64", "1955": "3301.44", "1956": "3532.36", "1957": "3641.72", "1958": "3673.80", "1959": "3855.80", "1960": "4007.12", "1961": "4086.76", "1962": "4291.40", "1963": "4396.64", "1964": "4576.32", "1965": "4658.72", "1966": "4938.36", "1967": "5213.44", "1968": "5571.76", "1969": "5893.76", "1970": "6186.24", "1971": "6497.08", "1972": "7133.80", "1973": "7580.16", "1974": "8030.76", "1975": "8630.92", "1976": "9226.48", "1977": "9779.44", "1978": "10556.03", "1979": "11479.46", "1980": "12513.46", "1981": "13773.10", "1982": "14531.34", "1983": "15239.24", "1984": "16135.07", "1985": "16822.51", "1986": "17321.82", "1987": "18426.51", "1988": "19334.04", "1989": "20099.55", "1990": "21027.98", "1991": "21811.60", "1992": "22935.42", "1993": "23132.67", "1994": "23753.53", "1995": "24705.66", "1996": "25913.90", "1997": "27426.00", "1998": "28861.44", "1999": "30469.84", "2000": "32154.82", "2001": "32921.92", "2002": "33252.09", "2003": "34064.95", "2004": "35648.55", "2005": "36952.94", "2006": "38651.41", "2007": "40405.48", "2008": "41334.97", "2009": "40711.61", "2010": "41673.83", "2011": "42979.61", "2012": "44321.67", "2013": "44888.16", "2014": "46481.52", "2015": "48098.63", "2016": "48642.15", "2017": "50321.89", "2018": "52145.80"} \ No newline at end of file diff --git a/retirement_api/data/early_penalty_2020.csv b/retirement_api/data/early_penalty_2020.csv new file mode 100644 index 0000000..e27fabe --- /dev/null +++ b/retirement_api/data/early_penalty_2020.csv @@ -0,0 +1,14 @@ +YOB,FRA,reduction_months,primary_pia,primary_pct_reduction,spouse_pia,spouse_pct_reduction +1937 or earlier,65,36,$800,20.00%,$375,25.00% +1938,65 and 2 months,38,791,20.83%,370,25.83% +1939,65 and 4 months,40,783,21.67%,366,26.67% +1940,65 and 6 months,42,775,22.50%,362,27.50% +1941,65 and 8 months,44,766,23.33%,358,28.33% +1942,65 and 10 months,46,758,24.17%,354,29.17% +1943-1954,66,48,750,25.00%,350,30.00% +1955,66 and 2 months,50,741,25.83%,345,30.83% +1956,66 and 4 months,52,733,26.67%,341,31.67% +1957,66 and 6 months,54,725,27.50%,337,32.50% +1958,66 and 8 months,56,716,28.33%,333,33.33% +1959,66 and 10 months,58,708,29.17%,329,34.17% +1960 and later,67,60,700,30.00%,325,35.00% diff --git a/retirement_api/data/early_penalty_2020.json b/retirement_api/data/early_penalty_2020.json new file mode 100644 index 0000000..130262c --- /dev/null +++ b/retirement_api/data/early_penalty_2020.json @@ -0,0 +1 @@ +{"1937 or earlier": {"FRA": "65", "reduction_months": "36", "primary_pia": "$800", "primary_pct_reduction": "20.00%", "spouse_pia": "$375", "spouse_pct_reduction": "25.00%"}, "1938": {"FRA": "65 and 2 months", "reduction_months": "38", "primary_pia": "791", "primary_pct_reduction": "20.83%", "spouse_pia": "370", "spouse_pct_reduction": "25.83%"}, "1939": {"FRA": "65 and 4 months", "reduction_months": "40", "primary_pia": "783", "primary_pct_reduction": "21.67%", "spouse_pia": "366", "spouse_pct_reduction": "26.67%"}, "1940": {"FRA": "65 and 6 months", "reduction_months": "42", "primary_pia": "775", "primary_pct_reduction": "22.50%", "spouse_pia": "362", "spouse_pct_reduction": "27.50%"}, "1941": {"FRA": "65 and 8 months", "reduction_months": "44", "primary_pia": "766", "primary_pct_reduction": "23.33%", "spouse_pia": "358", "spouse_pct_reduction": "28.33%"}, "1942": {"FRA": "65 and 10 months", "reduction_months": "46", "primary_pia": "758", "primary_pct_reduction": "24.17%", "spouse_pia": "354", "spouse_pct_reduction": "29.17%"}, "1943-1954": {"FRA": "66", "reduction_months": "48", "primary_pia": "750", "primary_pct_reduction": "25.00%", "spouse_pia": "350", "spouse_pct_reduction": "30.00%"}, "1955": {"FRA": "66 and 2 months", "reduction_months": "50", "primary_pia": "741", "primary_pct_reduction": "25.83%", "spouse_pia": "345", "spouse_pct_reduction": "30.83%"}, "1956": {"FRA": "66 and 4 months", "reduction_months": "52", "primary_pia": "733", "primary_pct_reduction": "26.67%", "spouse_pia": "341", "spouse_pct_reduction": "31.67%"}, "1957": {"FRA": "66 and 6 months", "reduction_months": "54", "primary_pia": "725", "primary_pct_reduction": "27.50%", "spouse_pia": "337", "spouse_pct_reduction": "32.50%"}, "1958": {"FRA": "66 and 8 months", "reduction_months": "56", "primary_pia": "716", "primary_pct_reduction": "28.33%", "spouse_pia": "333", "spouse_pct_reduction": "33.33%"}, "1959": {"FRA": "66 and 10 months", "reduction_months": "58", "primary_pia": "708", "primary_pct_reduction": "29.17%", "spouse_pia": "329", "spouse_pct_reduction": "34.17%"}, "1960 and later": {"FRA": "67", "reduction_months": "60", "primary_pia": "700", "primary_pct_reduction": "30.00%", "spouse_pia": "325", "spouse_pct_reduction": "35.00%"}} \ No newline at end of file diff --git a/retirement_api/data/ss_cola_2020.csv b/retirement_api/data/ss_cola_2020.csv new file mode 100644 index 0000000..5103b62 --- /dev/null +++ b/retirement_api/data/ss_cola_2020.csv @@ -0,0 +1,46 @@ +Year,COLA +1975,8.0 +1976,6.4 +1977,5.9 +1978,6.5 +1979,9.9 +1980,14.3 +1981,11.2 +1982,7.4 +1983,3.5 +1984,3.5 +1985,3.1 +1986,1.3 +1987,4.2 +1988,4.0 +1989,4.7 +1990,5.4 +1991,3.7 +1992,3.0 +1993,2.6 +1994,2.8 +1995,2.6 +1996,2.9 +1997,2.1 +1998,1.3 +1999,2.5 +2000,3.5 +2001,2.6 +2002,1.4 +2003,2.1 +2004,2.7 +2005,4.1 +2006,3.3 +2007,2.3 +2008,5.8 +2009,0.0 +2010,0.0 +2011,3.6 +2012,1.7 +2013,1.5 +2014,1.7 +2015,0.0 +2016,0.3 +2017,2.0 +2018,2.8 +2019,1.6 diff --git a/retirement_api/data/ss_cola_2020.json b/retirement_api/data/ss_cola_2020.json new file mode 100644 index 0000000..400663a --- /dev/null +++ b/retirement_api/data/ss_cola_2020.json @@ -0,0 +1 @@ +{"1975": "8.0", "1976": "6.4", "1977": "5.9", "1978": "6.5", "1979": "9.9", "1980": "14.3", "1981": "11.2", "1982": "7.4", "1983": "3.5", "1984": "3.5", "1985": "3.1", "1986": "1.3", "1987": "4.2", "1988": "4.0", "1989": "4.7", "1990": "5.4", "1991": "3.7", "1992": "3.0", "1993": "2.6", "1994": "2.8", "1995": "2.6", "1996": "2.9", "1997": "2.1", "1998": "1.3", "1999": "2.5", "2000": "3.5", "2001": "2.6", "2002": "1.4", "2003": "2.1", "2004": "2.7", "2005": "4.1", "2006": "3.3", "2007": "2.3", "2008": "5.8", "2009": "0.0", "2010": "0.0", "2011": "3.6", "2012": "1.7", "2013": "1.5", "2014": "1.7", "2015": "0.0", "2016": "0.3", "2017": "2.0", "2018": "2.8", "2019": "1.6"} \ No newline at end of file diff --git a/retirement_api/management/commands/check_ssa.py b/retirement_api/management/commands/check_ssa.py index 923fa12..d97e30d 100644 --- a/retirement_api/management/commands/check_ssa.py +++ b/retirement_api/management/commands/check_ssa.py @@ -1,6 +1,8 @@ from django.core.management.base import BaseCommand + from retirement_api.utils import check_api + COMMAND_HELP = """Sends a test post to SSA's Quick Calculator \ and checks the results to make sure we're getting valid results.""" PARSER_HELP = """Specify server to use. default is 'build', \ @@ -12,10 +14,8 @@ class Command(BaseCommand): help = COMMAND_HELP def add_arguments(self, parser): - parser.add_argument('--server', - default='build', - help=PARSER_HELP) + parser.add_argument("--server", default="build", help=PARSER_HELP) def handle(self, *args, **options): - result = check_api.run(options['server']) + result = check_api.run(options["server"]) self.stdout.write(check_api.build_msg(result)) diff --git a/retirement_api/management/commands/check_ssa_values.py b/retirement_api/management/commands/check_ssa_values.py index 5503d5d..0c29f37 100644 --- a/retirement_api/management/commands/check_ssa_values.py +++ b/retirement_api/management/commands/check_ssa_values.py @@ -1,6 +1,8 @@ from django.core.management.base import BaseCommand + from retirement_api.utils import ssa_check + HELP_NOTE = """Checks a range of results from SSA's Quick Calculator \ to detect whether benefit formulas have changed.""" END_NOTE = "Checked SSA values; see results at {0}" @@ -10,12 +12,14 @@ class Command(BaseCommand): help = HELP_NOTE def add_arguments(self, parser): - parser.add_argument('--recalibrate', - action='store_true', - help='Create a new calibration file') + parser.add_argument( + "--recalibrate", + action="store_true", + help="Create a new calibration file", + ) def handle(self, *args, **options): - if options['recalibrate']: + if options["recalibrate"]: endmsg = ssa_check.run_tests(recalibrate=True) else: endmsg = ssa_check.run_tests() diff --git a/retirement_api/migrations/0001_initial.py b/retirement_api/migrations/0001_initial.py index 0d6a026..b8c1c66 100644 --- a/retirement_api/migrations/0001_initial.py +++ b/retirement_api/migrations/0001_initial.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals +import django from django.db import models, migrations @@ -84,16 +84,16 @@ class Migration(migrations.Migration): migrations.AddField( model_name='page', name='step1', - field=models.ForeignKey(related_name='step1', blank=True, to='retirement_api.Step', null=True), + field=models.ForeignKey(related_name='step1', blank=True, to='retirement_api.Step', null=True, on_delete=django.db.models.deletion.CASCADE), ), migrations.AddField( model_name='page', name='step2', - field=models.ForeignKey(related_name='step2', blank=True, to='retirement_api.Step', null=True), + field=models.ForeignKey(related_name='step2', blank=True, to='retirement_api.Step', null=True, on_delete=django.db.models.deletion.CASCADE), ), migrations.AddField( model_name='page', name='step3', - field=models.ForeignKey(related_name='step3', blank=True, to='retirement_api.Step', null=True), + field=models.ForeignKey(related_name='step3', blank=True, to='retirement_api.Step', null=True, on_delete=django.db.models.deletion.CASCADE), ), ] diff --git a/retirement_api/models.py b/retirement_api/models.py index 198d080..3388ad3 100644 --- a/retirement_api/models.py +++ b/retirement_api/models.py @@ -3,14 +3,15 @@ WORKFLOW_STATE = [ - ('APPROVED', 'Approved'), - ('REVISED', 'Revised'), - ('SUBMITTED', 'Submitted'), + ("APPROVED", "Approved"), + ("REVISED", "Revised"), + ("SUBMITTED", "Submitted"), ] class Calibration(models.Model): """Graph values for SSA test cases""" + created = models.DateTimeField(auto_now_add=True) results_json = models.TextField() @@ -26,15 +27,15 @@ class Step(models.Model): def __unicode__(self): return self.title - def trans_instructions(self, language='en'): - if language == 'es': + def trans_instructions(self, language="en"): + if language == "es": return self.instructions_es else: return self.instructions def translist(self): """returns list of fields that should be translated""" - return ['title', 'instructions'] + return ["title", "instructions"] class AgeChoice(models.Model): @@ -42,15 +43,18 @@ class AgeChoice(models.Model): aside = models.CharField(max_length=500) def get_subhed(self): - return "You've chosen age %s. %s Here are some steps \ - to help you in the next few years." % (self.age, self.aside) + return ( + "You've chosen age %s. %s Here are some steps \ + to help you in the next few years." + % (self.age, self.aside) + ) def translist(self): """returns list of fields that should be translated""" - return ['aside'] + return ["aside"] class Meta: - ordering = ['age'] + ordering = ["age"] class Page(models.Model): @@ -60,17 +64,32 @@ class Page(models.Model): h2 = models.CharField(max_length=255, blank=True) h3 = models.CharField(max_length=255, blank=True) h4 = models.CharField(max_length=255, blank=True) - step1 = models.ForeignKey(Step, related_name='step1', - blank=True, null=True) - step2 = models.ForeignKey(Step, related_name='step2', - blank=True, null=True) - step3 = models.ForeignKey(Step, related_name='step3', - blank=True, null=True) + step1 = models.ForeignKey( + Step, + related_name="step1", + blank=True, + null=True, + on_delete=models.CASCADE, + ) + step2 = models.ForeignKey( + Step, + related_name="step2", + blank=True, + null=True, + on_delete=models.CASCADE, + ) + step3 = models.ForeignKey( + Step, + related_name="step3", + blank=True, + null=True, + on_delete=models.CASCADE, + ) final_steps = models.TextField(blank=True) def translist(self): """returns list of fields that should be translated""" - return ['title', 'h1', 'intro', 'h2', 'h3', 'h4', 'final_steps'] + return ["title", "h1", "intro", "h2", "h3", "h4", "final_steps"] def __unicode__(self): return self.title @@ -82,7 +101,7 @@ class Tooltip(models.Model): def translist(self): """returns list of fields that should be translated""" - return ['text'] + return ["text"] def __unicode__(self): return self.title @@ -95,7 +114,7 @@ def __unicode__(self): '"Content-Type: text/plain; charset=UTF-8\\n"\n', '"Content-Transfer-Encoding: 8bit\\n"\n', '"Project-Id-Version: retirement\\n"\n', - '"Language: es\\n"\n\n' + '"Language: es\\n"\n\n', ] @@ -103,33 +122,40 @@ class Question(models.Model): title = models.CharField(max_length=500) slug = models.SlugField(blank=True) question = models.TextField(blank=True) - answer_yes_a_subhed = models.CharField(max_length=255, blank=True, - help_text="Under 50") + answer_yes_a_subhed = models.CharField( + max_length=255, blank=True, help_text="Under 50" + ) answer_yes_a = models.TextField(blank=True, help_text="Under 50") - answer_yes_b_subhed = models.CharField(max_length=255, blank=True, - help_text="50 and older") + answer_yes_b_subhed = models.CharField( + max_length=255, blank=True, help_text="50 and older" + ) answer_yes_b = models.TextField(blank=True, help_text="50 and older") - answer_no_a_subhed = models.CharField(max_length=255, blank=True, - help_text="Under 50") + answer_no_a_subhed = models.CharField( + max_length=255, blank=True, help_text="Under 50" + ) answer_no_a = models.TextField(blank=True, help_text="Under 50") - answer_no_b_subhed = models.CharField(max_length=255, blank=True, - help_text="50 and older") + answer_no_b_subhed = models.CharField( + max_length=255, blank=True, help_text="50 and older" + ) answer_no_b = models.TextField(blank=True, help_text="50 and older") - answer_unsure_a_subhed = models.CharField(max_length=255, blank=True, - help_text="Under 50") + answer_unsure_a_subhed = models.CharField( + max_length=255, blank=True, help_text="Under 50" + ) answer_unsure_a = models.TextField(blank=True, help_text="Under 50") - answer_unsure_b_subhed = models.CharField(max_length=255, blank=True, - help_text="50 and older") + answer_unsure_b_subhed = models.CharField( + max_length=255, blank=True, help_text="50 and older" + ) answer_unsure_b = models.TextField(blank=True, help_text="50 and older") workflow_state = models.CharField( - max_length=255, choices=WORKFLOW_STATE, default='SUBMITTED') + max_length=255, choices=WORKFLOW_STATE, default="SUBMITTED" + ) def __unicode__(self): return self.title def save(self, *args, **kwargs): if not self.slug: - self.slug = slugify(self.title).replace('-', '_') + self.slug = slugify(self.title).replace("-", "_") super(Question, self).save(*args, **kwargs) def translist(self): @@ -159,17 +185,20 @@ def dump_translation_text(self, output=False, outfile=None): or outputs a utf-8 .po file to /tmp/ """ fieldlist = self.translist() - phrases = [self.__getattribute__(attr) for attr in fieldlist if - self.__getattribute__(attr)] + phrases = [ + self.__getattribute__(attr) + for attr in fieldlist + if self.__getattribute__(attr) + ] if output is True: outfile = outfile or "/tmp/%s.po" % self.slug - with open(outfile, 'wb') as f: + with open(outfile, "wb") as f: for line in POHEADER: - f.write(line.encode('utf-8')) + f.write(line.encode("utf-8")) for phrase in phrases: - f.write('#: templates/claiming.html\n'.encode('utf-8')) - f.write(str('msgid "%s"\n' % phrase).encode('utf-8')) - f.write('msgstr ""\n\n'.encode('utf-8')) + f.write("#: templates/claiming.html\n".encode("utf-8")) + f.write(str('msgid "%s"\n' % phrase).encode("utf-8")) + f.write('msgstr ""\n\n'.encode("utf-8")) else: return phrases diff --git a/retirement_api/tests/test_commands.py b/retirement_api/tests/test_commands.py index f38e888..b1e5e40 100644 --- a/retirement_api/tests/test_commands.py +++ b/retirement_api/tests/test_commands.py @@ -1,10 +1,9 @@ -import mock import unittest - from io import StringIO from django.core.management import call_command +import mock from retirement_api.utils.check_api import collector @@ -12,22 +11,22 @@ class CommandTests(unittest.TestCase): - @mock.patch('retirement_api.management.commands.check_ssa_values.ssa_check.run_tests') + @mock.patch( + "retirement_api.management.commands.check_ssa_values.ssa_check.run_tests" # noqa: E501 + ) def test_check_ssa_values(self, mock_run_tests): - mock_run_tests.return_value = 'OK' - call_command('check_ssa_values', stdout=out) + mock_run_tests.return_value = "OK" + call_command("check_ssa_values", stdout=out) self.assertTrue(mock_run_tests.call_count == 1) - call_command('check_ssa_values', - '--recalibrate', - stdout=out) + call_command("check_ssa_values", "--recalibrate", stdout=out) self.assertTrue(mock_run_tests.call_count == 2) # mock_run_tests.return_value = 'Mismatches' # with self.assertRaises(CommandError): # call_command('check_ssa_values') - @mock.patch('retirement_api.management.commands.check_ssa.check_api.run') + @mock.patch("retirement_api.management.commands.check_ssa.check_api.run") def test_check_ssa(self, mock_run): mock_run.return_value = collector - call_command('check_ssa', stdout=out) + call_command("check_ssa", stdout=out) self.assertEqual(mock_run.call_count, 1) diff --git a/retirement_api/tests/test_models.py b/retirement_api/tests/test_models.py index 06bca92..19e0b3d 100644 --- a/retirement_api/tests/test_models.py +++ b/retirement_api/tests/test_models.py @@ -1,19 +1,20 @@ +import datetime import os import sys -import datetime import tempfile -from retirement_api.models import (AgeChoice, - Question, - Step, - Page, - Tooltip, - Calibration) -import mock -from mock import patch, mock_open - from django.test import TestCase +from retirement_api.models import ( + AgeChoice, + Calibration, + Page, + Question, + Step, + Tooltip, +) + + BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(__file__))) sys.path.append(BASE_DIR) os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings") @@ -23,14 +24,15 @@ class ViewModels(TestCase): testagechoice = AgeChoice(age=62, aside="Aside.") testquestion = Question( - title="Test Question", slug='', question="Test question.") + title="Test Question", slug="", question="Test question." + ) teststep = Step(title="Test Step") testpage = Page(title="Page title", intro="Intro") testtip = Tooltip(title="Test Tooltip") testcalibration = Calibration(created=datetime.datetime.now()) def test_calibration(self): - self.assertTrue('calibration' in self.testcalibration.__unicode__()) + self.assertTrue("calibration" in self.testcalibration.__unicode__()) def test_get_subhed(self): tc = self.testagechoice @@ -38,28 +40,32 @@ def test_get_subhed(self): def test_question_slug(self): self.testquestion.save() - self.assertTrue(self.testquestion.slug != '') + self.assertTrue(self.testquestion.slug != "") def test_question_translist(self): tlist = self.testquestion.translist() self.assertTrue(type(tlist) == list) - for term in ['question', - 'answer_yes_a', - 'answer_no_b', - 'answer_unsure_a_subhed']: + for term in [ + "question", + "answer_yes_a", + "answer_no_b", + "answer_unsure_a_subhed", + ]: self.assertTrue(term in tlist) def test_question_dump(self): with tempfile.NamedTemporaryFile() as f: self.testquestion.dump_translation_text( - output=True, - outfile=f.name + output=True, outfile=f.name ) f.seek(0) translation_po_file_content = f.read() - self.assertEqual(translation_po_file_content, (b'''\ + self.assertEqual( + translation_po_file_content, + ( + b"""\ msgid "" msgstr "" "MIME-Version: 1.0\\n" @@ -72,11 +78,13 @@ def test_question_dump(self): msgid "Test question." msgstr "" -''')) +""" + ), + ) def test_question_dump_no_output(self): dump = self.testquestion.dump_translation_text() - self.assertEqual('Test question.', dump[0]) + self.assertEqual("Test question.", dump[0]) def test_agechoice_translist(self): tlist = self.testagechoice.translist() diff --git a/retirement_api/tests/test_views.py b/retirement_api/tests/test_views.py index 50008d0..0166881 100644 --- a/retirement_api/tests/test_views.py +++ b/retirement_api/tests/test_views.py @@ -1,79 +1,78 @@ -import sys -import os import datetime import json -import mock - -from django.core.urlresolvers import reverse -from django.shortcuts import render_to_response -from django.template import RequestContext -from django.test import TestCase # import unittest from django.http import HttpRequest +from django.test import TestCase + +from retirement_api.views import ( + estimator, + get_full_retirement_age, + income_check, + param_check, +) + + +try: + from django.urls import reverse +except ImportError: + from django.core.urlresolvers import reverse -from retirement_api.views import (param_check, - income_check, - estimator, - get_full_retirement_age, - claiming, - about) -from retirement_api.utils.ss_calculator import get_retire_data today = datetime.datetime.now().date() PARAMS = { - 'dobmon': 8, - 'dobday': 14, - 'yob': 1970, - 'earnings': 70000, - 'lastYearEarn': '', # possible use for unemployed or already retired - 'lastEarn': '', # possible use for unemployed or already retired - 'retiremonth': '', # leve blank to get triple calculation -- 62, 67 and 70 - 'retireyear': '', # leve blank to get triple calculation -- 62, 67 and 70 - 'dollars': 1, # benefits to be calculated in current-year dollars - 'prgf': 2 + "dobmon": 8, + "dobday": 14, + "yob": 1970, + "earnings": 70000, + "lastYearEarn": "", # possible use for unemployed or already retired + "lastEarn": "", # possible use for unemployed or already retired + "retiremonth": "", # leve blank to get triple calculation -- 62, 67 and 70 + "retireyear": "", # leve blank to get triple calculation -- 62, 67 and 70 + "dollars": 1, # benefits to be calculated in current-year dollars + "prgf": 2, } class ViewTests(TestCase): - fixtures = ['retiredata.json'] + fixtures = ["retiredata.json"] req_good = HttpRequest() - req_good.GET['dob'] = '1955-05-05' - req_good.GET['income'] = '40000' + req_good.GET["dob"] = "1955-05-05" + req_good.GET["income"] = "40000" req_blank = HttpRequest() - req_blank.GET['dob'] = '' - req_blank.GET['income'] = '' + req_blank.GET["dob"] = "" + req_blank.GET["income"] = "" req_invalid = HttpRequest() - req_invalid.GET['dob'] = '1-2-%s' % (today.year + 5) - req_invalid.GET['income'] = 'x' - return_keys = ['data', 'error'] + req_invalid.GET["dob"] = "1-2-%s" % (today.year + 5) + req_invalid.GET["income"] = "x" + return_keys = ["data", "error"] def test_base_view(self): - url = reverse('retirement_api:claiming') + url = reverse("retirement_api:claiming") response = self.client.get(url) self.assertTrue(response.status_code == 200) - url = reverse('retirement_api:claiming_es') + url = reverse("retirement_api:claiming_es") response = self.client.get(url) self.assertTrue(response.status_code == 200) def test_param_check(self): - self.assertEqual(param_check(self.req_good, 'dob'), '1955-05-05') - self.assertEqual(param_check(self.req_good, 'income'), '40000') - self.assertEqual(param_check(self.req_blank, 'dob'), None) - self.assertEqual(param_check(self.req_blank, 'income'), None) + self.assertEqual(param_check(self.req_good, "dob"), "1955-05-05") + self.assertEqual(param_check(self.req_good, "income"), "40000") + self.assertEqual(param_check(self.req_blank, "dob"), None) + self.assertEqual(param_check(self.req_blank, "income"), None) def test_income_check(self): - self.assertEqual(income_check('544.30'), 544) - self.assertEqual(income_check('$55,000.15'), 55000) - self.assertEqual(income_check('0'), 0) - self.assertEqual(income_check('x'), None) - self.assertEqual(income_check(''), None) + self.assertEqual(income_check("544.30"), 544) + self.assertEqual(income_check("$55,000.15"), 55000) + self.assertEqual(income_check("0"), 0) + self.assertEqual(income_check("x"), None) + self.assertEqual(income_check(""), None) def test_get_full_retirement_age(self): request = self.req_blank - response = get_full_retirement_age(request, birth_year='1953') + response = get_full_retirement_age(request, birth_year="1953") self.assertTrue(json.loads(response.content) == [66, 0]) response2 = get_full_retirement_age(request, birth_year=1957) self.assertTrue(json.loads(response2.content) == [66, 6]) @@ -84,7 +83,7 @@ def test_get_full_retirement_age(self): def test_estimator_url_data(self): request = self.req_blank - response = estimator(request, dob='1955-05-05', income='40000') + response = estimator(request, dob="1955-05-05", income="40000") self.assertIsInstance(response.content, bytes) rdata = json.loads(response.content) for each in self.return_keys: @@ -92,12 +91,12 @@ def test_estimator_url_data(self): def test_estimator_url_data_bad_income(self): request = self.req_blank - response = estimator(request, dob='1955-05-05', income='z') + response = estimator(request, dob="1955-05-05", income="z") self.assertTrue(response.status_code == 400) def test_estimator_url_data_bad_dob(self): request = self.req_blank - response = estimator(request, dob='1955-05-xx', income='4000') + response = estimator(request, dob="1955-05-xx", income="4000") self.assertTrue(response.status_code == 400) def test_estimator_query_data(self): @@ -116,23 +115,23 @@ def test_estimator_query_data_blank(self): def test_estimator_query_data_blank_dob(self): request = self.req_blank - response = estimator(request, income='40000') + response = estimator(request, income="40000") self.assertTrue(response.status_code == 400) def test_estimator_query_data_blank_income(self): request = self.req_blank - response = estimator(request, dob='1955-05-05') + response = estimator(request, dob="1955-05-05") self.assertTrue(response.status_code == 400) def test_estimator_query_data_bad_income(self): request = self.req_invalid - response = estimator(request, dob='1955-05-05') + response = estimator(request, dob="1955-05-05") self.assertTrue(response.status_code == 400) def test_about_pages(self): - url = reverse('retirement_api:about') + url = reverse("retirement_api:about") response = self.client.get(url) self.assertTrue(response.status_code == 200) - url = reverse('retirement_api:about_es', kwargs={'language': 'es'}) + url = reverse("retirement_api:about_es", kwargs={"language": "es"}) response = self.client.get(url) self.assertTrue(response.status_code == 200) diff --git a/retirement_api/tests/urls.py b/retirement_api/tests/urls.py index 41a89bd..371432a 100644 --- a/retirement_api/tests/urls.py +++ b/retirement_api/tests/urls.py @@ -1,10 +1,16 @@ -from django.conf.urls import include, url -from django.contrib import admin +from django.contrib import admin import retirement_api.urls +try: + from django.urls import include, re_path +except ImportError: + from django.conf.urls import include + from django.conf.urls import url as re_path + + urlpatterns = [ - url(r'^', include(retirement_api.urls, 'retirement_api')), - url(r'^admin/', include(admin.site.urls)), + re_path(r"^admin/", admin.site.urls), + re_path(r"^", include(retirement_api.urls)), ] diff --git a/retirement_api/urls.py b/retirement_api/urls.py index d55e344..1144e8b 100644 --- a/retirement_api/urls.py +++ b/retirement_api/urls.py @@ -1,19 +1,35 @@ -from django.conf.urls import url - from retirement_api.views import about, claiming, estimator -app_name = 'retirement_api' +try: + from django.urls import re_path +except ImportError: + from django.conf.urls import url as re_path + +app_name = "retirement_api" urlpatterns = [ - url(r'^before-you-claim/about/$', about, name='about'), - url(r'^before-you-claim/about/es/$', about, {'language': 'es'}, - name='about_es'), - url(r'^before-you-claim/$', claiming, name='claiming'), - url(r'^before-you-claim/es/$', claiming, {'es': True}, name='claiming_es'), - url(r'^retirement-api/estimator/(?P[^/]+)/(?P\d+)/$', - estimator, name='estimator'), - url(r'^retirement-api/estimator/(?P[^/]+)/(?P\d+)/es/$', - estimator, {'language': 'es'}, name='estimator_es'), + re_path(r"^before-you-claim/about/$", about, name="about"), + re_path( + r"^before-you-claim/about/es/$", + about, + {"language": "es"}, + name="about_es", + ), + re_path(r"^before-you-claim/$", claiming, name="claiming"), + re_path( + r"^before-you-claim/es/$", claiming, {"es": True}, name="claiming_es" + ), + re_path( + r"^retirement-api/estimator/(?P[^/]+)/(?P\d+)/$", + estimator, + name="estimator", + ), + re_path( + r"^retirement-api/estimator/(?P[^/]+)/(?P\d+)/es/$", + estimator, + {"language": "es"}, + name="estimator_es", + ), ] diff --git a/retirement_api/utils/check_api.py b/retirement_api/utils/check_api.py index 0d68edf..a562d48 100644 --- a/retirement_api/utils/check_api.py +++ b/retirement_api/utils/check_api.py @@ -1,21 +1,23 @@ # script to check the retirement api to make sure # the SSA Quick Calculator is operational # and to log the result to a csv -import os -import sys -import requests import datetime import json import logging +import os import random import signal +import sys import time +import requests + + timestamp = datetime.datetime.now() -default_base = 'build' +default_base = "build" # rolling dob to guarantee subject is 44 and full retirement age is 67 -dob = timestamp.date().replace(year=timestamp.year-44) +dob = timestamp.date().replace(year=timestamp.year - 44) timeout_seconds = 20 API_ROOT = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) @@ -30,23 +32,19 @@ def handler(signum, frame): class Collector(object): - data = '' + data = "" date = ("{0}".format(timestamp))[:16] - domain = '' - status = '' - error = '' - note = '' - api_fail = '' - timer = '' + domain = "" + status = "" + error = "" + note = "" + api_fail = "" + timer = "" + collector = Collector() -log_header = ['data', - 'date', - 'domain', - 'status', - 'error', - 'api_fail', - 'timer'] + +log_header = ["data", "date", "domain", "status", "error", "api_fail", "timer"] def build_msg(collector): @@ -59,25 +57,27 @@ def check_data(data): always return an age, a full retirement age and a value for benefits at age 70 """ - if (data['current_age'] == 44 and - data['data']['full retirement age'] == '67' and - data['data']['benefits']['age 70']): + if ( + data["current_age"] == 44 + and data["data"]["full retirement age"] == "67" + and data["data"]["benefits"]["age 70"] + ): return "OK" else: return "BAD DATA" -prefix = 'http://' -suffix = '.consumerfinance.gov/retirement' -api_string = 'retirement-api/estimator/{0}-{1}-{2}/{3}/'.format(dob.month, - dob.day, - dob.year, - random.randrange(20000, 100000)) + +prefix = "http://" +suffix = ".consumerfinance.gov/retirement" +api_string = "retirement-api/estimator/{0}-{1}-{2}/{3}/".format( + dob.month, dob.day, dob.year, random.randrange(20000, 100000) +) BASES = { - 'unitybox': 'http://localhost:8080/retirement', - 'standalone': 'http://localhost:8000/retirement', - default_base: '{0}{1}{2}'.format(prefix, default_base, suffix), - 'prod': '{0}www{1}'.format(prefix, suffix), - } + "unitybox": "http://localhost:8080/retirement", + "standalone": "http://localhost:8000/retirement", + default_base: "{0}{1}{2}".format(prefix, default_base, suffix), + "prod": "{0}www{1}".format(prefix, suffix), +} def run(base): @@ -96,39 +96,46 @@ def run(base): end = time.time() signal.alarm(0) collector.status = "ABORTED" - collector.error = 'Server connection error' - collector.api_fail = 'FAIL' + collector.error = "Server connection error" + collector.api_fail = "FAIL" except TimeoutError: end = time.time() signal.alarm(0) collector.status = "TIMEDOUT" - collector.error = 'SSA request exceeded {0} sec'.format(timeout_seconds) + collector.error = "SSA request exceeded {0} sec".format( + timeout_seconds + ) else: if test_request.status_code != 200: signal.alarm(0) end = time.time() collector.status = "{0}".format(test_request.status_code) - collector.error = test_request.reason.replace(',', ';') - collector.api_fail = 'FAIL' + collector.error = test_request.reason.replace(",", ";") + collector.api_fail = "FAIL" else: end = time.time() signal.alarm(0) data = json.loads(test_request.text) collector.status = "%s" % test_request.status_code - collector.error = "{0}".format(data['error']).replace(',', ';').replace("'", '').replace('"', '') - collector.note = data['note'] + collector.error = ( + "{0}".format(data["error"]) + .replace(",", ";") + .replace("'", "") + .replace('"', "") + ) + collector.note = data["note"] collector.data = check_data(data) if collector.data == "BAD DATA": - collector.api_fail = 'FAIL' + collector.api_fail = "FAIL" collector.timer = "%s" % round(end - start, 1) - msg = build_msg(collector) + build_msg(collector) # print msg # with open('%s/tests/logs/api_check.log' % API_ROOT, 'a') as f: # f.write("%s\n" % msg) return collector -if __name__ == '__main__': +if __name__ == "__main__": """ runs against one of these base urls: unitybox, standalone, build, or prod; @@ -137,7 +144,7 @@ def run(base): helpmsg = "pass a base to test: unitybox, standalone, build, or prod" try: BASE = sys.argv[1] - except: + except Exception: run(default_base) else: if BASE in BASES: diff --git a/retirement_api/utils/ss_calculator.py b/retirement_api/utils/ss_calculator.py index 48a821a..95996f3 100755 --- a/retirement_api/utils/ss_calculator.py +++ b/retirement_api/utils/ss_calculator.py @@ -20,17 +20,23 @@ Outputs: - a python dictionary of benefit data and error messages """ -import re -import requests import datetime -from datetime import date import logging +import re +from datetime import date -from dateutil import parser +import requests from bs4 import BeautifulSoup as bs -from .ss_utilities import (get_retirement_age, get_current_age, past_fra_test, - get_months_until_next_birthday, - get_months_past_birthday) +from dateutil import parser + +from .ss_utilities import ( + get_current_age, + get_months_past_birthday, + get_months_until_next_birthday, + get_retirement_age, + past_fra_test, +) + TIMEOUT_SECONDS = 20 LOGGER = logging.getLogger(__name__) @@ -53,8 +59,8 @@ de sus beneficios.""" ERROR_NOTES = { - 'down': {'en': down_note, 'es': down_note_es}, - 'earnings': {'en': no_earnings_note, 'es': no_earnings_note_es} + "down": {"en": down_note, "es": down_note_es}, + "earnings": {"en": no_earnings_note, "es": no_earnings_note_es}, } PAST_NOTE = "Age {0} is past your full benefit claiming age." @@ -64,10 +70,11 @@ def get_note(note_type, language): """return language_specific error""" - if language == 'es': - return ERROR_NOTES[note_type]['es'] + if language == "es": + return ERROR_NOTES[note_type]["es"] else: - return ERROR_NOTES[note_type]['en'] + return ERROR_NOTES[note_type]["en"] + # ORIGINAL_BASE_URL = "https://www.socialsecurity.gov # NOW REDIRECTED" # QUICK_URL = "{0}/OACT/quickcalc/".format(BASE_URL) # where users go @@ -79,7 +86,8 @@ def get_note(note_type, language): def clean_comment(comment): - return comment.replace('', '').strip() + return comment.replace("", "").strip() + # calculation constants EARLY_PENALTY = 0.00555555 # monthly penalty for months closest to FRA @@ -88,13 +96,13 @@ def clean_comment(comment): ANNUAL_BONUS = 0.08 # annual bonus value for each year's delay past FRA -def num_test(value=''): +def num_test(value=""): try: int(value) - except: + except ValueError: try: int(float(value)) - except: + except ValueError: LOGGER.info("Numeric test failed for {}".format(value)) return False else: @@ -107,20 +115,21 @@ def num_test(value=''): def parse_details(rows): datad = {} if len(rows) == 3: - titlerow = rows[0].split(':') - datad[titlerow[0].strip().upper()] = {'Bend points': - titlerow[1].strip()} + titlerow = rows[0].split(":") + datad[titlerow[0].strip().upper()] = { + "Bend points": titlerow[1].strip() + } outer = datad[titlerow[0].strip().upper()] - outer['AIME'] = rows[1] - outer['COLA'] = rows[2] + outer["AIME"] = rows[1] + outer["COLA"] = rows[2] return datad def calculate_lifetime_benefits(results, base, fra_tuple, dob, past_fra): """Add lifetime benefit values for each year shown in bar graph""" - AGE = results['current_age'] - BENS = results['data']['benefits'] - LIFE = results['data']['lifetime'] = {} + AGE = results["current_age"] + BENS = results["data"]["benefits"] + LIFE = results["data"]["lifetime"] = {} for year in range(62, 71): benkey = "age {0}".format(year) lifekey = "age{0}".format(year) @@ -131,7 +140,7 @@ def calculate_lifetime_benefits(results, base, fra_tuple, dob, past_fra): max_months = (85 - year) * 12 max_benefit = max_months * bar_value if year == AGE: - month_adjustment = results['data']['months_past_birthday'] + month_adjustment = results["data"]["months_past_birthday"] if year == 62 and month_adjustment == 0 and dob.day != 2: month_adjustment = 1 life_benefit = max_benefit - (month_adjustment * bar_value) @@ -158,7 +167,7 @@ def interpolate_benefits(results, base, fra_tuple, current_age, DOB): Those born on 1st of month and have have an FRA with a month value need special handling. """ - BENS = results['data']['benefits'] + BENS = results["data"]["benefits"] # today = datetime.date.today() # current_year_bd = datetime.date(today.year, DOB.month, DOB.day) # months_past_birthday = get_months_past_birthday(DOB) @@ -171,27 +180,39 @@ def interpolate_benefits(results, base, fra_tuple, current_age, DOB): # final_months_back = 12 - months_past_birthday # fill out the missing years, working backward and forward from the FRA if fra == 67: # subject is 56 or younger, so age is not within the graph - base = BENS['age 67'] + base = BENS["age 67"] if DOB.day == 2: # the born-on-the-2nd edge case - BENS['age 62'] = int(round(base - - base * (3 * 12 * EARLY_PENALTY) - - base * (2 * 12 * EARLIER_PENALTY))) + BENS["age 62"] = int( + round( + base + - base * (3 * 12 * EARLY_PENALTY) + - base * (2 * 12 * EARLIER_PENALTY) + ) + ) else: - BENS['age 62'] = int(round(base - - base * (3 * 12 * EARLY_PENALTY) - - base * (12 * EARLIER_PENALTY) - - base * (11 * EARLIER_PENALTY))) - BENS['age 63'] = int(round(base - - base * (3 * 12 * EARLY_PENALTY) - - base * (12 * EARLIER_PENALTY))) - BENS['age 64'] = int(round(base - base * (3 * 12 * EARLY_PENALTY))) - BENS['age 65'] = int(round(base - base * (2 * 12 * EARLY_PENALTY))) - BENS['age 66'] = int(round(base - base * (1 * 12 * EARLY_PENALTY))) - BENS['age 68'] = int(round(base + (base * ANNUAL_BONUS))) - BENS['age 69'] = int(round(base + (2 * (base * ANNUAL_BONUS)))) - BENS['age 70'] = int(round(base + (3 * (base * ANNUAL_BONUS)))) + BENS["age 62"] = int( + round( + base + - base * (3 * 12 * EARLY_PENALTY) + - base * (12 * EARLIER_PENALTY) + - base * (11 * EARLIER_PENALTY) + ) + ) + BENS["age 63"] = int( + round( + base + - base * (3 * 12 * EARLY_PENALTY) + - base * (12 * EARLIER_PENALTY) + ) + ) + BENS["age 64"] = int(round(base - base * (3 * 12 * EARLY_PENALTY))) + BENS["age 65"] = int(round(base - base * (2 * 12 * EARLY_PENALTY))) + BENS["age 66"] = int(round(base - base * (1 * 12 * EARLY_PENALTY))) + BENS["age 68"] = int(round(base + (base * ANNUAL_BONUS))) + BENS["age 69"] = int(round(base + (2 * (base * ANNUAL_BONUS)))) + BENS["age 70"] = int(round(base + (3 * (base * ANNUAL_BONUS)))) elif fra == 66: # DOB is 1/1/1960 or before - base = BENS['age 66'] + base = BENS["age 66"] annual_bump = round(base * ANNUAL_BONUS) monthly_bump = base * MONTHLY_BONUS first_bump = round(monthly_bump * initial_months_forward) @@ -199,71 +220,92 @@ def interpolate_benefits(results, base, fra_tuple, current_age, DOB): earlier_monthly_penalty = base * EARLIER_PENALTY dob_month_delta = 12 - get_months_past_birthday(DOB) first_penalty = initial_months_back * monthly_penalty - BENS['age 67'] = int(base + first_bump) - BENS['age 68'] = int(base + first_bump + annual_bump) - BENS['age 69'] = int(base + first_bump + (2 * annual_bump)) - BENS['age 70'] = int(base + first_bump + (3 * annual_bump)) + BENS["age 67"] = int(base + first_bump) + BENS["age 68"] = int(base + first_bump + annual_bump) + BENS["age 69"] = int(base + first_bump + (2 * annual_bump)) + BENS["age 70"] = int(base + first_bump + (3 * annual_bump)) if current_age == 65: - BENS['age 62'] = 0 - BENS['age 63'] = 0 - BENS['age 64'] = 0 - BENS['age 65'] = int(round(base - - (dob_month_delta * monthly_penalty))) + BENS["age 62"] = 0 + BENS["age 63"] = 0 + BENS["age 64"] = 0 + BENS["age 65"] = int( + round(base - (dob_month_delta * monthly_penalty)) + ) elif current_age == 64: - BENS['age 62'] = 0 - BENS['age 63'] = 0 - BENS['age 64'] = int(round(base - - first_penalty - - (dob_month_delta * monthly_penalty))) - BENS['age 65'] = int(round(base - first_penalty)) + BENS["age 62"] = 0 + BENS["age 63"] = 0 + BENS["age 64"] = int( + round( + base - first_penalty - (dob_month_delta * monthly_penalty) + ) + ) + BENS["age 65"] = int(round(base - first_penalty)) elif current_age == 63: - BENS['age 62'] = 0 - BENS['age 63'] = int(round(base - - first_penalty - - (12 * monthly_penalty) - - (dob_month_delta * monthly_penalty))) - BENS['age 64'] = int(round(base - - first_penalty - - (12 * monthly_penalty))) - BENS['age 65'] = int(round(base - first_penalty)) + BENS["age 62"] = 0 + BENS["age 63"] = int( + round( + base + - first_penalty + - (12 * monthly_penalty) + - (dob_month_delta * monthly_penalty) + ) + ) + BENS["age 64"] = int( + round(base - first_penalty - (12 * monthly_penalty)) + ) + BENS["age 65"] = int(round(base - first_penalty)) elif current_age == 62: - BENS['age 62'] = int(round(base - - first_penalty - - (2 * 12 * monthly_penalty) - - (dob_month_delta * - earlier_monthly_penalty))) - BENS['age 63'] = int(round(base - - first_penalty - - (2 * 12 * monthly_penalty))) - BENS['age 64'] = int(round(base - - first_penalty - - (12 * monthly_penalty))) - BENS['age 65'] = int(round(base - first_penalty)) + BENS["age 62"] = int( + round( + base + - first_penalty + - (2 * 12 * monthly_penalty) + - (dob_month_delta * earlier_monthly_penalty) + ) + ) + BENS["age 63"] = int( + round(base - first_penalty - (2 * 12 * monthly_penalty)) + ) + BENS["age 64"] = int( + round(base - first_penalty - (12 * monthly_penalty)) + ) + BENS["age 65"] = int(round(base - first_penalty)) elif current_age in range(55, 62): if DOB.day == 2: - BENS['age 62'] = int(round( - base - first_penalty - - (12 * monthly_penalty) - - ((12 - fra_months) * monthly_penalty) - - (fra_months * earlier_monthly_penalty) - - (12 * earlier_monthly_penalty))) + BENS["age 62"] = int( + round( + base + - first_penalty + - (12 * monthly_penalty) + - ((12 - fra_months) * monthly_penalty) + - (fra_months * earlier_monthly_penalty) + - (12 * earlier_monthly_penalty) + ) + ) else: - BENS['age 62'] = int(round( - base - first_penalty - - (12 * monthly_penalty) - - ((12 - fra_months) * monthly_penalty) - - (fra_months * earlier_monthly_penalty) - - (11 * earlier_monthly_penalty))) - BENS['age 63'] = int(round( - base - first_penalty - - (12 * monthly_penalty) - - ((12 - fra_months) * monthly_penalty) - - (fra_months * earlier_monthly_penalty) - )) - BENS['age 64'] = int(round( - base - first_penalty - - (12 * monthly_penalty))) - BENS['age 65'] = int(round(base - first_penalty)) + BENS["age 62"] = int( + round( + base + - first_penalty + - (12 * monthly_penalty) + - ((12 - fra_months) * monthly_penalty) + - (fra_months * earlier_monthly_penalty) + - (11 * earlier_monthly_penalty) + ) + ) + BENS["age 63"] = int( + round( + base + - first_penalty + - (12 * monthly_penalty) + - ((12 - fra_months) * monthly_penalty) + - (fra_months * earlier_monthly_penalty) + ) + ) + BENS["age 64"] = int( + round(base - first_penalty - (12 * monthly_penalty)) + ) + BENS["age 65"] = int(round(base - first_penalty)) return results @@ -273,39 +315,40 @@ def interpolate_for_past_fra(results, base, current_age, dob): Handles edge case when subject's born on 1st and birthday is in same month. """ # today = datetime.date.today() - BENS = results['data']['benefits'] + BENS = results["data"]["benefits"] annual_bump = round(base * ANNUAL_BONUS) monthly_bump = base * MONTHLY_BONUS first_bump = round(monthly_bump * get_months_until_next_birthday(dob)) if get_months_past_birthday(dob) == 11 and dob.day == 1: - results['params_adjusted'] = True + results["params_adjusted"] = True current_age = current_age + 1 - results['current_age'] = current_age - results['note'] = PAST_NOTE.format(current_age) - results['data']['months_past_birthday'] = 0 + results["current_age"] = current_age + results["note"] = PAST_NOTE.format(current_age) + results["data"]["months_past_birthday"] = 0 first_bump = annual_bump if current_age == 66: - BENS['age 66'] = base - BENS['age 67'] = base + first_bump - BENS['age 68'] = base + first_bump + annual_bump - BENS['age 69'] = base + first_bump + (2 * annual_bump) - BENS['age 70'] = base + first_bump + (3 * annual_bump) + BENS["age 66"] = base + BENS["age 67"] = base + first_bump + BENS["age 68"] = base + first_bump + annual_bump + BENS["age 69"] = base + first_bump + (2 * annual_bump) + BENS["age 70"] = base + first_bump + (3 * annual_bump) elif current_age == 67: - BENS['age 67'] = base - BENS['age 68'] = base + first_bump - BENS['age 69'] = base + first_bump + annual_bump - BENS['age 70'] = base + first_bump + (2 * annual_bump) + BENS["age 67"] = base + BENS["age 68"] = base + first_bump + BENS["age 69"] = base + first_bump + annual_bump + BENS["age 70"] = base + first_bump + (2 * annual_bump) elif current_age == 68: - BENS['age 68'] = base - BENS['age 69'] = base + first_bump - BENS['age 70'] = base + first_bump + annual_bump + BENS["age 68"] = base + BENS["age 69"] = base + first_bump + BENS["age 70"] = base + first_bump + annual_bump elif current_age == 69: - BENS['age 69'] = base - BENS['age 70'] = base + first_bump + BENS["age 69"] = base + BENS["age 70"] = base + first_bump elif current_age == 70: - BENS['age 70'] = base + BENS["age 70"] = base return results + # # sample params # params = { # 'dobmon': 8, @@ -321,7 +364,7 @@ def interpolate_for_past_fra(results, base, current_age, dob): # } -def set_up_runvars(params, language='en'): +def set_up_runvars(params, language="en"): """ Set up the results container and variables @@ -331,94 +374,95 @@ def set_up_runvars(params, language='en'): to match SSA's handling of edge cases """ today = date.today() - dobstring = "{0}-{1}-{2}".format(params['yob'], - params['dobmon'], - params['dobday']) - yobstring = "{0}".format(params['yob']) + dobstring = "{0}-{1}-{2}".format( + params["yob"], params["dobmon"], params["dobday"] + ) + yobstring = "{0}".format(params["yob"]) current_age = get_current_age(dobstring) dob = parser.parse(dobstring).date() benefits = {} for age in CHART_AGES: benefits["age {0}".format(age)] = 0 - results = {'data': { - 'months_past_birthday': get_months_past_birthday(dob), - 'early retirement age': '', - 'full retirement age': '', - 'benefits': benefits, - 'params': params, - 'disability': '', - 'survivor benefits': { - 'child': '', - 'spouse caring for child': '', - 'spouse at full retirement age': '', - 'family maximum': '' - } - }, - 'current_age': current_age, - 'error': '', - 'note': '', - 'past_fra': '', - 'params_adjusted': False, - } + results = { + "data": { + "months_past_birthday": get_months_past_birthday(dob), + "early retirement age": "", + "full retirement age": "", + "benefits": benefits, + "params": params, + "disability": "", + "survivor benefits": { + "child": "", + "spouse caring for child": "", + "spouse at full retirement age": "", + "family maximum": "", + }, + }, + "current_age": current_age, + "error": "", + "note": "", + "past_fra": "", + "params_adjusted": False, + } past_fra = past_fra_test(dobstring, language=language) if isinstance(past_fra, bool) is False: return (dob, dobstring, current_age, (0, 0), past_fra, results) else: - results['past_fra'] = past_fra - ssa_params = results['data']['params'] + results["past_fra"] = past_fra + ssa_params = results["data"]["params"] if dob.day == 1: - results['params_adjusted'] = True - ssa_params['dobday'] = 2 + results["params_adjusted"] = True + ssa_params["dobday"] = 2 if dob.month == 1: - yob = ssa_params['yob'] - 1 + yob = ssa_params["yob"] - 1 yobstring = "{0}".format(yob) - ssa_params['dobmon'] = 12 - ssa_params['yob'] = yob + ssa_params["dobmon"] = 12 + ssa_params["yob"] = yob else: - ssa_params['dobmon'] = params['dobmon'] - 1 + ssa_params["dobmon"] = params["dobmon"] - 1 fra_tuple = get_retirement_age(yobstring) # returns tuple: (year, months) if fra_tuple[1]: FRA = "{0} and {1} months".format(fra_tuple[0], fra_tuple[1]) else: FRA = "{0}".format(fra_tuple[0]) - results['data']['full retirement age'] = FRA + results["data"]["full retirement age"] = FRA if past_fra is True: - ssa_params['retireyear'] = today.year - ssa_params['retiremonth'] = today.month - results['note'] = PAST_NOTE.format(current_age) - results['data']['disability'] = NO_DISABILITY_NOTE + ssa_params["retireyear"] = today.year + ssa_params["retiremonth"] = today.month + results["note"] = PAST_NOTE.format(current_age) + results["data"]["disability"] = NO_DISABILITY_NOTE else: - retire_year = ssa_params['yob'] + fra_tuple[0] - retire_month = ssa_params['dobmon'] + fra_tuple[1] + retire_year = ssa_params["yob"] + fra_tuple[0] + retire_month = ssa_params["dobmon"] + fra_tuple[1] if retire_month > 12: retire_month = retire_month - 12 retire_year += 1 - ssa_params['retireyear'] = retire_year - ssa_params['retiremonth'] = retire_month + ssa_params["retireyear"] = retire_year + ssa_params["retiremonth"] = retire_month return (dob, dobstring, current_age, fra_tuple, past_fra, results) def parse_response(results, html, language): - soup = bs(html, 'html.parser') - if soup.find('p') and 'insufficient to receive' in soup.find('p').text: - results['error'] = "benefit is zero" - results['note'] = get_note('earnings', language) + soup = bs(html, "html.parser") + if soup.find("p") and "insufficient to receive" in soup.find("p").text: + results["error"] = "benefit is zero" + results["note"] = get_note("earnings", language) return (results, 0) - ret_amount_raw = soup.find('span', {'id': 'ret_amount'}) + ret_amount_raw = soup.find("span", {"id": "ret_amount"}) if not ret_amount_raw: - results['error'] = "bad response from SSA" - results['note'] = get_note('down', language) + results["error"] = "bad response from SSA" + results["note"] = get_note("down", language) return (results, 0) - ret_amount = ret_amount_raw.text.split('.')[0].replace(',', '') + ret_amount = ret_amount_raw.text.split(".")[0].replace(",", "") base_benefit = int(ret_amount) return (results, base_benefit) def validate_date(params): """Make sure delivered date is real""" - dobstring = "{0}-{1}-{2}".format(params['yob'], - params['dobmon'], - params['dobday']) + dobstring = "{0}-{1}-{2}".format( + params["yob"], params["dobmon"], params["dobday"] + ) try: parser.parse(dobstring).date() return True @@ -442,64 +486,75 @@ def get_retire_data(params, language): - dobs in 1950 that the Quick Calculator improperly treats as past FRA. """ if not validate_date(params): - return {'error': "An invalid date was entered."} + return {"error": "An invalid date was entered."} starter = datetime.datetime.now() - (dob, dobstring, current_age, - fra_tuple, past_fra, results) = set_up_runvars(params, language=language) + ( + dob, + dobstring, + current_age, + fra_tuple, + past_fra, + results, + ) = set_up_runvars(params, language=language) if isinstance(past_fra, bool) is False: # if past_fra is neither False nor True, there's an error and we bail if current_age and current_age > 70: - results['past_fra'] = True - results['note'] = past_fra - results['error'] = "visitor too old for tool" + results["past_fra"] = True + results["note"] = past_fra + results["error"] = "visitor too old for tool" return results elif current_age is None or current_age < 22: - results['note'] = past_fra - results['error'] = "visitor too young for tool" + results["note"] = past_fra + results["error"] = "visitor too young for tool" return results - elif 'invalid' in past_fra: # pragma: no cover -- tested elsewhere - results['note'] = "An invalid date was entered." - results['error'] = past_fra + elif "invalid" in past_fra: # pragma: no cover -- tested elsewhere + results["note"] = "An invalid date was entered." + results["error"] = past_fra return results try: - req = requests.post(RESULT_URL, - data=results['data']['params'], - timeout=TIMEOUT_SECONDS) + req = requests.post( + RESULT_URL, data=results["data"]["params"], timeout=TIMEOUT_SECONDS + ) except requests.exceptions.ConnectionError as e: - results['error'] = "connection error at SSA's website: {0}".format(e) - results['note'] = get_note('down', language) + results["error"] = "connection error at SSA's website: {0}".format(e) + results["note"] = get_note("down", language) return results except requests.exceptions.Timeout: - results['error'] = "SSA's website timed out" - results['note'] = get_note('down', language) + results["error"] = "SSA's website timed out" + results["note"] = get_note("down", language) return results except requests.exceptions.RequestException as e: - results['error'] = "request error at SSA's website: {0}".format(e) - results['note'] = get_note('down', language) + results["error"] = "request error at SSA's website: {0}".format(e) + results["note"] = get_note("down", language) return results - except: - results['error'] = "Unknown error at SSA's website" - results['note'] = get_note('down', language) + except Exception: + results["error"] = "Unknown error at SSA's website" + results["note"] = get_note("down", language) return results if not req.ok: ok_msg = "SSA's website is not responding. Status code: {0} ({1})" - results['error'] = ok_msg.format(req.status_code, req.reason) - results['note'] = get_note('down', language) + results["error"] = ok_msg.format(req.status_code, req.reason) + results["note"] = get_note("down", language) return results (results, base_benefit) = parse_response(results, req.text, language) - if results['error']: + if results["error"]: return results if past_fra is True: results = interpolate_for_past_fra( - results, base_benefit, current_age, dob) + results, base_benefit, current_age, dob + ) else: - results['data']['benefits']['age {0}'.format( - fra_tuple[0])] = base_benefit + results["data"]["benefits"][ + "age {0}".format(fra_tuple[0]) + ] = base_benefit results = interpolate_benefits( - results, base_benefit, fra_tuple, current_age, dob) + results, base_benefit, fra_tuple, current_age, dob + ) final_results = calculate_lifetime_benefits( - results, base_benefit, fra_tuple, dob, past_fra) - LOGGER.info("script took {0} to run".format( - (datetime.datetime.now() - starter))) + results, base_benefit, fra_tuple, dob, past_fra + ) + LOGGER.info( + "script took {0} to run".format((datetime.datetime.now() - starter)) + ) return final_results diff --git a/retirement_api/utils/ss_update_stats.py b/retirement_api/utils/ss_update_stats.py index 219ec63..0db5a64 100755 --- a/retirement_api/utils/ss_update_stats.py +++ b/retirement_api/utils/ss_update_stats.py @@ -1,9 +1,13 @@ -import os -import sys +import csv import datetime import json -import csv import logging +import os +import sys + +import requests +from bs4 import BeautifulSoup as bs + """ terms: @@ -11,10 +15,7 @@ AIME: Average Indexed Monthly Earnings """ -import requests # from django.template.defaultfilters import slugify -from bs4 import BeautifulSoup as bs -from io import StringIO TODAY = datetime.datetime.now().date() BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(__file__))) @@ -28,42 +29,62 @@ outjson = "%s/early_penalty_%s.json" % (data_dir, TODAY.year) ss_table_urls = { - 'cola': "http://www.socialsecurity.gov/OACT/COLA/colaseries.html", - 'actuarial_life': "http://www.socialsecurity.gov/OACT/STATS/table4c6.html", - 'retirement_ages': "http://www.socialsecurity.gov/OACT/ProgData/nra.html",# handled by get_retirement_age() - 'benefit_bases': 'http://www.socialsecurity.gov/OACT/COLA/cbb.html',# not needed if we use SS calculator - 'delay_credits': "http://www.socialsecurity.gov/retire2/delayret.htm", - 'awi_series': 'http://www.socialsecurity.gov/OACT/COLA/AWI.html', - 'bend_points': 'http://www.socialsecurity.gov/OACT/COLA/bendpoints.html',# not needed if we use SS calculator - 'early_retirement_example': 'http://www.socialsecurity.gov/OACT/quickcalc/earlyretire.html',# useful as viz - 'explainer of AMI calculations': 'http://www.socialsecurity.gov/OACT/COLA/piaformula.html',# info only - 'benefit_terms': 'http://www.socialsecurity.gov/OACT/COLA/Benefits.html#aime',# explanation of terms; info only - 'credit_rules': 'http://www.socialsecurity.gov/planners/retire/credits2.html',# out of scope: rules for achieving 40 work credits (10 years of work); not envisioned for app - 'quarter_of_coverage': 'http://www.socialsecurity.gov/OACT/COLA/QC.html',# out of scope: basic work-credit unit to determine whether a worker is covered by SS; you can earn 4 credits a year - 'death_probabilities': 'http://www.socialsecurity.gov/OACT/HistEst/DeathProbabilities2014.html',# out of scope: historical and projected male/female death probability tables - 'automatic_values': 'http://www.socialsecurity.gov/OACT/COLA/autoAdj.html',# out of scope: compendium of bend points, COlA and other adjustment values used in SS calculations - } + "cola": "http://www.socialsecurity.gov/OACT/COLA/colaseries.html", + "actuarial_life": "http://www.socialsecurity.gov/OACT/STATS/table4c6.html", + # handled by get_retirement_age() + "retirement_ages": "http://www.socialsecurity.gov/OACT/ProgData/nra.html", + # not needed if we use SS calculator + "benefit_bases": "http://www.socialsecurity.gov/OACT/COLA/cbb.html", + "delay_credits": "http://www.socialsecurity.gov/retire2/delayret.htm", + "awi_series": "http://www.socialsecurity.gov/OACT/COLA/AWI.html", + # not needed if we use SS calculator + "bend_points": "http://www.socialsecurity.gov/OACT/COLA/bendpoints.html", + # useful as viz + "early_retirement_example": "http://www.socialsecurity.gov/OACT/quickcalc/earlyretire.html", # noqa: E501 + # info only + "explainer of AMI calculations": "http://www.socialsecurity.gov/OACT/COLA/piaformula.html", # noqa: E501 + # explanation of terms; info only + "benefit_terms": "http://www.socialsecurity.gov/OACT/COLA/Benefits.html#aime", # noqa: E501 + # out of scope: rules for achieving 40 work credits (10 years of work); + # not envisioned for app + "credit_rules": "http://www.socialsecurity.gov/planners/retire/credits2.html", # noqa: E501 + # out of scope: basic work-credit unit to determine whether a worker is + # covered by SS; you can earn 4 credits a year + "quarter_of_coverage": "http://www.socialsecurity.gov/OACT/COLA/QC.html", + # out of scope: + # historical and projected male/female death probability tables + "death_probabilities": "http://www.socialsecurity.gov/OACT/HistEst/DeathProbabilities2014.html", # noqa: E501 + # out of scope: compendium of bend points, + # COlA and other adjustment values used in SS calculations + "automatic_values": "http://www.socialsecurity.gov/OACT/COLA/autoAdj.html", +} log = logging.getLogger(__name__) def output_csv(filepath, headings, bs_rows): - with open(filepath, 'w') as f: + with open(filepath, "w") as f: writer = csv.writer(f) writer.writerow(headings) for row in bs_rows: - writer.writerow([cell.text.replace(',', '').strip() - for cell in row.findAll('td') - if row.findAll('td')]) + writer.writerow( + [ + cell.text.replace(",", "").strip() + for cell in row.findAll("td") + if row.findAll("td") + ] + ) def output_json(filepath, headings, bs_rows): json_out = {} for row in bs_rows: - cells = [cell.text.replace(',', '').strip() - for cell in row.findAll('td') - if row.findAll('td')] + cells = [ + cell.text.replace(",", "").strip() + for cell in row.findAll("td") + if row.findAll("td") + ] if len(cells) == 2: json_out[cells[0]] = cells[1] else: @@ -72,19 +93,19 @@ def output_json(filepath, headings, bs_rows): for tup in tups: tupd[tup[0]] = tup[1] json_out[cells[0]] = tupd - with open(filepath, 'w') as f: + with open(filepath, "w") as f: f.write(json.dumps(json_out)) def make_soup(url): req = requests.get(url) - if req.reason != 'OK': - log.warn("request to %s failed: %s %s" % (url, - req.status_code, - req.reason)) - return '' + if req.reason != "OK": + log.warn( + "request to %s failed: %s %s" % (url, req.status_code, req.reason) + ) + return "" else: - soup = bs(req.text, 'html.parser') + soup = bs(req.text, "html.parser") return soup @@ -93,20 +114,20 @@ def update_example_reduction(): SSA's example shows Primary and spousal benefits at age 62, assuming a primary insurance amount of $1,000 """ - url = ss_table_urls['early_retirement_example'] + url = ss_table_urls["early_retirement_example"] headings = [ - 'YOB', - 'FRA', - 'reduction_months', - 'primary_pia', - 'primary_pct_reduction', - 'spouse_pia', - 'spouse_pct_reduction' - ] + "YOB", + "FRA", + "reduction_months", + "primary_pia", + "primary_pct_reduction", + "spouse_pia", + "spouse_pct_reduction", + ] soup = make_soup(url) if soup: - table = soup.findAll('table')[5].find('table') - rows = [row for row in table.findAll('tr') if row.findAll('td')] + table = soup.findAll("table")[5].find("table") + rows = [row for row in table.findAll("tr") if row.findAll("td")] output_csv(outcsv, headings, rows) log.info("updated %s with %s rows" % (outcsv, len(rows))) output_json(outjson, headings, rows) @@ -114,18 +135,19 @@ def update_example_reduction(): def update_awi_series(): - url = ss_table_urls['awi_series'] + url = ss_table_urls["awi_series"] outcsv = "%s/awi_series_%s.csv" % (data_dir, TODAY.year) outjson = "%s/awi_series_%s.json" % (data_dir, TODAY.year) - headings = ['Year', 'Index'] + headings = ["Year", "Index"] soup = make_soup(url) if soup: - tables = soup.findAll('table')[1].findAll('table') + tables = soup.findAll("table")[1].findAll("table") rows = [] log.info("found %s tables" % len(tables)) for table in tables: - rows.extend([row for row in table.findAll('tr') - if row.findAll('td')]) + rows.extend( + [row for row in table.findAll("tr") if row.findAll("td")] + ) output_csv(outcsv, headings, rows) log.info("updated %s with %s rows" % (outcsv, len(rows))) output_json(outjson, headings, rows) @@ -133,18 +155,18 @@ def update_awi_series(): def update_cola(): - url = ss_table_urls['cola'] + url = ss_table_urls["cola"] outcsv = "%s/ss_cola_%s.csv" % (data_dir, TODAY.year) outjson = "%s/ss_cola_%s.json" % (data_dir, TODAY.year) - headings = ['Year', 'COLA'] + headings = ["Year", "COLA"] soup = make_soup(url) if soup: - [s.extract() for s in soup('small')] - tables = soup.findAll('table')[-3:] + [s.extract() for s in soup("small")] + tables = soup.findAll("table")[-3:] rows = [] log.info("found %s tables" % len(tables)) for table in tables: - rows.extend([row for row in table.findAll('tr') if row.findAll('td')]) + rows.extend([row for row in table.findAll("tr") if row.findAll("td")]) output_csv(outcsv, headings, rows) log.info("updated %s with %s rows" % (outcsv, len(rows))) output_json(outjson, headings, rows) @@ -153,31 +175,33 @@ def update_cola(): def update_life(): """update the actuarial life tables from SSA""" - msg = '' - url = ss_table_urls['actuarial_life'] + msg = "" + url = ss_table_urls["actuarial_life"] # outcsv = "%s/actuarial_life_%s.csv" % (data_dir, TODAY.year) # outjson = "%s/actuarial_life_%s.json" % (data_dir, TODAY.year) headings = [ - 'exact_age', - 'male_death_probability', - 'male_number_of_lives', - 'male_life_expectancy', - 'female_death_probability', - 'female_number_of_lives', - 'female_life_expectancy', + "exact_age", + "male_death_probability", + "male_number_of_lives", + "male_life_expectancy", + "female_death_probability", + "female_number_of_lives", + "female_life_expectancy", ] soup = make_soup(url) if soup: - table = soup.find('table').find('table') + table = soup.find("table").find("table") if not table: log.info("couldn't find table at %s" % url) else: - rows = table.findAll('tr')[2:] + rows = table.findAll("tr")[2:] if len(rows) > 100: output_csv(outcsv, headings, rows) msg += "updated %s with %s rows" % (outcsv, len(rows)) output_json(outjson, headings, rows) - msg += "updated {0} with {1} entries".format(outjson, len(rows)) + msg += "updated {0} with {1} entries".format( + outjson, len(rows) + ) else: msg += "didn't find more than 100 rows at {0}".format(url) log.info(msg) @@ -190,7 +214,12 @@ def harvest_all(): update_awi_series() update_example_reduction() + if __name__ == "__main__": starter = datetime.datetime.now() harvest_all() - log.info("update took {0} to update four data stores".format((datetime.datetime.now()-starter))) + log.info( + "update took {0} to update four data stores".format( + (datetime.datetime.now() - starter) + ) + ) diff --git a/retirement_api/utils/ss_utilities.py b/retirement_api/utils/ss_utilities.py index a4c6f61..5ce11c1 100755 --- a/retirement_api/utils/ss_utilities.py +++ b/retirement_api/utils/ss_utilities.py @@ -1,9 +1,11 @@ # coding: utf-8 -import os -import json import datetime -from dateutil import parser +import json import logging +import os + +from dateutil import parser + TODAY = datetime.datetime.now().date() BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(__file__))) @@ -46,25 +48,27 @@ """ AGE_ERROR_NOTES = { - 'too_old': {'en': TOO_OLD, 'es': TOO_OLD_ES}, - 'too_young': {'en': TOO_YOUNG, 'es': TOO_YOUNG_ES} + "too_old": {"en": TOO_OLD, "es": TOO_OLD_ES}, + "too_young": {"en": TOO_YOUNG, "es": TOO_YOUNG_ES}, } def get_note(note_type, language): """return language_specific error""" - if language == 'es': - return AGE_ERROR_NOTES[note_type]['es'] + if language == "es": + return AGE_ERROR_NOTES[note_type]["es"] else: - return AGE_ERROR_NOTES[note_type]['en'] + return AGE_ERROR_NOTES[note_type]["en"] # this datafile specifies years that have unique retirement age values # since this may change, it is maintained in an external file datafile = "{0}\ -/retirement_api/data/unique_retirement_ages.json".format(BASE_DIR) +/retirement_api/data/unique_retirement_ages.json".format( + BASE_DIR +) -with open(datafile, 'r') as f: +with open(datafile, "r") as f: age_map = json.loads(f.read()) for year in age_map: age_map[year] = tuple(age_map[year]) @@ -83,7 +87,7 @@ def get_current_age(dob): try: # when dob is 2/29 and the current year is not a leap year birthday = DOB.replace(year=today.year) except ValueError: - birthday = DOB.replace(year=today.year, day=DOB.day-1) + birthday = DOB.replace(year=today.year, day=DOB.day - 1) if birthday > today: return today.year - DOB.year - 1 else: @@ -95,8 +99,8 @@ def get_current_age(dob): def get_months_past_birthday(dob): """return the number of months a person is past their last birthday""" today = datetime.date.today() - months_at_birth = dob.year*12 + dob.month - 1 - months_today = today.year*12 + today.month - 1 + months_at_birth = dob.year * 12 + dob.month - 1 + months_today = today.year * 12 + today.month - 1 delta = months_today - months_at_birth return delta % 12 @@ -167,32 +171,32 @@ def get_retirement_age(birth_year): return None -def past_fra_test(dob=None, language='en'): +def past_fra_test(dob=None, language="en"): """ tests whether a person is past his/her full retirement age """ if not dob: - return 'invalid birth date entered' + return "invalid birth date entered" try: DOB = parser.parse(dob).date() except (TypeError, ValueError): - return 'invalid birth date entered' + return "invalid birth date entered" today = datetime.date.today() current_age = get_current_age(dob) if DOB >= today: - return get_note('too_young', language) + return get_note("too_young", language) # SSA has a special rule for people born on Jan. 1 # http://www.socialsecurity.gov/OACT/ProgData/nra.html if DOB.month == 1 and DOB.day == 1: - fra_tuple = get_retirement_age(DOB.year-1) + fra_tuple = get_retirement_age(DOB.year - 1) else: fra_tuple = get_retirement_age(DOB.year) age_tuple = (current_age, get_months_past_birthday(DOB)) # print "age_tuple: %s; fra_tuple: %s" % (age_tuple, fra_tuple) if age_tuple[0] < 22: - return get_note('too_young', language) + return get_note("too_young", language) if age_tuple[0] > 70: - return get_note('too_old', language).format(DOB.strftime("%m/%d/%Y")) + return get_note("too_old", language).format(DOB.strftime("%m/%d/%Y")) if age_tuple[0] > fra_tuple[0]: return True elif age_tuple[0] < fra_tuple[0]: diff --git a/retirement_api/utils/ssa_check.py b/retirement_api/utils/ssa_check.py index 70d3d99..62538b6 100644 --- a/retirement_api/utils/ssa_check.py +++ b/retirement_api/utils/ssa_check.py @@ -1,23 +1,24 @@ # utilities for checking results from SSA's Quick Calculator import datetime -from copy import copy import json import logging +from copy import copy -from .ss_calculator import get_retire_data from ..models import Calibration +from .ss_calculator import get_retire_data + SSA_PARAMS = { - 'dobmon': 0, - 'dobday': 0, - 'yob': 0, - 'earnings': 40000, - 'lastYearEarn': '', - 'lastEarn': '', - 'retiremonth': '', - 'retireyear': '', - 'dollars': 1, - 'prgf': 2 + "dobmon": 0, + "dobday": 0, + "yob": 0, + "earnings": 40000, + "lastYearEarn": "", + "lastEarn": "", + "retiremonth": "", + "retireyear": "", + "dollars": 1, + "prgf": 2, } logger = logging.getLogger(__name__) @@ -37,24 +38,26 @@ def get_test_params(age, dob_day, dob_year=None, income=40000): dob = dob.replace(year=(dob.year - 1), month=12) else: dob = dob.replace(month=(dob.month - 1)) - params['dobmon'] = dob.month - params['dobday'] = dob.day - params['yob'] = dob.year - params['earnings'] = income + params["dobmon"] = dob.month + params["dobday"] = dob.day + params["yob"] = dob.year + params["earnings"] = income return params def assemble_test_params(): """build a set of edge-case test parameters""" tests = { - 'born-on-1st-age-46': get_test_params(46, 1), - 'born-on-2nd-age-46': get_test_params(46, 2), - 'born-on-3rd-age-46': get_test_params(46, 3) - } + "born-on-1st-age-46": get_test_params(46, 1), + "born-on-2nd-age-46": get_test_params(46, 2), + "born-on-3rd-age-46": get_test_params(46, 3), + } for year in range(1946, 1961): - tests['born_on_3rd_in_{0}'.format(year)] = get_test_params(46, 3, dob_year=year) + tests["born_on_3rd_in_{0}".format(year)] = get_test_params( + 46, 3, dob_year=year + ) - tests['born_on_3rd_in_1970'] = get_test_params(46, 3, dob_year=1970) + tests["born_on_3rd_in_1970"] = get_test_params(46, 3, dob_year=1970) return tests @@ -63,50 +66,75 @@ def check_results(test_data, TESTS): today = datetime.date.today() error_msg = "Mismatches found on {0}".format(today) OK = True - calibration = Calibration.objects.order_by('-created').first() + calibration = Calibration.objects.order_by("-created").first() target_result_set = json.loads(calibration.results_json) for slug in test_data: target_results = target_result_set[slug] test_results = test_data[slug] - for key in ['note', - 'params_adjusted', - 'current_age', - 'past_fra', - 'error']: + for key in [ + "note", + "params_adjusted", + "current_age", + "past_fra", + "error", + ]: if test_results[key] != target_results[key]: OK = False - error_msg += "\n{0}: base param {1} did not match; expected {2} but found {3}".format( - slug, - key, - target_results[key], - test_results[key]) - for data_key in ['months_past_birthday', 'full retirement age']: - if test_results['data'][data_key] != target_results['data'][data_key]: + error_msg += ( + "\n{0}: base param {1} did not match; " + "expected {2} but found {3}".format( + slug, key, target_results[key], test_results[key] + ) + ) + for data_key in ["months_past_birthday", "full retirement age"]: + if ( + test_results["data"][data_key] + != target_results["data"][data_key] + ): OK = False - error_msg += "\n{0}: data param {1} did not match; expected {2} but found {3}".format( - slug, - data_key, - target_results['data'][data_key], - test_results['data'][data_key]) - for benefit_key in target_results['data']['benefits'].keys(): - if test_results['data']['benefits'][benefit_key] != target_results['data']['benefits'][benefit_key]: + error_msg += ( + "\n{0}: data param {1} did not match; " + "expected {2} but found {3}".format( + slug, + data_key, + target_results["data"][data_key], + test_results["data"][data_key], + ) + ) + for benefit_key in target_results["data"]["benefits"].keys(): + if ( + test_results["data"]["benefits"][benefit_key] + != target_results["data"]["benefits"][benefit_key] + ): OK = False - error_msg += "\n{0}: benefit param {1} did not match; expected {2} but found {3}".format( - slug, - benefit_key, - target_results['data']['benefits'][benefit_key], - test_results['data']['benefits'][benefit_key]) - for ssa_param_key in target_results['data']['params'].keys(): - if test_results['data']['params'][ssa_param_key] != target_results['data']['params'][ssa_param_key]: + error_msg += ( + "\n{0}: benefit param {1} did not match; " + "expected {2} but found {3}".format( + slug, + benefit_key, + target_results["data"]["benefits"][benefit_key], + test_results["data"]["benefits"][benefit_key], + ) + ) + for ssa_param_key in target_results["data"]["params"].keys(): + if ( + test_results["data"]["params"][ssa_param_key] + != target_results["data"]["params"][ssa_param_key] + ): OK = False - error_msg += "\n{0}: ssa param {1} did not match; expected {2} but found {3}".format( - slug, - ssa_param_key, - target_results['data']['params'][ssa_param_key], - test_results['data']['params'][ssa_param_key]) + error_msg += ( + "\n{0}: ssa param {1} did not match; " + "expected {2} but found {3}".format( + slug, + ssa_param_key, + target_results["data"]["params"][ssa_param_key], + test_results["data"]["params"][ssa_param_key], + ) + ) if OK: - return ("All tests pass on {0}; " - "last recalibrated on {1}".format(today, calibration.created.date())) + return "All tests pass on {0}; " "last recalibrated on {1}".format( + today, calibration.created.date() + ) else: logger.warn(error_msg) return error_msg @@ -118,7 +146,7 @@ def run_tests(recalibrate=False): for test in TESTS: # sys.stdout.write('.') # sys.stdout.flush() - collector[test] = get_retire_data(TESTS[test], language='en') + collector[test] = get_retire_data(TESTS[test], language="en") if recalibrate: new_calibration = Calibration(results_json=json.dumps(collector)) new_calibration.save() diff --git a/retirement_api/utils/tests/test_api_check.py b/retirement_api/utils/tests/test_api_check.py index 2f1d144..7e1a2ef 100644 --- a/retirement_api/utils/tests/test_api_check.py +++ b/retirement_api/utils/tests/test_api_check.py @@ -1,85 +1,88 @@ -import os -import sys -import json import datetime +import json +import unittest -import requests import mock -import unittest +import requests + +from ..check_api import Collector, TimeoutError, build_msg, check_data, run + -from ..check_api import Collector, build_msg, check_data, run, TimeoutError timestamp = datetime.datetime.now() class TestApi(unittest.TestCase): """test the tester""" + test_collector = Collector() - test_collector.domain = 'build' + test_collector.domain = "build" test_data = { - 'current_age': 44, - 'note': "", - 'data': { - 'benefits': { - 'age 63': 1603, - 'age 62': 1476, - 'age 67': 2137, - 'age 66': 1995, - 'age 65': 1852, - 'age 64': 1710, - 'age 69': 2479, - 'age 68': 2308, - 'age 70': 2650 - }, - 'disability': "$1,899", - 'early retirement age': "62 and 1 month", - 'params': { - 'dollars': 1, - 'lastYearEarn': "", - 'dobday': 7, - 'prgf': 2, - 'dobmon': 7, - 'retiremonth': "", - 'retireyear': "", - 'yob': 1970, - 'lastEarn': "", - 'earnings': 70000 - }, - 'full retirement age': "67", - 'survivor benefits': { - 'spouse at full retirement age': "$1,912", - 'family maximum': "$3,377", - 'spouse caring for child': "$1,434", - 'child': "$1,434" - } + "current_age": 44, + "note": "", + "data": { + "benefits": { + "age 63": 1603, + "age 62": 1476, + "age 67": 2137, + "age 66": 1995, + "age 65": 1852, + "age 64": 1710, + "age 69": 2479, + "age 68": 2308, + "age 70": 2650, + }, + "disability": "$1,899", + "early retirement age": "62 and 1 month", + "params": { + "dollars": 1, + "lastYearEarn": "", + "dobday": 7, + "prgf": 2, + "dobmon": 7, + "retiremonth": "", + "retireyear": "", + "yob": 1970, + "lastEarn": "", + "earnings": 70000, + }, + "full retirement age": "67", + "survivor benefits": { + "spouse at full retirement age": "$1,912", + "family maximum": "$3,377", + "spouse caring for child": "$1,434", + "child": "$1,434", }, - 'error': "" - } + }, + "error": "", + } def test_check_data(self): msg = check_data(self.test_data) - self.assertTrue(msg == 'OK') + self.assertTrue(msg == "OK") def test_build_msg(self): - target_text = ',{0},build,,,,'.format(self.test_collector.date) + target_text = ",{0},build,,,,".format(self.test_collector.date) test_text = build_msg(self.test_collector) self.assertTrue(test_text == target_text) - @mock.patch('retirement_api.utils.check_api.requests.get') - @mock.patch('retirement_api.utils.check_api.build_msg') + @mock.patch("retirement_api.utils.check_api.requests.get") + @mock.patch("retirement_api.utils.check_api.build_msg") def test_run(self, mock_build_msg, mock_requests): mock_requests.return_value.text = json.dumps(self.test_data) mock_requests.return_value.status_code = 200 - mock_build_msg.return_value = ',%s,,,mock error,,,' % self.test_collector.date - run('build') + mock_build_msg.return_value = ( + ",%s,,,mock error,,," % self.test_collector.date + ) + run("build") self.assertTrue(mock_build_msg.call_count == 1) mock_requests.return_value.status_code = 400 - collector = run('build') - self.assertTrue('FAIL' in collector.api_fail) - collector = run('fakeplaceholder.com') - self.assertTrue('recognized' in collector.error) + collector = run("build") + self.assertTrue("FAIL" in collector.api_fail) + collector = run("fakeplaceholder.com") + self.assertTrue("recognized" in collector.error) mock_requests.side_effect = requests.ConnectionError - collector = run('build') - self.assertTrue(collector.status == 'ABORTED') + collector = run("build") + self.assertTrue(collector.status == "ABORTED") mock_requests.side_effect = TimeoutError - collector = run('prod') + collector = run("prod") self.assertTrue(collector.status == "TIMEDOUT") diff --git a/retirement_api/utils/tests/test_ss_update_stats.py b/retirement_api/utils/tests/test_ss_update_stats.py index 44e6759..6e8d92e 100644 --- a/retirement_api/utils/tests/test_ss_update_stats.py +++ b/retirement_api/utils/tests/test_ss_update_stats.py @@ -1,27 +1,29 @@ -import os -import sys -import json +import csv import datetime +import json +import os import shutil +import sys import tempfile -import csv -from bs4 import BeautifulSoup as bs -import requests -import mock from django.test import TestCase -# if __name__ == '__main__': -# BASE_DIR = '~/Projects/retirement1.6/retirement/retirement_api' -# else: -# BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(__file__)))) -BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(__file__)))) +import mock +from bs4 import BeautifulSoup as bs +from retirement_api import utils +from retirement_api.utils.ss_update_stats import ( + make_soup, + output_csv, + output_json, +) + + +BASE_DIR = os.path.dirname( + os.path.dirname(os.path.dirname(os.path.dirname(__file__))) +) sys.path.append(BASE_DIR) os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings") -sys.path.append("{0}/retirement_api".format(BASE_DIR)) -import utils -from utils import ss_update_stats -from utils.ss_update_stats import output_csv, output_json, make_soup, update_life, update_cola, ss_table_urls, requests + TODAY = datetime.date.today() mock_data_path = "{0}/retirement_api/data/mock_data".format(BASE_DIR) @@ -33,19 +35,19 @@ class UpdateSsStatsTests(TestCase): life_page = "{0}/ssa_life.html".format(mock_data_path) earlyretire_page = "{0}/ssa_earlyretire.html".format(mock_data_path) life_headings = [ - 'exact_age', - 'male_death_probability', - 'male_number_of_lives', - 'male_life_expectancy', - 'female_death_probability', - 'female_number_of_lives', - 'female_life_expectancy', - ] + "exact_age", + "male_death_probability", + "male_number_of_lives", + "male_life_expectancy", + "female_death_probability", + "female_number_of_lives", + "female_life_expectancy", + ] sample_life_results = { - 1: '0,0.006680,100000,76.10,0.005562,100000,80.94', - 10: '9,0.000096,99164,67.74,0.000090,99313,72.50', - 100: '99,0.339972,1323,2.22,0.287178,3807,2.60' - } + 1: "0,0.006680,100000,76.10,0.005562,100000,80.94", + 10: "9,0.000096,99164,67.74,0.000090,99313,72.50", + 100: "99,0.339972,1323,2.22,0.287178,3807,2.60", + } def setUp(self): self.tempdir = tempfile.mkdtemp() @@ -55,43 +57,54 @@ def tearDown(self): # def output_csv(filepath, headings, bs_rows): def test_output_csv(self): - """ outputs csv based on inputs of + """ outputs csv based on inputs of headings and beautiful_soup rows """ mockpath = "{0}/mock_life.csv".format(self.tempdir) - with open(self.life_page, 'r') as f: + with open(self.life_page, "r") as f: mockpage = f.read() - table = bs(mockpage, 'html.parser').find('table').find('table') - rows = table.findAll('tr')[2:] + table = bs(mockpage, "html.parser").find("table").find("table") + rows = table.findAll("tr")[2:] output_csv(mockpath, self.life_headings, rows) self.assertTrue(os.path.isfile(mockpath)) - with open(mockpath, 'r') as f: + with open(mockpath, "r") as f: reader = csv.reader(f) data = [row for row in reader] for sample in self.sample_life_results: - self.assertEqual(data[sample], self.sample_life_results[sample].split(',')) + self.assertEqual( + data[sample], self.sample_life_results[sample].split(",") + ) # def output_json(filepath, headings, bs_rows): def test_output_json(self): - """ outputs json to file based on inputs of + """ outputs json to file based on inputs of path, headings and beautiful_soup rows """ mockpath = "{0}/mock_life.json".format(self.tempdir) sample_json_results = { - '0': {'female_life_expectancy': '80.94', 'male_life_expectancy': '76.10'}, - '57': {'female_life_expectancy': '26.91', 'male_life_expectancy': '23.69'}, - '99': {'female_life_expectancy': '2.60', 'male_life_expectancy': '2.22'} + "0": { + "female_life_expectancy": "80.94", + "male_life_expectancy": "76.10", + }, + "57": { + "female_life_expectancy": "26.91", + "male_life_expectancy": "23.69", + }, + "99": { + "female_life_expectancy": "2.60", + "male_life_expectancy": "2.22", + }, } - with open(self.life_page, 'r') as f: + with open(self.life_page, "r") as f: mockpage = f.read() - table = bs(mockpage, 'html.parser').find('table').find('table') - rows = table.findAll('tr')[2:] + table = bs(mockpage, "html.parser").find("table").find("table") + rows = table.findAll("tr")[2:] output_json(mockpath, self.life_headings, rows) self.assertTrue(os.path.isfile(mockpath)) - with open(mockpath, 'r') as f: - data = json.loads(f.read()) + with open(mockpath, "r") as f: + data = json.loads(f.read()) self.assertEqual(type(data), dict) - for key in data['0'].keys(): + for key in data["0"].keys(): self.assertTrue(key in self.life_headings) for age in sample_json_results: for key in sample_json_results[age]: @@ -100,39 +113,49 @@ def test_output_json(self): def test_make_soup(self): """ given a url, makes a request and returns beautifulsoup for parsing """ - url = 'http://www.socialsecurity.gov/OACT/ProgData/nra.html' + url = "http://www.socialsecurity.gov/OACT/ProgData/nra.html" soup = make_soup(url) - self.assertTrue('Social Security' in soup.find('h1').text) + self.assertTrue("Social Security" in soup.find("h1").text) - @mock.patch('requests.get') + @mock.patch("requests.get") def test_make_soup_error(self, mock_requests): - url = 'http://www.socialsecurity.gov/xxxx/' - mock_requests.return_value.reason = 'Not found' + url = "http://www.socialsecurity.gov/xxxx/" + mock_requests.return_value.reason = "Not found" soup = make_soup(url) - self.assertEqual(soup, '') + self.assertEqual(soup, "") # @mock.patch('utils.ss_update_stats.requests.get') # @mock.patch('utils.ss_update_stats.update_life') - @mock.patch('utils.ss_update_stats.update_life') - @mock.patch('utils.ss_update_stats.update_cola') - @mock.patch('utils.ss_update_stats.update_awi_series') - @mock.patch('utils.ss_update_stats.update_example_reduction') - def test_harvest_all(self, mock_update_example, mock_update_awi, mock_update_cola, mock_update_life): + @mock.patch("retirement_api.utils.ss_update_stats.update_life") + @mock.patch("retirement_api.utils.ss_update_stats.update_cola") + @mock.patch("retirement_api.utils.ss_update_stats.update_awi_series") + @mock.patch( + "retirement_api.utils.ss_update_stats.update_example_reduction" + ) + def test_harvest_all( + self, + mock_update_example, + mock_update_awi, + mock_update_cola, + mock_update_life, + ): utils.ss_update_stats.harvest_all() assert mock_update_example.call_count == 1 assert mock_update_awi.call_count == 1 assert mock_update_cola.call_count == 1 assert mock_update_life.call_count == 1 - @mock.patch('utils.ss_update_stats.output_csv') - @mock.patch('utils.ss_update_stats.output_json') - @mock.patch('utils.ss_update_stats.make_soup') - def test_example_reduction(self, mock_soup, mock_output_json, mock_output_csv): + @mock.patch("retirement_api.utils.ss_update_stats.output_csv") + @mock.patch("retirement_api.utils.ss_update_stats.output_json") + @mock.patch("retirement_api.utils.ss_update_stats.make_soup") + def test_example_reduction( + self, mock_soup, mock_output_json, mock_output_csv + ): # arrange - with open(self.earlyretire_page, 'r') as f: + with open(self.earlyretire_page, "r") as f: mockpage = f.read() - mock_soup.return_value = bs(mockpage, 'html.parser') + mock_soup.return_value = bs(mockpage, "html.parser") # action utils.ss_update_stats.update_example_reduction() @@ -143,15 +166,14 @@ def test_example_reduction(self, mock_soup, mock_output_json, mock_output_csv): assert mock_output_csv.call_count == 1 assert mock_output_json.call_count == 1 - - @mock.patch('utils.ss_update_stats.output_csv') - @mock.patch('utils.ss_update_stats.output_json') - @mock.patch('utils.ss_update_stats.make_soup') + @mock.patch("retirement_api.utils.ss_update_stats.output_csv") + @mock.patch("retirement_api.utils.ss_update_stats.output_json") + @mock.patch("retirement_api.utils.ss_update_stats.make_soup") def test_update_life(self, mock_soup, mock_output_json, mock_output_csv): # arrange - with open(self.life_page, 'r') as f: + with open(self.life_page, "r") as f: mockpage = f.read() - mock_soup.return_value = bs(mockpage, 'html.parser') + mock_soup.return_value = bs(mockpage, "html.parser") # action utils.ss_update_stats.update_life() @@ -162,15 +184,14 @@ def test_update_life(self, mock_soup, mock_output_json, mock_output_csv): assert mock_output_csv.call_count == 1 assert mock_output_json.call_count == 1 - - @mock.patch('utils.ss_update_stats.output_csv') - @mock.patch('utils.ss_update_stats.output_json') - @mock.patch('utils.ss_update_stats.make_soup') + @mock.patch("retirement_api.utils.ss_update_stats.output_csv") + @mock.patch("retirement_api.utils.ss_update_stats.output_json") + @mock.patch("retirement_api.utils.ss_update_stats.make_soup") def test_update_cola(self, mock_soup, mock_output_json, mock_output_csv): # arrange - with open(self.cola_page, 'r') as f: + with open(self.cola_page, "r") as f: mockpage = f.read() - mock_soup.return_value = bs(mockpage, 'html.parser') + mock_soup.return_value = bs(mockpage, "html.parser") # action utils.ss_update_stats.update_cola() @@ -180,14 +201,16 @@ def test_update_cola(self, mock_soup, mock_output_json, mock_output_csv): assert mock_output_csv.call_count == 1 assert mock_output_json.call_count == 1 - @mock.patch('utils.ss_update_stats.output_csv') - @mock.patch('utils.ss_update_stats.output_json') - @mock.patch('utils.ss_update_stats.make_soup') - def test_update_awi_series(self, mock_soup, mock_output_json, mock_output_csv): + @mock.patch("retirement_api.utils.ss_update_stats.output_csv") + @mock.patch("retirement_api.utils.ss_update_stats.output_json") + @mock.patch("retirement_api.utils.ss_update_stats.make_soup") + def test_update_awi_series( + self, mock_soup, mock_output_json, mock_output_csv + ): # arrange - with open(self.awi_page, 'r') as f: + with open(self.awi_page, "r") as f: mockpage = f.read() - mock_soup.return_value = bs(mockpage, 'html.parser') + mock_soup.return_value = bs(mockpage, "html.parser") # action utils.ss_update_stats.update_awi_series() diff --git a/retirement_api/utils/tests/test_ss_utilities.py b/retirement_api/utils/tests/test_ss_utilities.py index 3e60c25..7d01680 100644 --- a/retirement_api/utils/tests/test_ss_utilities.py +++ b/retirement_api/utils/tests/test_ss_utilities.py @@ -1,48 +1,54 @@ +import copy +import datetime +import json import os import sys -import json -import datetime -import copy -from datetime import timedelta -from datetime import date - -from dateutil.relativedelta import relativedelta -from freezegun import freeze_time -import requests -import mock import unittest +from datetime import date, timedelta import django -from retirement_api.models import Calibration + +import mock +import requests +from dateutil.relativedelta import relativedelta +from freezegun import freeze_time from retirement_api import utils +from retirement_api.models import Calibration +from retirement_api.utils.ss_calculator import ( + calculate_lifetime_benefits, + clean_comment, + get_retire_data, + interpolate_benefits, + interpolate_for_past_fra, + num_test, + parse_details, + parse_response, + set_up_runvars, + validate_date, +) +from retirement_api.utils.ss_utilities import ( + age_map, + get_current_age, + get_delay_bonus, + get_months_past_birthday, + get_months_until_next_birthday, + get_retirement_age, + past_fra_test, + yob_test, +) +from retirement_api.utils.ssa_check import ( + assemble_test_params, + check_results, + get_test_params, +) + -from ..ss_utilities import (get_retirement_age, - get_months_until_next_birthday, - past_fra_test, - get_current_age, - get_delay_bonus, - age_map, - get_months_past_birthday, - yob_test) -from ..ss_calculator import (num_test, - parse_details, - parse_response, - clean_comment, - interpolate_benefits, - interpolate_for_past_fra, - calculate_lifetime_benefits, - get_retire_data, - set_up_runvars, - validate_date) -# from ..check_api import TimeoutError -from ..ssa_check import (assemble_test_params, - get_test_params, - check_results) # , # run_tests) BASE_DIR = os.path.dirname( - os.path.dirname(os.path.dirname(os.path.dirname(__file__)))) + os.path.dirname(os.path.dirname(os.path.dirname(__file__))) +) sys.path.append(BASE_DIR) os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings") @@ -51,19 +57,19 @@ class SSACheckTests(django.test.TestCase): - fixtures = ['test_calibration.json'] + fixtures = ["test_calibration.json"] TESTS = assemble_test_params() sample_params = { - 'dobmon': 1, - 'dobday': 3, - 'yob': 1970, - 'earnings': 40000, - 'lastYearEarn': '', - 'lastEarn': '', - 'retiremonth': 1, - 'retireyear': 2037, - 'dollars': 1, - 'prgf': 2 + "dobmon": 1, + "dobday": 3, + "yob": 1970, + "earnings": 40000, + "lastYearEarn": "", + "lastEarn": "", + "retiremonth": 1, + "retireyear": 2037, + "dollars": 1, + "prgf": 2, } def test_assemble_test_params(self): @@ -76,24 +82,25 @@ def test_check_results(self): test_msg = check_results(test_data, self.TESTS) self.assertTrue("pass" in test_msg) slug = list(test_data.keys())[0] - test_data[slug]['current_age'] = 99 - test_data[slug]['current_age'] = 99 - test_data[slug]['data']['months_past_birthday'] = 13 - test_data[slug]['data']['benefits']['age 70'] = 0 - test_data[slug]['data']['params']['yob'] = 0 + test_data[slug]["current_age"] = 99 + test_data[slug]["current_age"] = 99 + test_data[slug]["data"]["months_past_birthday"] = 13 + test_data[slug]["data"]["benefits"]["age 70"] = 0 + test_data[slug]["data"]["params"]["yob"] = 0 test_msg2 = check_results(test_data, self.TESTS) self.assertTrue("Mismatches" in test_msg2) - @mock.patch('retirement_api.utils.ssa_check.get_retire_data') - @mock.patch('retirement_api.utils.ssa_check.check_results') + @mock.patch("retirement_api.utils.ssa_check.get_retire_data") + @mock.patch("retirement_api.utils.ssa_check.check_results") def test_run_tests(self, mock_check_results, mock_get_retire_data): mock_get_retire_data.return_value = ( - Calibration.objects.first().results_json) + Calibration.objects.first().results_json + ) mock_check_results.return_value = "All pass" test1 = utils.ssa_check.run_tests() self.assertTrue(mock_get_retire_data.call_count == len(self.TESTS)) self.assertTrue(mock_check_results.call_count == 1) - self.assertTrue('pass' in test1) + self.assertTrue("pass" in test1) utils.ssa_check.run_tests(recalibrate=True) self.assertTrue(Calibration.objects.count() == 2) @@ -105,152 +112,163 @@ class UtilitiesTests(unittest.TestCase): today = today.replace(day=today.day - 1) sample_params = { - 'dobmon': 1, - 'dobday': 5, - 'yob': 1970, - 'earnings': 70000, - 'lastYearEarn': '', - 'lastEarn': '', - 'retiremonth': 1, - 'retireyear': 2037, - 'dollars': 1, - 'prgf': 2 + "dobmon": 1, + "dobday": 5, + "yob": 1970, + "earnings": 70000, + "lastYearEarn": "", + "lastEarn": "", + "retiremonth": 1, + "retireyear": 2037, + "dollars": 1, + "prgf": 2, + } + sample_results = { + "data": { + "early retirement age": "", + "full retirement age": "", + "benefits": { + "age 62": 0, + "age 63": 0, + "age 64": 0, + "age 65": 0, + "age 66": 0, + "age 67": 2176, + "age 68": 0, + "age 69": 0, + "age 70": 0, + }, + "params": { + "dobmon": 1, + "dobday": 5, + "yob": 1970, + "earnings": 40000, + "lastYearEarn": "", + "lastEarn": "", + "retiremonth": 1, + "retireyear": 2037, + "dollars": 1, + "prgf": 2, + }, + "disability": "", + "months_past_birthday": 0, + "survivor benefits": { + "child": "", + "spouse caring for child": "", + "spouse at full retirement age": "", + "family maximum": "", + }, + }, + "current_age": 44, + "error": "", + "note": "", + "past_fra": False, + } + + sample_lifetime_benefits = { + "age62": 261800, # born in 1957, 40K + "age63": 267168, + "age64": 274176, + "age65": 282000, + "age66": 289932, + "age67": 293328, + "age68": 298248, + "age69": 300672, + "age70": 300600, } - sample_results = {'data': {'early retirement age': '', - 'full retirement age': '', - 'benefits': {'age 62': 0, - 'age 63': 0, - 'age 64': 0, - 'age 65': 0, - 'age 66': 0, - 'age 67': 2176, - 'age 68': 0, - 'age 69': 0, - 'age 70': 0, - }, - 'params': {'dobmon': 1, - 'dobday': 5, - 'yob': 1970, - 'earnings': 40000, - 'lastYearEarn': '', - 'lastEarn': '', - 'retiremonth': 1, - 'retireyear': 2037, - 'dollars': 1, - 'prgf': 2}, - 'disability': '', - 'months_past_birthday': 0, - 'survivor benefits': { - 'child': '', - 'spouse caring for child': '', - 'spouse at full retirement age': '', - 'family maximum': '' - } - }, - 'current_age': 44, - 'error': '', - 'note': '', - 'past_fra': False, - } - - sample_lifetime_benefits = {'age62': 261800, # born in 1957, 40K - 'age63': 267168, - 'age64': 274176, - 'age65': 282000, - 'age66': 289932, - 'age67': 293328, - 'age68': 298248, - 'age69': 300672, - 'age70': 300600} def test_calculate_lifetime_benefits(self): results = copy.deepcopy(self.sample_results) - results['data']['benefits'] = {'age 62': 952, - 'age 63': 1012, - 'age 64': 1088, - 'age 65': 1175, - 'age 66': 1306, - 'age 67': 1358, - 'age 68': 1462, - 'age 69': 1566, - 'age 70': 1670} - results['current_age'] = 59 + results["data"]["benefits"] = { + "age 62": 952, + "age 63": 1012, + "age 64": 1088, + "age 65": 1175, + "age 66": 1306, + "age 67": 1358, + "age 68": 1462, + "age 69": 1566, + "age 70": 1670, + } + results["current_age"] = 59 base_benefit = 1306 fra_tuple = (66, 6) dob = datetime.date(1957, 1, 3) past_fra = False - test_results = calculate_lifetime_benefits(results, - base_benefit, - fra_tuple, - dob, - past_fra) - for key in results['data']['benefits']: - lifekey = key.replace('age ', 'age') - self.assertTrue(self.sample_lifetime_benefits[lifekey] == - test_results['data']['lifetime'][lifekey]) + test_results = calculate_lifetime_benefits( + results, base_benefit, fra_tuple, dob, past_fra + ) + for key in results["data"]["benefits"]: + lifekey = key.replace("age ", "age") + self.assertTrue( + self.sample_lifetime_benefits[lifekey] + == test_results["data"]["lifetime"][lifekey] + ) dob = dob.replace(day=2) - test_results = calculate_lifetime_benefits(results, - base_benefit, - fra_tuple, - dob, - past_fra) - self.assertTrue(test_results['data']['lifetime']['age62'] == 262752) + test_results = calculate_lifetime_benefits( + results, base_benefit, fra_tuple, dob, past_fra + ) + self.assertTrue(test_results["data"]["lifetime"]["age62"] == 262752) def test_get_test_params(self): test_params = get_test_params(46, 3) - self.assertEqual(test_params['dobday'], 3) + self.assertEqual(test_params["dobday"], 3) test_params = get_test_params(46, 3, dob_year=1950) - self.assertEqual(test_params['yob'], 1950) + self.assertEqual(test_params["yob"], 1950) test_params = get_test_params(46, 3, dob_year=1950) - self.assertEqual(test_params['yob'], 1950) + self.assertEqual(test_params["yob"], 1950) if self.today.day > 27: test_today = self.today.replace(day=27) # pragma: no cover else: test_today = self.today test_params = get_test_params(46, test_today.day + 1) - self.assertEqual(test_params['dobday'], test_today.day + 1) + self.assertEqual(test_params["dobday"], test_today.day + 1) - @mock.patch('retirement_api.utils.ssa_check.datetime.date') + @mock.patch("retirement_api.utils.ssa_check.datetime.date") def test_get_test_params_in_january(self, mock_date): mock_date.today.return_value = date(2017, 1, 2) test_params = get_test_params(46, 3) - self.assertEqual(test_params['yob'], 1970) + self.assertEqual(test_params["yob"], 1970) mock_date.today.return_value = date(2017, 1, 27) test_params = get_test_params(46, 28) - self.assertEqual(test_params['yob'], 1970) + self.assertEqual(test_params["yob"], 1970) mock_date.today.return_value = date(2017, 2, 27) test_params = get_test_params(46, 28) - self.assertEqual(test_params['yob'], 1971) + self.assertEqual(test_params["yob"], 1971) def test_clean_comment(self): - test_comment = '' - expected_comment = 'This is a test comment' + test_comment = "" + expected_comment = "This is a test comment" self.assertTrue(clean_comment(test_comment) == expected_comment) def test_set_up_runvars(self): mock_params = copy.copy(self.sample_params) - (test_dob, - test_dobstring, - test_current_age, - test_fra_tuple, - test_past_fra, - test_results) = set_up_runvars(mock_params) - self.assertTrue(test_results['data']['params']['yob'] == 1970) - mock_params['dobday'] = 1 - (test_dob, - test_dobstring, - test_current_age, - test_fra_tuple, - test_past_fra, - test_results2) = set_up_runvars(mock_params) - self.assertTrue(test_results2['data']['params']['yob'] == 1969) + ( + test_dob, + test_dobstring, + test_current_age, + test_fra_tuple, + test_past_fra, + test_results, + ) = set_up_runvars(mock_params) + self.assertTrue(test_results["data"]["params"]["yob"] == 1970) + mock_params["dobday"] = 1 + ( + test_dob, + test_dobstring, + test_current_age, + test_fra_tuple, + test_past_fra, + test_results2, + ) = set_up_runvars(mock_params) + self.assertTrue(test_results2["data"]["params"]["yob"] == 1969) def test_months_past_birthday(self): - dob = self.today-timedelta(days=(365 * 20) + 6) + dob = self.today - timedelta(days=(365 * 20) + 6) self.assertTrue(get_months_past_birthday(dob) in [0, 1]) - dob = self.today-timedelta(days=(365 * 20) + 70) + dob = self.today - timedelta(days=(365 * 20) + 70) self.assertTrue(get_months_past_birthday(dob) in [2, 3]) - dob = self.today-timedelta(days=(365 * 20) + 320) + dob = self.today - timedelta(days=(365 * 20) + 320) self.assertTrue(get_months_past_birthday(dob) in [10, 11]) def test_months_until_next_bday(self): @@ -265,106 +283,122 @@ def test_months_until_next_bday(self): self.assertTrue(diff3 in [1, 2]) def test_get_current_age(self): - age_pairs = [(self.today.replace(year=self.today.year - 1), 1), - ('{0}'.format(self.today.replace(year=self.today.year - 1)), 1), - (self.today.replace(year=self.today.year - 20), 20), - (self.today.replace(year=self.today.year - 60), 60), - (self.today, (0 or None)), - ('xx', (0 or None)), - (self.today + datetime.timedelta(days=2), (0 or None))] + age_pairs = [ + (self.today.replace(year=self.today.year - 1), 1), + ("{0}".format(self.today.replace(year=self.today.year - 1)), 1), + (self.today.replace(year=self.today.year - 20), 20), + (self.today.replace(year=self.today.year - 60), 60), + (self.today, (0 or None)), + ("xx", (0 or None)), + (self.today + datetime.timedelta(days=2), (0 or None)), + ] for pair in age_pairs: self.assertTrue(get_current_age(pair[0]) == pair[1]) - @mock.patch('retirement_api.utils.ss_utilities.datetime.date') + @mock.patch("retirement_api.utils.ss_utilities.datetime.date") def test_get_current_age_leapyear(self, mock_date): mock_date.today.return_value = date(2015, 1, 29) mock_date.side_effect = lambda *args, **kw: date(*args, **kw) - age_pair = ('2-29-1980', 34) + age_pair = ("2-29-1980", 34) self.assertEqual(get_current_age(age_pair[0]), age_pair[1]) def test_interpolate_benefits(self): mock_results = copy.deepcopy(self.sample_results) expected_benefits = { - 'age 62': 1532, - 'age 63': 1632, - 'age 64': 1741, - 'age 65': 1886, - 'age 66': 2031, - 'age 67': 2176, - 'age 68': 2350, - 'age 69': 2524, - 'age 70': 2698 - } - dob = self.today.replace(year=self.today.year-44) + "age 62": 1532, + "age 63": 1632, + "age 64": 1741, + "age 65": 1886, + "age 66": 2031, + "age 67": 2176, + "age 68": 2350, + "age 69": 2524, + "age 70": 2698, + } + dob = self.today.replace(year=self.today.year - 44) if dob.day == 2: # pragma: no cover - expected_benefits['age 62'] = 1523 + expected_benefits["age 62"] = 1523 # need to pass results, base, fra_tuple, current_age, DOB results = interpolate_benefits(mock_results, 2176, (67, 0), 44, dob) - for key in results['data']['benefits'].keys(): - self.assertTrue(results['data']['benefits'][key] == expected_benefits[key]) - mock_results['data']['benefits']['age 66'] = mock_results['data']['benefits']['age 67'] - mock_results['data']['benefits']['age 67'] = 0 - dob = self.today - datetime.timedelta(days=365*55) - datetime.timedelta(days=14) + for key in results["data"]["benefits"].keys(): + self.assertTrue( + results["data"]["benefits"][key] == expected_benefits[key] + ) + mock_results["data"]["benefits"]["age 66"] = mock_results["data"][ + "benefits" + ]["age 67"] + mock_results["data"]["benefits"]["age 67"] = 0 + dob = ( + self.today + - datetime.timedelta(days=365 * 55) + - datetime.timedelta(days=14) + ) results = interpolate_benefits(mock_results, 2176, (66, 0), 55, dob) - for key in sorted(results['data']['benefits'].keys()): - self.assertTrue(results['data']['benefits'][key] != 0) + for key in sorted(results["data"]["benefits"].keys()): + self.assertTrue(results["data"]["benefits"][key] != 0) dob = dob.replace(day=2) results = interpolate_benefits(mock_results, 2176, (66, 0), 55, dob) - self.assertTrue(results['data']['benefits']['age 62'] != 0) + self.assertTrue(results["data"]["benefits"]["age 62"] != 0) dob = dob.replace(year=self.today.year - 45) results = interpolate_benefits(mock_results, 2176, (67, 0), 45, dob) - self.assertTrue(results['data']['benefits']['age 62'] != 0) - dob = self.today - datetime.timedelta(days=365*64) + self.assertTrue(results["data"]["benefits"]["age 62"] != 0) + dob = self.today - datetime.timedelta(days=365 * 64) results = interpolate_benefits(mock_results, 2176, (66, 0), 64, dob) - for key in sorted(results['data']['benefits'].keys())[2:]: - self.assertTrue(results['data']['benefits'][key] != 0) - dob = self.today - datetime.timedelta(days=365*65) + for key in sorted(results["data"]["benefits"].keys())[2:]: + self.assertTrue(results["data"]["benefits"][key] != 0) + dob = self.today - datetime.timedelta(days=365 * 65) results = interpolate_benefits(mock_results, 2176, (66, 0), 65, dob) - for key in sorted(results['data']['benefits'].keys())[3:]: - self.assertTrue(results['data']['benefits'][key] != 0) - dob = self.today - datetime.timedelta(days=365*63) + for key in sorted(results["data"]["benefits"].keys())[3:]: + self.assertTrue(results["data"]["benefits"][key] != 0) + dob = self.today - datetime.timedelta(days=365 * 63) results = interpolate_benefits(mock_results, 2176, (66, 0), 63, dob) - for key in sorted(results['data']['benefits'].keys())[1:]: - self.assertTrue(results['data']['benefits'][key] != 0) + for key in sorted(results["data"]["benefits"].keys())[1:]: + self.assertTrue(results["data"]["benefits"][key] != 0) def test_parse_details(self): sample_rows = [ - "early: Base year for indexing is 2013. Bend points are 826 & 4980", - "AIME = 2930 & PIA in 2018 is 1416.6.", - "PIA in 2018 after COLAs is $1,416.60." - ] - output = {'EARLY': - {'AIME': 'AIME = 2930 & PIA in 2018 is 1416.6.', - 'Bend points': 'Base year for indexing is 2013. Bend points are 826 & 4980', - 'COLA': 'PIA in 2018 after COLAs is $1,416.60.'}} + "early: Base year for indexing is 2013. " + "Bend points are 826 & 4980", + "AIME = 2930 & PIA in 2018 is 1416.6.", + "PIA in 2018 after COLAs is $1,416.60.", + ] + output = { + "EARLY": { + "AIME": "AIME = 2930 & PIA in 2018 is 1416.6.", + "Bend points": "Base year for indexing is 2013. " + "Bend points are 826 & 4980", + "COLA": "PIA in 2018 after COLAs is $1,416.60.", + } + } self.assertEqual(parse_details(sample_rows), output) def test_parse_response(self): - result = parse_response({}, '', 'en') + result = parse_response({}, "", "en") self.assertTrue(result[1] == 0) - self.assertTrue('error' in result[0]) - self.assertTrue('responding' in result[0]['note']) - result = parse_response({}, '', 'es') - self.assertTrue('error' in result[0]) - self.assertTrue('respondiendo' in result[0]['note']) + self.assertTrue("error" in result[0]) + self.assertTrue("responding" in result[0]["note"]) + result = parse_response({}, "", "es") + self.assertTrue("error" in result[0]) + self.assertTrue("respondiendo" in result[0]["note"]) self.assertTrue(result[1] == 0) - def check_interpolate_for_past_fra(self, today, dob, base_benefit, - expected_benefits): + def check_interpolate_for_past_fra( + self, today, dob, base_benefit, expected_benefits + ): """Tests benefits of retirees past full retirement age.""" - mock_results = {'data': {'benefits': {}}} + mock_results = {"data": {"benefits": {}}} - current_age = relativedelta(today, dob).years + # current_age = relativedelta(today, dob).years with freeze_time(today): results = interpolate_for_past_fra( results=mock_results, base=base_benefit, current_age=relativedelta(today, dob).years, - dob=dob + dob=dob, ) - self.assertEqual(results['data']['benefits'], expected_benefits) + self.assertEqual(results["data"]["benefits"], expected_benefits) def test_interpolate_for_past_fra_68(self): self.check_interpolate_for_past_fra( @@ -373,16 +407,16 @@ def test_interpolate_for_past_fra_68(self): base_benefit=1431, expected_benefits={ # Benefit at current age should equal base benefit. - 'age 68': 1431, + "age 68": 1431, # Turning 69 in 6 months. Waiting to retire until age 69 # should equal base benefit plus a bump equal to 6 * .667%. # 1431 * (1 + (.00667 * 6)) = 1488 - 'age 69': 1488, + "age 69": 1488, # Turning 70 in one year plus six months. Waiting to retire # until then adds another 8% of base benefit. # 1431 * (1 + (.00667 * 6) + (.08)) = 1602 - 'age 70': 1602, - } + "age 70": 1602, + }, ) def test_interpolate_for_past_fra_68_born_on_the_1st_next_month(self): @@ -399,34 +433,34 @@ def test_interpolate_for_past_fra_68_born_on_the_1st_next_month(self): base_benefit=1431, expected_benefits={ # Benefit at "current age" should equal base benefit. - 'age 69': 1431, + "age 69": 1431, # Turning 70 in one year. Waiting to retire until then adds # another 8% of base benefit. # 1431 * (1 + .08) = 1545 - 'age 70': 1545, - } + "age 70": 1545, + }, ) def test_validate_date_invalid(self): - test_params = {'yob': 1952, 'dobmon': 2, 'dobday': 30} + test_params = {"yob": 1952, "dobmon": 2, "dobday": 30} validate = validate_date(test_params) self.assertIs(validate, False) def test_validate_date_valid(self): - test_params = {'yob': 1952, 'dobmon': 2, 'dobday': 29} + test_params = {"yob": 1952, "dobmon": 2, "dobday": 29} validate = validate_date(test_params) self.assertIs(validate, True) def test_num_test(self): inputs = [ - ("", False), - ("a", False), - ("3c", False), - ("4", True), - (4, True), - (4.4, True), + ("", False), + ("a", False), + ("3c", False), + ("4", True), + (4, True), + (4.4, True), ("55.0", True), - ("0.55", True) + ("0.55", True), ] for tup in inputs: self.assertEqual(num_test(tup[0]), tup[1]) @@ -454,32 +488,34 @@ def test_get_retirement_age(self): "1959": (66, 10), "1960": (67, 0), "1980": (67, 0), - '198': None, - 'abc': None, - str(self.today.year+1): None, + "198": None, + "abc": None, + str(self.today.year + 1): None, } for year in sample_inputs: self.assertEqual(get_retirement_age(year), sample_inputs[year]) def test_past_fra_test(self): - one_one = "{0}".format(date(1980, 1, 1).replace(year=self.today.year-25)) - way_old = "{0}".format(self.today-timedelta(days=80*365)) - too_old = "{0}".format(self.today-timedelta(days=68*365)) - ok = "{0}".format(self.today-timedelta(days=57*365)) - too_young = "{0}".format(self.today-timedelta(days=21*365)) - future = "{0}".format(self.today+timedelta(days=365)) - edge = "{0}".format(self.today-timedelta(days=67*365)) + one_one = "{0}".format( + date(1980, 1, 1).replace(year=self.today.year - 25) + ) + way_old = "{0}".format(self.today - timedelta(days=80 * 365)) + too_old = "{0}".format(self.today - timedelta(days=68 * 365)) + ok = "{0}".format(self.today - timedelta(days=57 * 365)) + too_young = "{0}".format(self.today - timedelta(days=21 * 365)) + future = "{0}".format(self.today + timedelta(days=365)) + edge = "{0}".format(self.today - timedelta(days=67 * 365)) invalid = "xx/xx/xxxx" - self.assertTrue(past_fra_test(one_one, language='en') == False) - self.assertTrue(past_fra_test(too_old, language='en') == True) - self.assertTrue(past_fra_test(too_old, language='es') == True) - self.assertTrue(past_fra_test(ok, language='en') == False) - self.assertTrue("22" in past_fra_test(too_young, language='en')) - self.assertTrue("sentimos" in past_fra_test(too_young, language='es')) - self.assertTrue("22" in past_fra_test(future, language='en')) - self.assertTrue("70" in past_fra_test(way_old, language='en')) - self.assertTrue(past_fra_test(edge, language='en') == True) - self.assertTrue("invalid" in past_fra_test(invalid, language='en')) + self.assertFalse(past_fra_test(one_one, language="en")) + self.assertTrue(past_fra_test(too_old, language="en")) + self.assertTrue(past_fra_test(too_old, language="es")) + self.assertFalse(past_fra_test(ok, language="en")) + self.assertTrue("22" in past_fra_test(too_young, language="en")) + self.assertTrue("sentimos" in past_fra_test(too_young, language="es")) + self.assertTrue("22" in past_fra_test(future, language="en")) + self.assertTrue("70" in past_fra_test(way_old, language="en")) + self.assertTrue(past_fra_test(edge, language="en")) + self.assertTrue("invalid" in past_fra_test(invalid, language="en")) self.assertTrue("invalid" in past_fra_test()) def test_age_map(self): @@ -508,13 +544,13 @@ def test_get_delay_bonus(self): def test_yob_test(self): sample_inputs = { "1933": "1933", - str(self.today.year+2): None, + str(self.today.year + 2): None, "935": None, "1957": "1957", "1979": "1979", "abc": None, 1980: "1980", - None: None + None: None, } for year in sample_inputs: self.assertEqual(yob_test(year), sample_inputs[year]) @@ -558,102 +594,106 @@ def test_get_retire_data(self): return a dictionary of social security values """ params = copy.copy(self.sample_params) - data_keys = ['early retirement age', - 'full retirement age', - 'lifetime', - 'benefits', - 'params', - 'disability', - 'months_past_birthday', - 'survivor benefits'] - benefit_keys = ['age 62', - 'age 63', - 'age 64', - 'age 65', - 'age 66', - 'age 67', - 'age 68', - 'age 69', - 'age 70'] - data = get_retire_data(params, language='en')['data'] - self.assertEqual(data['params']['yob'], 1970) + data_keys = [ + "early retirement age", + "full retirement age", + "lifetime", + "benefits", + "params", + "disability", + "months_past_birthday", + "survivor benefits", + ] + benefit_keys = [ + "age 62", + "age 63", + "age 64", + "age 65", + "age 66", + "age 67", + "age 68", + "age 69", + "age 70", + ] + data = get_retire_data(params, language="en")["data"] + self.assertEqual(data["params"]["yob"], 1970) for each in data.keys(): self.assertTrue(each in data_keys) - for each in data['benefits'].keys(): + for each in data["benefits"].keys(): self.assertTrue(each in benefit_keys) - params['dobday'] = 1 - params['dobmon'] = 6 - data = get_retire_data(params, language='en')['data'] - self.assertEqual(data['params']['yob'], 1970) - params['yob'] = self.today.year-62 - params['dobmon'] = self.today.month - params['dobday'] = self.today.day - data = get_retire_data(params, language='en') - self.assertTrue(data['data']['benefits']['age 62'] != 0) - params['yob'] = 1937 - data = get_retire_data(params, language='en') - self.assertEqual(data['data']['params']['yob'], 1937) - self.assertTrue('70' in data['note']) - params['yob'] = self.today.year-21 - data = get_retire_data(params, language='en') - self.assertTrue("22" in data['note']) - params['yob'] = self.today.year-57 - data = get_retire_data(params, language='en') - self.assertTrue(data['data']['benefits']['age 62'] != 0) - self.assertTrue(data['data']['benefits']['age 70'] != 0) - params['yob'] = self.today.year-64 - data = get_retire_data(params, language='en') - self.assertTrue(data['data']['benefits']['age 70'] != 0) - params['yob'] = self.today.year-65 - data = get_retire_data(params, language='en') - self.assertTrue(data['data']['benefits']['age 70'] != 0) - params['yob'] = self.today.year-66 - data = get_retire_data(params, language='en') - self.assertTrue(data['data']['benefits']['age 70'] != 0) - self.assertTrue(data['data']['benefits']['age 66'] != 0) - params['yob'] = self.today.year-67 - data = get_retire_data(params, language='en') - self.assertTrue(data['data']['benefits']['age 70'] != 0) - params['yob'] = self.today.year-68 - data = get_retire_data(params, language='en') - self.assertTrue(data['data']['benefits']['age 70'] != 0) - params['yob'] = self.today.year-69 - data = get_retire_data(params, language='en') - self.assertTrue(data['data']['benefits']['age 70'] != 0) - params['yob'] = self.today.year-70 - data = get_retire_data(params, language='en') - self.assertTrue(data['data']['benefits']['age 70'] != 0) - params['earnings'] = 0 - data = get_retire_data(params, language='en') - self.assertTrue("zero" in data['error']) - params['yob'] = self.today.year-45 - data = get_retire_data(params, language='en') - self.assertTrue("zero" in data['error'] or "SSA" in data['error']) - params['earnings'] = 100000 - params['yob'] = self.today.year-68 - data = get_retire_data(params, language='en') - self.assertTrue("past" in data['note']) - params['yob'] = self.today.year + 1 - data = get_retire_data(params, language='en') - self.assertTrue("22" in data['note']) - - @mock.patch('retirement_api.utils.ss_calculator.requests.post') + params["dobday"] = 1 + params["dobmon"] = 6 + data = get_retire_data(params, language="en")["data"] + self.assertEqual(data["params"]["yob"], 1970) + params["yob"] = self.today.year - 62 + params["dobmon"] = self.today.month + params["dobday"] = self.today.day + data = get_retire_data(params, language="en") + self.assertTrue(data["data"]["benefits"]["age 62"] != 0) + params["yob"] = 1937 + data = get_retire_data(params, language="en") + self.assertEqual(data["data"]["params"]["yob"], 1937) + self.assertTrue("70" in data["note"]) + params["yob"] = self.today.year - 21 + data = get_retire_data(params, language="en") + self.assertTrue("22" in data["note"]) + params["yob"] = self.today.year - 57 + data = get_retire_data(params, language="en") + self.assertTrue(data["data"]["benefits"]["age 62"] != 0) + self.assertTrue(data["data"]["benefits"]["age 70"] != 0) + params["yob"] = self.today.year - 64 + data = get_retire_data(params, language="en") + self.assertTrue(data["data"]["benefits"]["age 70"] != 0) + params["yob"] = self.today.year - 65 + data = get_retire_data(params, language="en") + self.assertTrue(data["data"]["benefits"]["age 70"] != 0) + params["yob"] = self.today.year - 66 + data = get_retire_data(params, language="en") + self.assertTrue(data["data"]["benefits"]["age 70"] != 0) + self.assertTrue(data["data"]["benefits"]["age 66"] != 0) + params["yob"] = self.today.year - 67 + data = get_retire_data(params, language="en") + self.assertTrue(data["data"]["benefits"]["age 70"] != 0) + params["yob"] = self.today.year - 68 + data = get_retire_data(params, language="en") + self.assertTrue(data["data"]["benefits"]["age 70"] != 0) + params["yob"] = self.today.year - 69 + data = get_retire_data(params, language="en") + self.assertTrue(data["data"]["benefits"]["age 70"] != 0) + params["yob"] = self.today.year - 70 + data = get_retire_data(params, language="en") + self.assertTrue(data["data"]["benefits"]["age 70"] != 0) + params["earnings"] = 0 + data = get_retire_data(params, language="en") + self.assertTrue("zero" in data["error"]) + params["yob"] = self.today.year - 45 + data = get_retire_data(params, language="en") + self.assertTrue("zero" in data["error"] or "SSA" in data["error"]) + params["earnings"] = 100000 + params["yob"] = self.today.year - 68 + data = get_retire_data(params, language="en") + self.assertTrue("past" in data["note"]) + params["yob"] = self.today.year + 1 + data = get_retire_data(params, language="en") + self.assertTrue("22" in data["note"]) + + @mock.patch("retirement_api.utils.ss_calculator.requests.post") def test_bad_calculator_requests(self, mock_requests): params = copy.copy(self.sample_params) mock_requests.return_value.ok = False - mock_results = get_retire_data(params, language='en') - self.assertTrue('not responding' in mock_results['error']) + mock_results = get_retire_data(params, language="en") + self.assertTrue("not responding" in mock_results["error"]) mock_requests.side_effect = requests.exceptions.RequestException - mock_results = get_retire_data(params, language='en') - self.assertTrue('request error' in mock_results['error']) - mock_results = get_retire_data(params, language='es') - self.assertTrue('request error' in mock_results['error']) + mock_results = get_retire_data(params, language="en") + self.assertTrue("request error" in mock_results["error"]) + mock_results = get_retire_data(params, language="es") + self.assertTrue("request error" in mock_results["error"]) mock_requests.side_effect = requests.exceptions.ConnectionError - mock_results = get_retire_data(params, language='en') - self.assertTrue('connection error' in mock_results['error']) + mock_results = get_retire_data(params, language="en") + self.assertTrue("connection error" in mock_results["error"]) mock_requests.side_effect = requests.exceptions.Timeout - mock_results = get_retire_data(params, language='en') - self.assertTrue('timed out' in mock_results['error']) + mock_results = get_retire_data(params, language="en") + self.assertTrue("timed out" in mock_results["error"]) mock_requests.side_effect = ValueError - mock_results = get_retire_data(params, language='en') - self.assertTrue('SSA' in mock_results['error']) + mock_results = get_retire_data(params, language="en") + self.assertTrue("SSA" in mock_results["error"]) diff --git a/retirement_api/views.py b/retirement_api/views.py index 9c4dbd4..53c474b 100644 --- a/retirement_api/views.py +++ b/retirement_api/views.py @@ -1,19 +1,22 @@ -import os +import datetime import json +import os from django.conf import settings -from django.shortcuts import render from django.http import HttpResponse, HttpResponseBadRequest +from django.shortcuts import render +from django.utils.translation import activate, deactivate_all, ugettext as _ + +from dateutil import parser +from retirement_api.models import AgeChoice, Page, Question, Step, Tooltip + from .utils.ss_calculator import get_retire_data from .utils.ss_utilities import get_retirement_age -from dateutil import parser -import datetime -from retirement_api.models import Step, AgeChoice, Page, Tooltip, Question -from django.utils.translation import ugettext as _ -from django.utils.translation import activate, deactivate_all + + BASEDIR = os.path.dirname(__file__) -standalone = getattr(settings, 'STANDALONE', False) +standalone = getattr(settings, "STANDALONE", False) if standalone: base_template = "retirement_api/standalone/base_update.html" @@ -23,15 +26,15 @@ def claiming(request, es=False): if es is True: - activate('es') - language = 'es' + activate("es") + language = "es" else: - language = 'en' + language = "en" deactivate_all() ages = {} for age in AgeChoice.objects.all(): ages[age.age] = _(age.aside) - page = Page.objects.get(title='Planning your Social Security claiming age') + page = Page.objects.get(title="Planning your Social Security claiming age") tips = {} for tooltip in Tooltip.objects.all(): tips[tooltip.title] = tooltip.text @@ -43,19 +46,19 @@ def claiming(request, es=False): steps[step.title] = step.trans_instructions(language=language) cdict = { - 'tstamp': datetime.datetime.now(), - 'steps': steps, - 'questions': questions, - 'tips': tips, - 'ages': ages, - 'page': page, - 'base_template': base_template, - 'available_languages': ['en', 'es'], - 'es': es, - 'about_view_name': 'retirement_api:' + ('about_es' if es else 'about'), + "tstamp": datetime.datetime.now(), + "steps": steps, + "questions": questions, + "tips": tips, + "ages": ages, + "page": page, + "base_template": base_template, + "available_languages": ["en", "es"], + "es": es, + "about_view_name": "retirement_api:" + ("about_es" if es else "about"), } - return render(request, 'retirement_api/claiming.html', cdict) + return render(request, "retirement_api/claiming.html", cdict) def param_check(request, param): @@ -66,7 +69,7 @@ def param_check(request, param): def income_check(param): - cleaned = param.replace('$', '').replace(',', '').partition('.')[0] + cleaned = param.replace("$", "").replace(",", "").partition(".")[0] try: clean_income = int(cleaned) except ValueError: @@ -75,25 +78,25 @@ def income_check(param): return clean_income -def estimator(request, dob=None, income=None, language='en'): +def estimator(request, dob=None, income=None, language="en"): ssa_params = { - 'dobmon': 0, - 'dobday': 0, - 'yob': 0, - 'earnings': 0, - 'lastYearEarn': '', # not using - 'lastEarn': '', # not using - 'retiremonth': '', # only using for past-FRA users - 'retireyear': '', # only using for past-FRA users - 'dollars': 1, # benefits to be calculated in current-year dollars - 'prgf': 2 + "dobmon": 0, + "dobday": 0, + "yob": 0, + "earnings": 0, + "lastYearEarn": "", # not using + "lastEarn": "", # not using + "retiremonth": "", # only using for past-FRA users + "retireyear": "", # only using for past-FRA users + "dollars": 1, # benefits to be calculated in current-year dollars + "prgf": 2, } if dob is None: - dob = param_check(request, 'dob') + dob = param_check(request, "dob") if not dob: return HttpResponseBadRequest("invalid date of birth") if income is None: - income_raw = param_check(request, 'income') + income_raw = param_check(request, "income") if not income_raw: return HttpResponseBadRequest("invalid income") else: @@ -110,12 +113,12 @@ def estimator(request, dob=None, income=None, language='en'): return HttpResponseBadRequest("invalid date of birth") else: DOB = dob_parsed.date() - ssa_params['dobmon'] = DOB.month - ssa_params['dobday'] = DOB.day - ssa_params['yob'] = DOB.year - ssa_params['earnings'] = income + ssa_params["dobmon"] = DOB.month + ssa_params["dobday"] = DOB.day + ssa_params["yob"] = DOB.year + ssa_params["earnings"] = income data = get_retire_data(ssa_params, language) - return HttpResponse(json.dumps(data), content_type='application/json') + return HttpResponse(json.dumps(data), content_type="application/json") def get_full_retirement_age(request, birth_year): @@ -124,20 +127,20 @@ def get_full_retirement_age(request, birth_year): return HttpResponseBadRequest("bad birth year (%s)" % birth_year) else: data = json.dumps(data_tuple) - return HttpResponse(data, content_type='application/json') + return HttpResponse(data, content_type="application/json") -def about(request, language='en'): +def about(request, language="en"): """Return our 'about' calculation-explainer page in Engish or Spanish""" - if language == 'es': - activate('es') + if language == "es": + activate("es") es = True else: deactivate_all() es = False cdict = { - 'base_template': base_template, - 'available_languages': ['en', 'es'], - 'es': es + "base_template": base_template, + "available_languages": ["en", "es"], + "es": es, } - return render(request, 'retirement_api/about.html', cdict) + return render(request, "retirement_api/about.html", cdict) diff --git a/settings/standalone.py b/settings/standalone.py index ce8706b..d3fceec 100755 --- a/settings/standalone.py +++ b/settings/standalone.py @@ -31,7 +31,7 @@ 'retirement_api', ) -MIDDLEWARE_CLASSES = ( +MIDDLEWARE = ( 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.locale.LocaleMiddleware', 'django.middleware.common.CommonMiddleware', diff --git a/settings/test.py b/settings/test.py index 2e0594c..5474782 100755 --- a/settings/test.py +++ b/settings/test.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import - from .standalone import * LOGGING = { diff --git a/setup.py b/setup.py index 9c3c9c8..bade3e4 100644 --- a/setup.py +++ b/setup.py @@ -3,24 +3,24 @@ install_requires = [ - 'beautifulsoup4>=4.5.0,<4.7', - 'Django>=1.11,<1.12', - 'dj-database-url>=0.4.2,<1', - 'python-dateutil>=2.1<3', - 'requests>=2.18,<3', + "beautifulsoup4>=4.5.0,<4.7", + "Django>=1.11,<2.3", + "dj-database-url>=0.4.2,<1", + "python-dateutil>=2.1<3", + "requests>=2.18,<3", ] setup_requires = [ - 'cfgov-setup==1.2', - 'setuptools-git-version==1.0.3', + "cfgov-setup==1.2", + "setuptools-git-version==1.0.3", ] testing_extras = [ - 'coverage>=4.5.1,<5', - 'freezegun>=0.3.1,<1', - 'mock==2.0.0', + "coverage>=4.5.1,<5", + "freezegun>=0.3.1,<1", + "mock==2.0.0", ] @@ -31,34 +31,32 @@ def read_file(filename): try: return open(filepath).read() except IOError: - return '' + return "" setup( - name='retirement', - author='CFPB', - author_email='tech@cfpb.gov', - version_format='{tag}.dev{commitcount}+{gitsha}', - maintainer='cfpb', - maintainer_email='tech@cfpb.gov', - packages=['retirement_api', 'retirement_api.utils'], + name="retirement", + author="CFPB", + author_email="tech@cfpb.gov", + version_format="{tag}.dev{commitcount}+{gitsha}", + maintainer="cfpb", + maintainer_email="tech@cfpb.gov", + packages=["retirement_api", "retirement_api.utils"], include_package_data=True, - description=u'Retirement app and api', + description="Retirement app and api", classifiers=[ - 'Topic :: Internet :: WWW/HTTP', - 'Intended Audience :: Developers', - 'Programming Language :: Python', - 'Programming Language :: Python :: 3.6', - 'Framework :: Django', - 'Development Status :: 4 - Beta', - 'Operating System :: OS Independent', + "Topic :: Internet :: WWW/HTTP", + "Intended Audience :: Developers", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Framework :: Django", + "Development Status :: 4 - Beta", + "Operating System :: OS Independent", ], - long_description=read_file('README.md'), + long_description=read_file("README.md"), zip_safe=False, install_requires=install_requires, setup_requires=setup_requires, - extras_require={ - 'testing': testing_extras, - }, - frontend_build_script='frontendbuild.sh' + extras_require={"testing": testing_extras,}, + frontend_build_script="frontendbuild.sh", ) diff --git a/tox.ini b/tox.ini index c1b36c1..4221eca 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] skipsdist=True -envlist=py{36}-dj{111} +envlist=lint,py{36}-dj{111,22} [testenv] install_command=pip install -e ".[testing]" -U {opts} {packages} @@ -11,6 +11,43 @@ commands= basepython= py36: python3.6 + py38: python3.8 deps= dj111: Django>=1.11,<1.12 + dj22: Django>=2.2,<2.3 + +[testenv:lint] +recreate=False +basepython=python3.6 +deps= + black + flake8 + isort +commands= + black --check retirement_api setup.py + flake8 retirement_api + isort --check-only --diff --recursive retirement_api + +[flake8] +ignore=E731,W503,W504 +exclude= + .git, + .tox, + __pycache__, + node_modules, + */migrations/*.py, + .eggs/*, + +[isort] +combine_as_imports=1 +lines_after_imports=2 +include_trailing_comma=1 +multi_line_output=3 +skip=.tox,migrations +not_skip=__init__.py +use_parentheses=1 +known_django=django +known_future_library=future +default_section=THIRDPARTY +sections=FUTURE,STDLIB,DJANGO,THIRDPARTY,FIRSTPARTY,LOCALFOLDER