Browse Source

DIV-1036: Update eFiling Hub integration and added POC code.

pull/170/head
Steven Ly 5 years ago
parent
commit
4a692edc1f
8 changed files with 618 additions and 19 deletions
  1. +11
    -1
      .env.example
  2. +241
    -0
      edivorce/apps/core/efilinghub.py
  3. +232
    -0
      edivorce/apps/core/tests/test_efiling_hub.py
  4. +46
    -0
      edivorce/apps/poc/templates/hub.html
  5. +4
    -0
      edivorce/apps/poc/templates/poc-sidebar.html
  6. +1
    -0
      edivorce/apps/poc/urls.py
  7. +47
    -1
      edivorce/apps/poc/views.py
  8. +36
    -17
      edivorce/settings/base.py

+ 11
- 1
.env.example View File

@ -16,4 +16,14 @@ CLAMAV_TCP_ADDR=localhost
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_DB=
REDIS_PASSWORD=
REDIS_PASSWORD=
# eFiling Hub settings
EFILING_HUB_TOKEN_BASE_URL=''
EFILING_HUB_REALM=''
EFILING_HUB_CLIENT_ID=''
EFILING_HUB_CLIENT_SECRET=''
EFILING_HUB_API_BASE_URL=''
# BCE ID test accounts for localdev
EFILING_BCEID=

+ 241
- 0
edivorce/apps/core/efilinghub.py View File

