Merge branch 'main' into sspj/domain-interface

This commit is contained in:
Seamus Johnston 2023-06-01 16:00:19 -05:00
commit 66640ebafb
No known key found for this signature in database
GPG key ID: 2F21225985069105
345 changed files with 350 additions and 19266 deletions

View file

@ -23,8 +23,10 @@ requests = "*"
django-fsm = "*"
django-phonenumber-field = {extras = ["phonenumberslite"], version = "*"}
boto3 = "*"
typing-extensions ='*'
fred-epplib = {git = "https://github.com/cisagov/epplib.git", ref = "master"}
[dev-packages]
django-debug-toolbar = "*"
nplusone = "*"

42
src/Pipfile.lock generated
View file

@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
"sha256": "14b5ea3c520cf927e27ae76936802a586dc0ff78e91fefdeb71125a2a320c301"
"sha256": "fd7d0efa9a87dfe4b2bb228ee0e7978fba16c7cfdd3c443870900cfe899e2cfd"
},
"pipfile-spec": 6,
"requires": {},
@ -24,19 +24,19 @@
},
"boto3": {
"hashes": [
"sha256:2eb9e688aa86bf1fadcec0b6995b42ec9788e7cd5f1a9c8ac1b66a2506aa209f",
"sha256:5b7e9f2674fe8aa99e2d168744023a3f66da12d9c51e0624489dd0db7aafe30d"
"sha256:30f8ab1cf89d5864a80ba2d5eb5316dbd2a63c9469877e0cffb522630438aa85",
"sha256:77e8fa7c257f9ed8bfe0c3ffc2ccc47b1cfa27058f99415b6003699d1202e0c0"
],
"index": "pypi",
"version": "==1.26.144"
"version": "==1.26.145"
},
"botocore": {
"hashes": [
"sha256:c60b9158cbc7447411abdec77b87a71d86d9404064702e92d317dca6a1ec9a5b",
"sha256:e2b970e68643cf4752cad4e45ba3319fc35707f1bff7f150f7ffcac1b1427b47"
"sha256:264a3f19ed280d80711b7e278be09acff7ed379a96432fdf179b4e6e3a687e6a",
"sha256:65e2a2b1cc70583225f87d6d63736215f93c6234721967bdab872270ba7a1f45"
],
"markers": "python_version >= '3.7'",
"version": "==1.29.144"
"version": "==1.29.145"
},
"cachetools": {
"hashes": [
@ -794,7 +794,7 @@
"sha256:06006244c70ac8ee83fa8282cb188f697b8db25bc8b4df07be1873c43897060c",
"sha256:3a8b36f13dd5fdc5d1b16fe317f5668545de77fa0b8e02006381fd49d731ab98"
],
"markers": "python_version < '3.11'",
"index": "pypi",
"version": "==4.6.2"
},
"urllib3": {
@ -880,11 +880,11 @@
},
"boto3": {
"hashes": [
"sha256:2eb9e688aa86bf1fadcec0b6995b42ec9788e7cd5f1a9c8ac1b66a2506aa209f",
"sha256:5b7e9f2674fe8aa99e2d168744023a3f66da12d9c51e0624489dd0db7aafe30d"
"sha256:30f8ab1cf89d5864a80ba2d5eb5316dbd2a63c9469877e0cffb522630438aa85",
"sha256:77e8fa7c257f9ed8bfe0c3ffc2ccc47b1cfa27058f99415b6003699d1202e0c0"
],
"index": "pypi",
"version": "==1.26.144"
"version": "==1.26.145"
},
"boto3-mocking": {
"hashes": [
@ -896,27 +896,27 @@
},
"boto3-stubs": {
"hashes": [
"sha256:1062612f63f154f47a4f5b7b40c2cb15debe5f44774587110da76fd292f528f0",
"sha256:bc0cc5067f55b2da628db8a73119ecccd74b27cf424af83e56526cd90beaf9f8"
"sha256:9413cb395c803d5b85e9ec7b16fba855a613ecd78b2e0011e2f6b62cf0b4fc1e",
"sha256:be2007f92138781288c7a22eba30b7d60742466fc28edd04637b31fabee854a5"
],
"index": "pypi",
"version": "==1.26.144"
"version": "==1.26.145"
},
"botocore": {
"hashes": [
"sha256:c60b9158cbc7447411abdec77b87a71d86d9404064702e92d317dca6a1ec9a5b",
"sha256:e2b970e68643cf4752cad4e45ba3319fc35707f1bff7f150f7ffcac1b1427b47"
"sha256:264a3f19ed280d80711b7e278be09acff7ed379a96432fdf179b4e6e3a687e6a",
"sha256:65e2a2b1cc70583225f87d6d63736215f93c6234721967bdab872270ba7a1f45"
],
"markers": "python_version >= '3.7'",
"version": "==1.29.144"
"version": "==1.29.145"
},
"botocore-stubs": {
"hashes": [
"sha256:b9db32981b4deefb01784d9b196afeaca7df6f6f185d8ba7f96c02b1c3bc0d90",
"sha256:d456543af79fbdd23df76a2d7a7525cd672b4bb5b057d7e060bc117d9af71694"
"sha256:80ffab72ad428d20cb1cf538ee55fcd94f7d81315b77d84fec99e218c3974e8b",
"sha256:928c58a434dd83bef956e3b5bb1e96278fff5eee9f8b8ab08d916cef1e9a2014"
],
"markers": "python_version >= '3.7' and python_version < '4.0'",
"version": "==1.29.144"
"version": "==1.29.145"
},
"click": {
"hashes": [
@ -1309,7 +1309,7 @@
"sha256:06006244c70ac8ee83fa8282cb188f697b8db25bc8b4df07be1873c43897060c",
"sha256:3a8b36f13dd5fdc5d1b16fe317f5668545de77fa0b8e02006381fd49d731ab98"
],
"markers": "python_version < '3.11'",
"index": "pypi",
"version": "==4.6.2"
},
"urllib3": {

View file

@ -27,6 +27,8 @@ services:
- DJANGO_DEBUG=True
# Tell Django where it is being hosted
- DJANGO_BASE_URL=http://localhost:8080
# Public site URL link
- GETGOV_PUBLIC_SITE_URL=https://beta.get.gov
# Set a username for accessing the registry
- REGISTRY_CL_ID=nothing
# Set a password for accessing the registry

View file

@ -77,6 +77,7 @@ h2 {
font-weight: font-weight('semibold');
line-height: line-height('heading', 3);
margin: units(4) 0 units(1);
color: color('primary-darker');
}
.register-form-step > h1 {
@ -431,4 +432,4 @@ abbr[title] {
@include at-media('tablet') {
height: units('mobile');
}
}
}

View file

@ -62,6 +62,8 @@ secret_registry_key = b64decode(secret("REGISTRY_KEY", ""))
secret_registry_key_passphrase = secret("REGISTRY_KEY_PASSPHRASE", "")
secret_registry_hostname = secret("REGISTRY_HOSTNAME")
secret_getgov_public_site_url = secret("GETGOV_PUBLIC_SITE_URL", "")
# region: Basic Django Config-----------------------------------------------###
# Build paths inside the project like this: BASE_DIR / "subdir".
@ -505,6 +507,10 @@ ROOT_URLCONF = "registrar.config.urls"
# Must be relative and end with "/"
STATIC_URL = "public/"
# Base URL of our separate static public website. Used by the
# {% public_site_url subdir/path %} template tag
GETGOV_PUBLIC_SITE_URL = secret_getgov_public_site_url
# endregion
# region: Registry----------------------------------------------------------###

View file

@ -88,6 +88,11 @@ urlpatterns = [
views.DomainYourContactInformationView.as_view(),
name="domain-your-contact-information",
),
path(
"domain/<int:pk>/authorizing-official",
views.DomainAuthorizingOfficialView.as_view(),
name="domain-authorizing-official",
),
path(
"domain/<int:pk>/security-email",
views.DomainSecurityEmailView.as_view(),

View file

@ -28,13 +28,6 @@ NameserverFormset = formset_factory(
)
class DomainSecurityEmailForm(forms.Form):
"""Form for adding or editing a security email to a domain."""
security_email = forms.EmailField(label="Security email")
class ContactForm(forms.ModelForm):
"""Form for updating contacts."""
@ -59,8 +52,15 @@ class ContactForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# take off maxlength attribute for the phone number field
# which interferes with our input_with_errors template tag
# which interferes with out input_with_errors template tag
self.fields["phone"].widget.attrs.pop("maxlength", None)
for field_name in self.required:
self.fields[field_name].required = True
class DomainSecurityEmailForm(forms.Form):
"""Form for adding or editing a security email to a domain."""
security_email = forms.EmailField(label="Security email")

View file

@ -0,0 +1,19 @@
# Generated by Django 4.2.1 on 2023-06-01 19:23
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("registrar", "0023_alter_contact_first_name_alter_contact_last_name_and_more"),
]
operations = [
migrations.AlterField(
model_name="contact",
name="email",
field=models.EmailField(
blank=True, db_index=True, help_text="Email", max_length=254, null=True
),
),
]

View file

@ -41,7 +41,7 @@ class Contact(TimeStampedModel):
help_text="Title",
verbose_name="title or role in your organization",
)
email = models.TextField(
email = models.EmailField(
null=True,
blank=True,
help_text="Email",

View file

@ -562,11 +562,13 @@ class DomainApplication(TimeStampedModel):
"""Show this step if the answer to the first question implies it.
This shows for answers that aren't "Federal" or "Interstate".
This also doesnt show if user selected "School District" as well (#524)
"""
user_choice = self.organization_type
excluded = [
DomainApplication.OrganizationChoices.FEDERAL,
DomainApplication.OrganizationChoices.INTERSTATE,
DomainApplication.OrganizationChoices.SCHOOL_DISTRICT,
]
return bool(user_choice and user_choice not in excluded)

View file

@ -1,5 +1,5 @@
{% extends 'application_form.html' %}
{% load field_helpers %}
{% load field_helpers url_helpers %}
{% block form_instructions %}
<p>.Gov domain names are for use on the internet. Dont register a .gov to simply reserve a
@ -8,7 +8,7 @@ domain name or for mainly internal use.</p>
<p>Describe the reason for your domain request. Explain how you plan to use this domain.
Who is your intended audience? Will you use it for a website and/or email? Are you moving
your website from another top-level domain (like .com or .org)?
Read about <a href="{% url 'todo' %}">activities that are prohibited on .gov domains.</a></p>
Read about <a href="{% public_site_url 'domains/requirements/' %}">activities that are prohibited on .gov domains.</a></p>
{% endblock %}

View file

@ -0,0 +1,43 @@
{% extends "domain_base.html" %}
{% load static field_helpers%}
{% block title %}Domain authorizing official | {{ domain.name }} | {% endblock %}
{% block domain_content %}
{# this is right after the messages block in the parent template #}
{% include "includes/form_errors.html" with form=form %}
<h1>Authorizing official</h1>
<p>Your authorizing official is the person within your organization who can
authorize domain requests. This is generally the highest-ranking or
highest-elected official in your organization. <a class="usa-link"
href="https://federalist-877ab29f-16f6-4f12-961c-96cf064cf070.sites.pages.cloud.gov/site/cisagov/getgov-home/domains/eligibility/#you-must-have-approval-from-an-authorizing-official-within-your-organization">Read more about who can serve
as an authorizing official.</a></p>
{% include "includes/required_fields.html" %}
<form class="usa-form usa-form--large" method="post" novalidate id="form-container">
{% csrf_token %}
{% input_with_errors form.first_name %}
{% input_with_errors form.middle_name %}
{% input_with_errors form.last_name %}
{% input_with_errors form.title %}
{% input_with_errors form.email %}
{% input_with_errors form.phone %}
<button
type="submit"
class="usa-button"
>Save</button>
</form>
{% endblock %} {# domain_content #}

View file

@ -1,6 +1,33 @@
{% extends "domain_base.html" %}
{% load static url_helpers %}
{% block domain_content %}
{{ block.super }}
<p>Active: {% if domain.is_active %}Yes{% else %}No{% endif %}</p>
<div class="margin-top-4 tablet:grid-col-10">
{% url 'domain-nameservers' pk=domain.id as url %}
{% if domain.nameservers %}
{% include "includes/summary_item.html" with title='DNS name servers' value=domain.nameservers list='true' edit_link=url %}
{% else %}
<h2 class="margin-top-neg-1"> DNS name servers </h2>
<p> No DNS name servers have been added yet. Before your domain can be used well need information about your domain name servers.</p>
<a class="usa-button margin-bottom-1" href="{{url}}"> Add DNS name servers </a>
{% endif %}
{% url 'todo' as url %}
{% include "includes/summary_item.html" with title='Organization name and mailing address' value=domain.domain_info address='true' edit_link=url %}
{% url 'domain-authorizing-official' pk=domain.id as url %}
{% include "includes/summary_item.html" with title='Authorizing official' value=domain.domain_info.authorizing_official contact='true' edit_link=url %}
{% url 'domain-your-contact-information' pk=domain.id as url %}
{% include "includes/summary_item.html" with title='Your contact information' value=request.user.contact contact='true' edit_link=url %}
{% url 'domain-security-email' pk=domain.id as url %}
{% include "includes/summary_item.html" with title='Security email' value=domain.security_email edit_link=url %}
{% url 'domain-users' pk=domain.id as url %}
{% include "includes/summary_item.html" with title='User management' users='true' list=True value=domain.permissions.all edit_link=url %}
</div>
{% endblock %} {# domain_content #}

View file

@ -8,7 +8,7 @@
<a href="{{ url }}"
{% if request.path == url %}class="usa-current"{% endif %}
>
Domain Overview
Domain overview
</a>
</li>
@ -31,7 +31,7 @@
</li>
<li class="usa-sidenav__item">
{% url 'todo' as url %}
{% url 'domain-authorizing-official' pk=domain.id as url %}
<a href="{{ url }}"
{% if request.path == url %}class="usa-current"{% endif %}
>

View file

@ -1,11 +1,13 @@
{% load static url_helpers %}
<section class="summary-item margin-top-3">
<hr class="" />
<p class="summary-item__title
<hr class="" aria-hidden="true" />
<h2 class="summary-item__title
text-primary-dark text-semibold
margin-top-0 margin-bottom-05"
>
{{ title }}
</p>
>
{{ title }}
</h2>
{% if address %}
{% include "includes/organization_address.html" with organization=value %}
{% elif contact %}
@ -30,11 +32,19 @@
{% endif %}
{% elif list %}
{% if value|length == 1 %}
<p class="margin-top-0">{{ value | first }} </p>
{% if users %}
<p class="margin-top-0">{{ value.0.user.email }} </p>
{% else %}
<p class="margin-top-0">{{ value | first }} </p>
{% endif %}
{% else %}
<ul class="usa-list margin-top-0">
{% for item in value %}
<li>{{ item }}</li>
{% if users %}
<li>{{ item.user.email }}</li>
{% else %}
<li>{{ item }}</li>
{% endif %}
{% empty %}
<li>None</li>
{% endfor %}</ul></p>
@ -45,5 +55,13 @@
{{ value }}
</p>
{% endif %}
{% if edit_link %}
<a
href="{{ edit_link }}"
>
Edit<span class="sr-only"> {{ title }}</span>
</a>
{% endif %}
</section>

View file

@ -1,6 +1,8 @@
from django import template
from django.urls import reverse
from django.conf import settings
register = template.Library()
@ -15,3 +17,16 @@ def startswith(text, starts):
if isinstance(text, str):
return text.startswith(starts)
return False
@register.simple_tag
def public_site_url(url_path):
"""Make a full URL for this path at our public site.
The public site base url is set by a GETGOV_PUBLIC_SITE_URL environment
variable.
"""
base_url = settings.GETGOV_PUBLIC_SITE_URL
# join the two halves with a single slash
public_url = "/".join([base_url.rstrip("/"), url_path.lstrip("/")])
return public_url

View file

@ -15,6 +15,7 @@ from registrar.forms.application_wizard import (
AnythingElseForm,
TypeOfWorkForm,
)
from registrar.forms.domain import ContactForm
class TestFormValidation(TestCase):
@ -277,3 +278,13 @@ class TestFormValidation(TestCase):
for error in form.non_field_errors()
)
)
class TestContactForm(TestCase):
def test_contact_form_email_invalid(self):
form = ContactForm(data={"email": "example.net"})
self.assertEqual(form.errors["email"], ["Enter a valid email address."])
def test_contact_form_email_invalid2(self):
form = ContactForm(data={"email": "@"})
self.assertEqual(form.errors["email"], ["Enter a valid email address."])

View file

@ -0,0 +1,31 @@
"""Test template tags."""
from django.conf import settings
from django.test import TestCase
from django.template import Context, Template
class TestTemplateTags(TestCase):
def _render_template(self, string, context=None):
"""Helper method to render a template given as a string.
Originally from https://stackoverflow.com/a/1690879
"""
context = context or {}
context = Context(context)
return Template(string).render(context)
def test_public_site_url(self):
result = self._render_template(
"{% load url_helpers %}{% public_site_url 'directory/page' %}"
)
self.assertTrue(result.startswith(settings.GETGOV_PUBLIC_SITE_URL))
self.assertTrue(result.endswith("/directory/page"))
def test_public_site_url_leading_slash(self):
result = self._render_template(
"{% load url_helpers %}{% public_site_url '/directory/page' %}"
)
self.assertTrue(result.startswith(settings.GETGOV_PUBLIC_SITE_URL))
# slash-slash host slash directory slash page
self.assertEqual(result.count("/"), 4)

View file

@ -1058,6 +1058,7 @@ class TestDomainPermissions(TestWithDomainPermissions):
"domain-users",
"domain-users-add",
"domain-nameservers",
"domain-authorizing-official",
"domain-your-contact-information",
"domain-security-email",
]:
@ -1077,6 +1078,7 @@ class TestDomainPermissions(TestWithDomainPermissions):
"domain-users",
"domain-users-add",
"domain-nameservers",
"domain-authorizing-official",
"domain-your-contact-information",
"domain-security-email",
]:
@ -1087,12 +1089,6 @@ class TestDomainPermissions(TestWithDomainPermissions):
)
self.assertEqual(response.status_code, 403)
with less_console_noise():
response = self.client.get(
reverse("domain-security-email", kwargs={"pk": self.domain.id})
)
self.assertEqual(response.status_code, 403)
class TestDomainDetail(TestWithDomainPermissions, WebTest):
def setUp(self):
@ -1301,6 +1297,24 @@ class TestDomainDetail(TestWithDomainPermissions, WebTest):
# the field.
self.assertContains(result, "This field is required", count=2, status_code=200)
def test_domain_authorizing_official(self):
"""Can load domain's authorizing official page."""
page = self.client.get(
reverse("domain-authorizing-official", kwargs={"pk": self.domain.id})
)
# once on the sidebar, once in the title
self.assertContains(page, "Authorizing official", count=2)
def test_domain_authorizing_official_content(self):
"""Authorizing official information appears on the page."""
self.domain_information.authorizing_official = Contact(first_name="Testy")
self.domain_information.authorizing_official.save()
self.domain_information.save()
page = self.app.get(
reverse("domain-authorizing-official", kwargs={"pk": self.domain.id})
)
self.assertContains(page, "Testy")
def test_domain_your_contact_information(self):
"""Can load domain's your contact information page."""
page = self.client.get(
@ -1309,10 +1323,9 @@ class TestDomainDetail(TestWithDomainPermissions, WebTest):
self.assertContains(page, "Domain contact information")
def test_domain_your_contact_information_content(self):
"""Your contact information appears on the page."""
self.domain_information.submitter = Contact(first_name="Testy")
self.domain_information.submitter.save()
self.domain_information.save()
"""Logged-in user's contact information appears on the page."""
self.user.contact.first_name = "Testy"
self.user.contact.save()
page = self.app.get(
reverse("domain-your-contact-information", kwargs={"pk": self.domain.id})
)

View file

@ -1,6 +1,7 @@
from .application import *
from .domain import (
DomainView,
DomainAuthorizingOfficialView,
DomainNameserversView,
DomainYourContactInformationView,
DomainSecurityEmailView,

View file

@ -15,6 +15,7 @@ from django.urls import reverse
from django.views.generic.edit import FormMixin
from registrar.models import (
Domain,
DomainInvitation,
User,
UserDomainRole,
@ -40,6 +41,48 @@ class DomainView(DomainPermissionView):
template_name = "domain_detail.html"
class DomainAuthorizingOfficialView(DomainPermissionView, FormMixin):
"""Domain authorizing official editing view."""
model = Domain
template_name = "domain_authorizing_official.html"
context_object_name = "domain"
form_class = ContactForm
def get_form_kwargs(self, *args, **kwargs):
"""Add domain_info.authorizing_official instance to make a bound form."""
form_kwargs = super().get_form_kwargs(*args, **kwargs)
form_kwargs["instance"] = self.get_object().domain_info.authorizing_official
return form_kwargs
def get_success_url(self):
"""Redirect to the overview page for the domain."""
return reverse("domain-authorizing-official", kwargs={"pk": self.object.pk})
def post(self, request, *args, **kwargs):
"""Form submission posts to this view.
This post method harmonizes using DetailView and FormMixin together.
"""
self.object = self.get_object()
form = self.get_form()
if form.is_valid():
return self.form_valid(form)
else:
return self.form_invalid(form)
def form_valid(self, form):
"""The form is valid, save the authorizing official."""
form.save()
messages.success(
self.request, "The authorizing official for this domain has been updated."
)
# superclass has the redirect
return super().form_valid(form)
class DomainNameserversView(DomainPermissionView, FormMixin):
"""Domain nameserver editing view."""
@ -116,7 +159,7 @@ class DomainYourContactInformationView(DomainPermissionView, FormMixin):
def get_form_kwargs(self, *args, **kwargs):
"""Add domain_info.submitter instance to make a bound form."""
form_kwargs = super().get_form_kwargs(*args, **kwargs)
form_kwargs["instance"] = self.get_object().domain_info.submitter
form_kwargs["instance"] = self.request.user.contact
return form_kwargs
def get_success_url(self):

View file

@ -1,27 +1,27 @@
-i https://pypi.python.org/simple
asgiref==3.6.0 ; python_version >= '3.7'
boto3==1.26.69
botocore==1.29.69 ; python_version >= '3.7'
cachetools==5.3.0
certifi==2022.12.7 ; python_version >= '3.6'
asgiref==3.7.2 ; python_version >= '3.7'
boto3==1.26.145
botocore==1.29.145 ; python_version >= '3.7'
cachetools==5.3.1
certifi==2023.5.7 ; python_version >= '3.6'
cfenv==0.5.3
cffi==1.15.1
charset-normalizer==3.0.1 ; python_version >= '3.6'
cryptography==39.0.1 ; python_version >= '3.6'
charset-normalizer==3.1.0 ; python_full_version >= '3.7.0'
cryptography==41.0.1 ; python_version >= '3.7'
defusedxml==0.7.1 ; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
dj-database-url==1.2.0
dj-database-url==2.0.0
dj-email-url==1.0.6
django==4.1.6
django==4.2.1
django-allow-cidr==0.6.0
django-auditlog==2.2.2
django-auditlog==2.3.0
django-cache-url==3.4.4
django-csp==3.7
django-fsm==2.8.1
django-phonenumber-field[phonenumberslite]==7.0.2
django-phonenumber-field[phonenumberslite]==7.1.0
django-widget-tweaks==1.4.12
environs[django]==9.5.0
faker==17.0.0
git+https://github.com/cisagov/epplib.git@master#egg=fred-epplib
faker==18.10.0
git+https://github.com/cisagov/epplib.git@f818cbf0b069a12f03e1d72e4b9f4900924b832d#egg=fred-epplib
furl==2.1.3
future==0.18.3 ; python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'
gunicorn==20.1.0
@ -31,21 +31,22 @@ lxml==4.9.2 ; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2,
mako==1.2.4 ; python_version >= '3.7'
markupsafe==2.1.2 ; python_version >= '3.7'
marshmallow==3.19.0 ; python_version >= '3.7'
oic==1.5.0
oic==1.6.0
orderedmultidict==1.0.1
packaging==23.0 ; python_version >= '3.7'
phonenumberslite==8.13.6
psycopg2-binary==2.9.5
packaging==23.1 ; python_version >= '3.7'
phonenumberslite==8.13.13
psycopg2-binary==2.9.6
pycparser==2.21
pycryptodomex==3.17
pycryptodomex==3.18.0
pydantic==1.10.8 ; python_version >= '3.7'
pyjwkest==1.4.2
python-dateutil==2.8.2 ; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
python-dotenv==0.21.1 ; python_version >= '3.7'
requests==2.28.2
s3transfer==0.6.0 ; python_version >= '3.7'
setuptools==67.2.0 ; python_version >= '3.7'
python-dotenv==1.0.0 ; python_version >= '3.8'
requests==2.31.0
s3transfer==0.6.1 ; python_version >= '3.7'
setuptools==67.8.0 ; python_version >= '3.7'
six==1.16.0 ; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
sqlparse==0.4.3 ; python_version >= '3.5'
typing-extensions==4.4.0 ; python_version >= '3.7'
urllib3==1.26.14 ; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'
whitenoise==6.3.0
sqlparse==0.4.4 ; python_version >= '3.5'
typing-extensions==4.6.2
urllib3==1.26.16 ; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'
whitenoise==6.4.0