@ -0,0 +1,241 @@
import json
import requests
import logging
import uuid
from django.conf import settings
from django.core.exceptions import PermissionDenied
from django.urls import reverse
logger = logging.getLogger(__name__)
PACKAGE_DOCUMENT_FORMAT = {
"name": "string",
"type": "WNC",
"isAmendment": "false",
"isSupremeCourtScheduling": "false",
"data": {},
"md5": "string"
}
PACKAGE_PARTY_FORMAT = {
"partyType": "IND",
"roleType": "CLA",
"firstName": "",
"middleName": "",
"lastName": "",
}
PACKAGE_FORMAT = {
"clientAppName": "Online Divorce Assistant",
"filingPackage": {
"documents": [],
"court": {
"location": "1211",
"level": "P",
"courtClass": "F",
"division": "I",
"fileNumber": "1234",
"participatingClass": "string"
},
"parties": []
},
"navigationUrls": {
"success": "string",
"error": "string",
"cancel": "string"
}
}
class EFilingHub:
def __init__(self):
self.client_id = settings.EFILING_HUB_CLIENT_ID
self.client_secret = settings.EFILING_HUB_CLIENT_SECRET
self.token_base_url = settings.EFILING_HUB_TOKEN_BASE_URL
self.token_realm = settings.EFILING_HUB_REALM
self.api_base_url = settings.EFILING_HUB_API_BASE_URL
self.submission_id = None
def _get_token(self, request):
payload = 'client_id={}&grant_type=client_credentials&client_secret={}'.format(self.client_id,
self.client_secret)
headers = {'Content-Type': 'application/x-www-form-urlencoded'}
url = '{}/auth/realms/{}/protocol/openid-connect/token'.format(self.token_base_url, self.token_realm)
response = requests.post(url, headers=headers, data=payload)
logging.debug('EFH - Get Token {}'.format(response.status_code))
if response.status_code == 200:
response = json.loads(response.text)
# save in session .. lets just assume that current user is authenticated
if 'access_token' in response:
request.session['access_token'] = response['access_token']
if 'refresh_token' in response:
request.session['refresh_token'] = response['refresh_token']
return True
return False
def _refresh_token(self, request):
refresh_token = request.session.get('refresh_token', None)
if not refresh_token:
return False
payload = 'client_id={}&grant_type=refresh_token&client_secret={}&refresh_token={}'.format(
self.client_id,
self.client_secret,
refresh_token)
headers = {'Content-Type': 'application/x-www-form-urlencoded'}
url = '{}/auth/realms/{}/protocol/openid-connect/token'.format(self.token_base_url, self.token_realm)
response = requests.post(url, headers=headers, data=payload)
logging.debug('EFH - Get Refresh Token {}'.format(response.status_code))
response = json.loads(response.text)
# save in session .. lets just assume that current user is authenticated
if 'access_token' in response:
request.session['access_token'] = response['access_token']
if 'refresh_token' in response:
request.session['refresh_token'] = response['refresh_token']
return True
return False
def _get_api(self, request, url, transaction_id, bce_id, headers, data=None, files=None):
# make sure we have a session
access_token = request.session.get('access_token', None)
if not access_token:
if not self._get_token(request):
return False
access_token = request.session.get('access_token', None)
headers.update({
'X-Transaction-Id': transaction_id,
'X-User-Id': bce_id,
'Authorization': 'Bearer {}'.format(access_token)
})
if not data:
data = {}
response = requests.post(url, headers=headers, data=data, files=files)
logging.debug('EFH - Get API {} {}'.format(response.status_code, response.text))
if response.status_code == 401:
# not authorized .. try refreshing token
if self._refresh_token(request):
access_token = request.session.get('access_token', None)
headers.update({
'X-Transaction-Id': transaction_id,
'X-User-Id': bce_id,
'Authorization': 'Bearer {}'.format(access_token)
})
response = requests.post(url, headers=headers, data=data, files=files)
logging.debug('EFH - Get API Retry {} {}'.format(response.status_code, response.text))
return response
def _get_transaction(self, request):
"""
Get the current transaction id stored in session, otherwise generate one.
:param request:
:return:
"""
guid = request.session.get('transaction_id', None)
if not guid:
guid = str(uuid.uuid4())
request.session['transaction_id'] = guid
return guid
def _get_bceid(self, request):
def _get_raw_bceid(request):
is_localdev = settings.DEPLOYMENT_TYPE in ['localdev', 'minishift']
if is_localdev:
# to integrate with the Test eFiling Hub, we need a valid BCEID which is
# unavailable for a local eDivorce environment. Use an env specified mapping
# to figure out what we should pass through to eFiling Hub. This BCEID username
# needs to match with what you will be logging in with to the Test BCEID environment.
username = request.session.get('login_name', None)
if username:
if username in settings.EFILING_BCEID:
return settings.EFILING_BCEID[username]
return request.session.get('fake_bceid_guid', None)
return request.session.get('smgov_userguid', None)
guid = _get_raw_bceid(request)
if guid:
return str(uuid.UUID(guid))
return guid
def _format_package(self, request, files, parties):
documents = []
for file in files:
document = PACKAGE_DOCUMENT_FORMAT.copy()
document['name'] = file[1][0]
documents.append(document)
package = PACKAGE_FORMAT.copy()
package['filingPackage']['documents'] = documents
if parties:
package['filingPackage']['parties'] = parties
# update return urls
package['navigationUrls']['success'] = request.build_absolute_uri(
reverse('dashboard_nav', args=['check_with_registry']))
package['navigationUrls']['error'] = request.build_absolute_uri(
reverse('dashboard_nav', args=['check_with_registry']))
package['navigationUrls']['cancel'] = request.build_absolute_uri(
reverse('dashboard_nav', args=['check_with_registry']))
return package
# -- EFILING HUB INTERFACE --
def upload(self, request, files, parties=None):
"""
Does an initial upload of documents and gets the generated eFiling Hub url.
:param parties:
:param request:
:param files: Files need to be a list of tuples in the form ('files': (filename, filecontent))
:return: The url for redirect and any error messages
"""
# Find the transaction id .. this will be a unique guid generated by eDivorce thats passed to Efiling Hub. We
# will tie it to the session.
transaction_id = self._get_transaction(request)
bce_id = self._get_bceid(request)
# if bce_id is None .. we basically have an anonymous user so raise an error
if bce_id is None:
raise PermissionDenied()
response = self._get_api(request, '{}/submission/documents'.format(self.api_base_url), transaction_id, bce_id,
headers={}, files=files)
if response.status_code == 200:
response = json.loads(response.text)
if "submissionId" in response and response['submissionId'] != "":
# get the redirect url
headers = {
'Content-Type': 'application/json'
}
package_data = self._format_package(request, files, parties=parties)
url = '{}/submission/{}/generateUrl'.format(self.api_base_url, response['submissionId'])
response = self._get_api(request, url, transaction_id, bce_id, headers=headers,
data=json.dumps(package_data))
if response.status_code == 200:
response = json.loads(response.text)
return response['efilingUrl'], 'success'
response = json.loads(response.text)
return None, '{} - {}'.format(response['error'], response['message'])
return None, '{} - {}'.format(response.status_code, response.text)

+ 232
- 0
edivorce/apps/core/tests/test_efiling_hub.py View File

@ -0,0 +1,232 @@
import json
import requests
from unittest import mock
from django.contrib.sessions.middleware import SessionMiddleware
from django.core.exceptions import PermissionDenied
from django.test import TransactionTestCase
from django.test.client import RequestFactory
from django.core.files.uploadedfile import SimpleUploadedFile
from edivorce.apps.core.efilinghub import EFilingHub, PACKAGE_PARTY_FORMAT
SAMPLE_TOKEN_RESPONSE = {
"access_token": "klkadlfjadsfkj",
"expires_in": 300,
"refresh_expires_in": 1800,
"refresh_token": "ljasdfjaofijwekfjadslkjf",
"token_type": "bearer",
"not-before-policy": 0,
"session_state": "bed88a31-4d73-4f31-a4ee-dd8aa225d801",
"scope": "email profile"
}
INITIAL_DOC_UPLOAD_RESPONSE = {
"submissionId": "70fc9ce1-0cd6-4170-b842-bbabb88452a9",
"received": 1
}
GENERATE_URL_RESPONSE = {
"expiryDate": 1597775531035,
"efilingUrl": "http://efiling.gov.bc.ca/efiling?submissionId=adfadsf&transactionId=adsfadsf"
}
GENERATE_URL_RESPONSE_ERROR = {
"error": "403",
"message": "Request does not meet criteria"
}
class EFilingHubTests(TransactionTestCase):
def setUp(self):
# Every test needs access to the request factory.
self.factory = RequestFactory()
self.request = self.factory.get('/')
middleware = SessionMiddleware()
middleware.process_request(self.request)
self.request.session.save()
self.hub = EFilingHub()
def _mock_response(self, status=200, text="Text", json_data=None, raise_for_status=None):
mock_resp = mock.Mock()
mock_resp.raise_for_status = mock.Mock()
if raise_for_status:
mock_resp.raise_for_status.side_effect = raise_for_status
mock_resp.status_code = status
mock_resp.text = text
if json_data:
mock_resp.json = mock.Mock(
return_value=json_data
)
return mock_resp
@mock.patch('requests.post')
def test_get_token(self, mock_request_post):
mock_request_post.return_value = self._mock_response(text=json.dumps(SAMPLE_TOKEN_RESPONSE))
self.assertTrue(self.hub._get_token(self.request))
self.assertEqual(self.request.session['access_token'], SAMPLE_TOKEN_RESPONSE['access_token'])
@mock.patch('requests.post')
def test_get_token_error(self, mock_request_post):
mock_request_post.return_value = self._mock_response(text=json.dumps(SAMPLE_TOKEN_RESPONSE), status=401)
self.assertFalse(self.hub._get_token(self.request))
self.assertFalse("access_token" in self.request.session)
@mock.patch('requests.post')
def test_renew_token(self, mock_request_post):
mock_request_post.return_value = self._mock_response(text=json.dumps(SAMPLE_TOKEN_RESPONSE))
self.request.session['refresh_token'] = 'alskdfjadlfads'
self.assertTrue(self.hub._refresh_token(self.request))
self.assertEqual(self.request.session['access_token'], SAMPLE_TOKEN_RESPONSE['access_token'])
@mock.patch('requests.post')
def test_renew_token_anon(self, mock_request_post):
# if we don't have a refresh token in session, this should fail
mock_request_post.return_value = self._mock_response(text=json.dumps(SAMPLE_TOKEN_RESPONSE))
self.assertFalse(self.hub._refresh_token(self.request))
self.assertFalse("access_token" in self.request.session)
@mock.patch('requests.post')
def test_renew_token_error(self, mock_request_post):
mock_request_post.return_value = self._mock_response(text=json.dumps(SAMPLE_TOKEN_RESPONSE), status=401)
self.request.session['refresh_token'] = 'alskdfjadlfads'
self.assertTrue(self.hub._refresh_token(self.request))
self.assertEqual(self.request.session['access_token'], SAMPLE_TOKEN_RESPONSE['access_token'])
@mock.patch('requests.post')
def test_get_api_success(self, mock_request_post):
mock_request_post.return_value = self._mock_response(text=json.dumps(INITIAL_DOC_UPLOAD_RESPONSE))
self.request.session['access_token'] = 'aslkfjadskfjd'
response = self.hub._get_api(self.request, 'https://somewhere.com', 'alksdjfa', 'kasdkfd', {})
self.assertTrue(response)
self.assertEqual(response.status_code, 200)
response = json.loads(response.text)
self.assertTrue("submissionId" in response)
@mock.patch('requests.post')
def test_get_api_expired_token(self, mock_request_post):
self.request.session['access_token'] = 'aslkfjadskfjd'
self.request.session['refresh_token'] = 'alskdfjadlfads'
# we want 3 mock side effects for post .. a 401 on the first and success on token renewal
mock_request_post.side_effect = [
self._mock_response(text=json.dumps(INITIAL_DOC_UPLOAD_RESPONSE), status=401),
self._mock_response(text=json.dumps(SAMPLE_TOKEN_RESPONSE)),
self._mock_response(text=json.dumps(INITIAL_DOC_UPLOAD_RESPONSE))
]
response = self.hub._get_api(self.request, 'https://somewhere.com', 'alksdjfa', 'kasdkfd', {})
self.assertTrue(response)
self.assertEqual(response.status_code, 200)
response = json.loads(response.text)
self.assertTrue("submissionId" in response)
@mock.patch('requests.post')
def test_get_api_no_token(self, mock_request_post):
mock_request_post.return_value = self._mock_response(text=json.dumps(INITIAL_DOC_UPLOAD_RESPONSE))
response = self.hub._get_api(self.request, 'https://somewhere.com', 'alksdjfa', 'kasdkfd', {})
self.assertFalse(response)
def test_transaction_id_current(self):
self.request.session['transaction_id'] = 'alksdjflaskdjf'
guid = self.hub._get_transaction(self.request)
self.assertEqual(guid, 'alksdjflaskdjf')
def test_transaction_id_empty(self):
self.assertFalse('transaction_id' in self.request.session)
guild = self.hub._get_transaction(self.request)
self.assertTrue('transaction_id' in self.request.session)
def test_bceid_get_current(self):
self.request.session['smgov_userguid'] = '70fc9ce1-0cd6-4170-b842-bbabb88452a9'
with self.settings(DEPLOYMENT_TYPE='prod'):
bceid = self.hub._get_bceid(self.request)
self.assertEqual(bceid, '70fc9ce1-0cd6-4170-b842-bbabb88452a9')
def test_bceid_get_local_fake(self):
self.request.session['fake_bceid_guid'] = '70fc9ce1-0cd6-4170-b842-bbabb88452a9'
with self.settings(DEPLOYMENT_TYPE='localdev'):
bceid = self.hub._get_bceid(self.request)
self.assertEqual(bceid, '70fc9ce1-0cd6-4170-b842-bbabb88452a9')
def test_bceid_anonymous_user(self):
with self.settings(DEPLOYMENT_TYPE='prod'):
bceid = self.hub._get_bceid(self.request)
self.assertFalse(bceid)
def test_format_package(self):
files = []
for i in range(0, 2):
file = SimpleUploadedFile('form_{}.pdf'.format(i), b'test content')
files.append(('files', (file.name, file.read())))
parties = []
for i in range(0, 2):
party = PACKAGE_PARTY_FORMAT.copy()
party['firstName'] = 'Party {}'.format(i)
party['lastName'] = 'Test'
parties.append(party)
package = self.hub._format_package(self.request, files, parties=parties)
self.assertTrue(package)
self.assertEqual(package['filingPackage']['documents'][0]['name'], 'form_0.pdf')
self.assertEqual(package['filingPackage']['documents'][1]['name'], 'form_1.pdf')
self.assertEqual(package['filingPackage']['parties'][0]['firstName'], 'Party 0')
self.assertEqual(package['filingPackage']['parties'][1]['firstName'], 'Party 1')
def test_upload_anonymous_user(self):
with self.settings(DEPLOYMENT_TYPE='prod'):
with self.assertRaises(PermissionDenied):
redirect, msg = self.hub.upload(self.request, None)
@mock.patch('edivorce.apps.core.efilinghub.EFilingHub._get_api')
def test_upload_success(self, mock_get_api):
self.request.session['smgov_userguid'] = '70fc9ce1-0cd6-4170-b842-bbabb88452a9'
with self.settings(DEPLOYMENT_TYPE='prod'):
mock_get_api.side_effect = [
self._mock_response(text=json.dumps(INITIAL_DOC_UPLOAD_RESPONSE)),
self._mock_response(text=json.dumps(GENERATE_URL_RESPONSE))
]
redirect, msg = self.hub.upload(self.request, {})
self.assertTrue(redirect)
self.assertEqual(redirect, GENERATE_URL_RESPONSE['efilingUrl'])
self.assertEqual(msg, 'success')
@mock.patch('edivorce.apps.core.efilinghub.EFilingHub._get_api')
def test_upload_failed_initial_upload(self, mock_get_api):
self.request.session['smgov_userguid'] = '70fc9ce1-0cd6-4170-b842-bbabb88452a9'
with self.settings(DEPLOYMENT_TYPE='prod'):
mock_get_api.side_effect = [
self._mock_response(text=json.dumps(INITIAL_DOC_UPLOAD_RESPONSE), status=401),
self._mock_response(text=json.dumps(GENERATE_URL_RESPONSE))
]
redirect, msg = self.hub.upload(self.request, {})
self.assertFalse(redirect)
@mock.patch('edivorce.apps.core.efilinghub.EFilingHub._get_api')
def test_upload_failed_generate_url(self, mock_get_api):
self.request.session['smgov_userguid'] = '70fc9ce1-0cd6-4170-b842-bbabb88452a9'
with self.settings(DEPLOYMENT_TYPE='prod'):
mock_get_api.side_effect = [
self._mock_response(text=json.dumps(INITIAL_DOC_UPLOAD_RESPONSE)),
self._mock_response(text=json.dumps(GENERATE_URL_RESPONSE_ERROR), status=403)
]
redirect, msg = self.hub.upload(self.request, {})
self.assertFalse(redirect)

+ 46
- 0
edivorce/apps/poc/templates/hub.html View File

@ -0,0 +1,46 @@
{% extends 'base.html' %}
{% load input_field %}
{% load step_order %}
{% load load_json %}
{% block title %}{{ block.super }}: POC{% endblock %}
{% block progress %}{% include "poc-sidebar.html" %}{% endblock %}
{% block content %}
<h1><small>Proof of Concept:</small>Efiling Hub Integration</h1>
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
<div class="question-well">
<h3>Specify files to upload to the eFiling Hub.</h3>
<div class="form-group {% if form.upload_file.errors %}has-error{% endif %}">
<div>{{ form.upload_file }}</div>
{% if form.upload_file.errors %}
<span class="help-block">
{% for err in form.upload_file.errors %}{{ err }}{% endfor %}
</span>
{% endif %}
</div>
</div>
<div class="form-buttons clearfix">
<button type="submit" class="btn btn-primary pull-right">Submit</button>
</div>
</form>
{% endblock %}
{% block formbuttons %}
{% endblock %}
{% block sidebarNav %}
<!-- no sidebar -->
{% endblock %}
{% block sidebar %}
<!-- no sidebar -->
{% endblock %}

+ 4
- 0
edivorce/apps/poc/templates/poc-sidebar.html View File

@ -12,4 +12,8 @@
<span class="progress-content">Redis Storage</span>
</a>
<a href="{% url 'poc-hub' %}" class="progress-question">
<span class="progress-icon"><i class="fa fa-location-arrow" aria-hidden="true"></i></span>
<span class="progress-content">eFiling Hub</span>
</a>
</div>

+ 1
- 0
edivorce/apps/poc/urls.py View File

@ -5,6 +5,7 @@ from ..core.decorators import bceid_required
urlpatterns = [
url(r'scan', bceid_required(views.UploadScan.as_view()), name="poc-scan"),
url(r'hub', bceid_required(views.EfilingHubUpload.as_view()), name="poc-hub"),
url(r'storage/doc/(?P<document_id>\d+)', bceid_required(views.view_document_file), name="poc-storage-download"),
url(r'storage/delete/(?P<pk>\d+)', bceid_required(views.UploadStorageDelete.as_view()), name="poc-storage-delete"),
url(r'storage', bceid_required(views.UploadStorage.as_view()), name="poc-storage"),

+ 47
- 1
edivorce/apps/poc/views.py View File

@ -1,3 +1,5 @@
import logging
from django.shortcuts import render
from django.utils.decorators import method_decorator
from django.views.decorators.csrf import csrf_exempt
@ -6,9 +8,12 @@ from django import forms
from django.http import HttpResponse
from django.conf import settings
from edivorce.apps.core.efilinghub import EFilingHub, PACKAGE_PARTY_FORMAT
from edivorce.apps.core.validators import file_scan_validation
from edivorce.apps.poc.models import Document
logger = logging.getLogger(__name__)
"""
Everything in this file is considered as proof of concept work and should not be used for production code.
"""
@ -18,6 +23,10 @@ class UploadScanForm(forms.Form):
upload_file = forms.FileField(validators=[file_scan_validation])
class MultipleUploadForm(forms.Form):
upload_file = forms.FileField(widget=forms.ClearableFileInput(attrs={'multiple': True}))
class UploadScan(FormView):
form_class = UploadScanForm
template_name = "scan.html"
@ -56,4 +65,41 @@ def view_document_file(request, document_id):
response = HttpResponse(doc.file.read(), content_type=content_type)
response['Content-Disposition'] = 'attachment; filename={}'.format(doc.filename)
return response
return response
class EfilingHubUpload(FormView):
form_class = MultipleUploadForm
template_name = 'hub.html'
success_url = '/poc/hub'
def post(self, request, *args, **kwargs):
form_class = self.get_form_class()
form = self.get_form(form_class)
files = request.FILES.getlist('upload_file')
if form.is_valid():
# NOTE: this does not do any validation for file types .. make sure that is done
# prior to sending to eFiling Hub
post_files = []
if files:
for file in files:
post_files.append(('files', (file.name, file.read())))
# generate the list of parties to send to eFiling Hub
parties = []
for i in range(0, 2):
party = PACKAGE_PARTY_FORMAT.copy()
party['firstName'] = 'Party {}'.format(i)
party['lastName'] = 'Test'
parties.append(party)
hub = EFilingHub()
redirect, msg = hub.upload(request, post_files, parties=parties)
if redirect:
self.success_url = redirect
return self.form_valid(form)
form.add_error('upload_file', msg)
return self.form_invalid(form)

+ 36
- 17
edivorce/settings/base.py View File

@ -102,6 +102,22 @@ REST_FRAMEWORK = {
]
}
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'handlers': {
'console': {
'class': 'logging.StreamHandler',
},
},
'loggers': {
'': {
'handlers': ['console'],
'level': os.getenv('DJANGO_LOG_LEVEL', 'INFO'),
},
},
}
# Internationalization
# https://docs.djangoproject.com/en/1.8/topics/i18n/
@ -156,20 +172,23 @@ SECURE_BROWSER_XSS_FILTER = True
LOGOUT_URL = '/accounts/logout/'
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'handlers': {
'console':{
'level':'DEBUG',
'class':'logging.StreamHandler',
},
},
'loggers': {
'django.request': {
'handlers':['console'],
'propagate': True,
'level':'DEBUG',
}
},
}
# CLAMAV settings
CLAMAV_ENABLED = env.bool('CLAMAV_ENABLED', True)
CLAMAV_TCP_PORT = env.int('CLAMAV_TCP_PORT', 3310)
CLAMAV_TCP_ADDR = env('CLAMAV_TCP_ADDR', 'localhost')
# Redis settings
REDIS_HOST = env('REDIS_HOST', 'localhost')
REDIS_PORT = env.int('REDIS_PORT', 6379)
REDIS_DB = env('REDIS_DB', '')
REDIS_PASSWORD = env('REDIS_PASSWORD', '')
# eFiling Hub settings
EFILING_HUB_TOKEN_BASE_URL=env('EFILING_HUB_TOKEN_BASE_URL')
EFILING_HUB_REALM=env('EFILING_HUB_REALM')
EFILING_HUB_CLIENT_ID=env('EFILING_HUB_CLIENT_ID')
EFILING_HUB_CLIENT_SECRET=env('EFILING_HUB_CLIENT_SECRET')
EFILING_HUB_API_BASE_URL=env('EFILING_HUB_API_BASE_URL')
EFILING_BCEID=env.dict('EFILING_BCEID', subcast=str)

Loading…
Cancel
Save