npis-api-utils 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- npis_api_utils-1.0.0/MANIFEST.in +1 -0
- npis_api_utils-1.0.0/PKG-INFO +20 -0
- npis_api_utils-1.0.0/README.md +12 -0
- npis_api_utils-1.0.0/requirements.txt +19 -0
- npis_api_utils-1.0.0/setup.cfg +4 -0
- npis_api_utils-1.0.0/setup.py +42 -0
- npis_api_utils-1.0.0/src/npis_api_utils/__init__.py +9 -0
- npis_api_utils-1.0.0/src/npis_api_utils/exceptions/__init__.py +16 -0
- npis_api_utils-1.0.0/src/npis_api_utils/schemas/__init__.py +2 -0
- npis_api_utils-1.0.0/src/npis_api_utils/schemas/formio_roles.py +14 -0
- npis_api_utils-1.0.0/src/npis_api_utils/services/__init__.py +7 -0
- npis_api_utils-1.0.0/src/npis_api_utils/services/external/__init__.py +3 -0
- npis_api_utils-1.0.0/src/npis_api_utils/services/external/formio.py +140 -0
- npis_api_utils-1.0.0/src/npis_api_utils/utils/__init__.py +35 -0
- npis_api_utils-1.0.0/src/npis_api_utils/utils/auth.py +83 -0
- npis_api_utils-1.0.0/src/npis_api_utils/utils/caching.py +5 -0
- npis_api_utils-1.0.0/src/npis_api_utils/utils/constants.py +46 -0
- npis_api_utils-1.0.0/src/npis_api_utils/utils/enums.py +70 -0
- npis_api_utils-1.0.0/src/npis_api_utils/utils/format.py +31 -0
- npis_api_utils-1.0.0/src/npis_api_utils/utils/logging.py +38 -0
- npis_api_utils-1.0.0/src/npis_api_utils/utils/pdf.py +101 -0
- npis_api_utils-1.0.0/src/npis_api_utils/utils/profiler.py +24 -0
- npis_api_utils-1.0.0/src/npis_api_utils/utils/roles.py +9 -0
- npis_api_utils-1.0.0/src/npis_api_utils/utils/startup.py +68 -0
- npis_api_utils-1.0.0/src/npis_api_utils/utils/translations/__init__.py +3 -0
- npis_api_utils-1.0.0/src/npis_api_utils/utils/translations/translations.py +130 -0
- npis_api_utils-1.0.0/src/npis_api_utils/utils/user_context.py +87 -0
- npis_api_utils-1.0.0/src/npis_api_utils/utils/util.py +125 -0
- npis_api_utils-1.0.0/src/npis_api_utils.egg-info/PKG-INFO +20 -0
- npis_api_utils-1.0.0/src/npis_api_utils.egg-info/SOURCES.txt +32 -0
- npis_api_utils-1.0.0/src/npis_api_utils.egg-info/dependency_links.txt +1 -0
- npis_api_utils-1.0.0/src/npis_api_utils.egg-info/not-zip-safe +1 -0
- npis_api_utils-1.0.0/src/npis_api_utils.egg-info/requires.txt +19 -0
- npis_api_utils-1.0.0/src/npis_api_utils.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
include README.md requirements.txt
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: npis_api_utils
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: NPIS api related libraries.
|
|
5
|
+
Home-page: https://github.com/katxeus/npis_api_utils
|
|
6
|
+
Author: JK
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
|
|
9
|
+
# NPIS UTILS
|
|
10
|
+
|
|
11
|
+
  
|
|
12
|
+
[](https://pycqa.github.io/isort/) [](https://github.com/psf/black)[](https://github.com/PyCQA/pylint)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
This Python package include all the libraries and utils needed for NPIS related services.
|
|
16
|
+
|
|
17
|
+
## Usage
|
|
18
|
+
|
|
19
|
+
Install using pip
|
|
20
|
+
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# NPIS UTILS
|
|
2
|
+
|
|
3
|
+
  
|
|
4
|
+
[](https://pycqa.github.io/isort/) [](https://github.com/psf/black)[](https://github.com/PyCQA/pylint)
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
This Python package include all the libraries and utils needed for NPIS related services.
|
|
8
|
+
|
|
9
|
+
## Usage
|
|
10
|
+
|
|
11
|
+
Install using pip
|
|
12
|
+
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
gunicorn
|
|
2
|
+
Flask
|
|
3
|
+
Flask-Caching
|
|
4
|
+
Flask-Migrate
|
|
5
|
+
Flask-Moment
|
|
6
|
+
Flask-SQLAlchemy
|
|
7
|
+
flask-restx
|
|
8
|
+
flask-marshmallow
|
|
9
|
+
flask-jwt-oidc
|
|
10
|
+
python-dotenv
|
|
11
|
+
psycopg2-binary
|
|
12
|
+
marshmallow-sqlalchemy
|
|
13
|
+
requests
|
|
14
|
+
Werkzeug
|
|
15
|
+
sqlalchemy_utils
|
|
16
|
+
markupsafe
|
|
17
|
+
PyJWT
|
|
18
|
+
selenium
|
|
19
|
+
selenium-wire
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import setuptools
|
|
2
|
+
from glob import glob
|
|
3
|
+
from os.path import basename, splitext
|
|
4
|
+
|
|
5
|
+
def read(filepath):
|
|
6
|
+
"""Read the contents from a file.
|
|
7
|
+
:param str filepath: path to the file to be read
|
|
8
|
+
:return: file contents
|
|
9
|
+
:rtype: str
|
|
10
|
+
"""
|
|
11
|
+
with open(filepath, "r") as file_handle:
|
|
12
|
+
content = file_handle.read()
|
|
13
|
+
return content
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def read_requirements(filename):
|
|
17
|
+
"""Get application requirements from the requirements.txt file.
|
|
18
|
+
:return: Python requirements
|
|
19
|
+
:rtype: list
|
|
20
|
+
"""
|
|
21
|
+
with open(filename, "r") as req:
|
|
22
|
+
requirements = req.readlines()
|
|
23
|
+
install_requires = [r.strip() for r in requirements if r.find("git+") != 0]
|
|
24
|
+
return install_requires
|
|
25
|
+
|
|
26
|
+
REQUIREMENTS = read_requirements("requirements.txt")
|
|
27
|
+
|
|
28
|
+
setuptools.setup(
|
|
29
|
+
name='npis_api_utils',
|
|
30
|
+
version='1.0.0',
|
|
31
|
+
author='JK',
|
|
32
|
+
description='NPIS api related libraries.',
|
|
33
|
+
long_description=read("README.md"),
|
|
34
|
+
long_description_content_type="text/markdown",
|
|
35
|
+
url='https://github.com/katxeus/npis_api_utils',
|
|
36
|
+
packages=setuptools.find_packages("src"),
|
|
37
|
+
package_dir={"": "src"},
|
|
38
|
+
py_modules=[splitext(basename(path))[0] for path in glob("src/*.py")],
|
|
39
|
+
include_package_data=True,
|
|
40
|
+
zip_safe=False,
|
|
41
|
+
install_requires=REQUIREMENTS,
|
|
42
|
+
)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Application Specific Exceptions, to manage the business errors.
|
|
2
|
+
|
|
3
|
+
BusinessException - error, status_code - Business rules error
|
|
4
|
+
error - a description of the error {code / description: classname / full text}
|
|
5
|
+
status_code - where possible use HTTP Error Codes
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class BusinessException(Exception):
|
|
10
|
+
"""Exception that adds error code and error."""
|
|
11
|
+
|
|
12
|
+
def __init__(self, error, status_code, *args, **kwargs):
|
|
13
|
+
"""Return a valid BusinessException."""
|
|
14
|
+
super().__init__(*args, **kwargs)
|
|
15
|
+
self.error = error
|
|
16
|
+
self.status_code = status_code
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""This is for marshmallowing Formio role ids."""
|
|
2
|
+
from marshmallow import EXCLUDE, Schema, fields
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class FormioRoleSchema(Schema):
|
|
6
|
+
"""This class manages the Formio role id request response schema."""
|
|
7
|
+
|
|
8
|
+
class Meta: # pylint: disable=too-few-public-methods
|
|
9
|
+
"""Exclude unknown fields in the deserialized output."""
|
|
10
|
+
|
|
11
|
+
unknown = EXCLUDE
|
|
12
|
+
|
|
13
|
+
roleId = fields.Str(data_key="_id", required=True)
|
|
14
|
+
type = fields.Str(data_key="machineName", required=True)
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""This exposes the Formio APIs."""
|
|
2
|
+
import json
|
|
3
|
+
from http import HTTPStatus
|
|
4
|
+
|
|
5
|
+
import jwt
|
|
6
|
+
import requests
|
|
7
|
+
from flask import current_app
|
|
8
|
+
|
|
9
|
+
from npis_api_utils.exceptions import BusinessException
|
|
10
|
+
from npis_api_utils.utils import cache
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class FormioService:
|
|
14
|
+
"""This class manages formio API calls."""
|
|
15
|
+
|
|
16
|
+
def __init__(self):
|
|
17
|
+
"""Initializing the service."""
|
|
18
|
+
self.base_url = current_app.config.get("FORMIO_URL")
|
|
19
|
+
|
|
20
|
+
@classmethod
|
|
21
|
+
def decode_timeout(cls, token):
|
|
22
|
+
"""Method to decode token and get timeout."""
|
|
23
|
+
token = jwt.decode(token, options={"verify_signature": False})
|
|
24
|
+
timeout = token["exp"] - token["iat"]
|
|
25
|
+
return timeout
|
|
26
|
+
|
|
27
|
+
def get_formio_access_token(self):
|
|
28
|
+
"""Method to get formio access token."""
|
|
29
|
+
formio_token = cache.get("formio_token")
|
|
30
|
+
if formio_token is None:
|
|
31
|
+
formio_token = self.generate_formio_token()
|
|
32
|
+
cache.set(
|
|
33
|
+
"formio_token",
|
|
34
|
+
formio_token,
|
|
35
|
+
timeout=self.decode_timeout(formio_token),
|
|
36
|
+
)
|
|
37
|
+
return formio_token
|
|
38
|
+
|
|
39
|
+
def generate_formio_token(self):
|
|
40
|
+
"""Method to generate formio token using formio login API."""
|
|
41
|
+
headers = {"Content-Type": "application/json"}
|
|
42
|
+
url = f"{self.base_url}/user/login"
|
|
43
|
+
payload = {
|
|
44
|
+
"data": {
|
|
45
|
+
"email": current_app.config.get("FORMIO_USERNAME"),
|
|
46
|
+
"password": current_app.config.get("FORMIO_PASSWORD"),
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
current_app.logger.info("Generate formio token using formio login API.")
|
|
50
|
+
response = requests.post(url, headers=headers, data=json.dumps(payload))
|
|
51
|
+
if response.ok:
|
|
52
|
+
form_io_token = response.headers["x-jwt-token"]
|
|
53
|
+
return form_io_token
|
|
54
|
+
raise BusinessException(
|
|
55
|
+
"Unable to get access token from formio server",
|
|
56
|
+
HTTPStatus.SERVICE_UNAVAILABLE,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
def create_form(self, data, formio_token):
|
|
60
|
+
"""Post request to formio API to create form."""
|
|
61
|
+
headers = {"Content-Type": "application/json", "x-jwt-token": formio_token}
|
|
62
|
+
url = f"{self.base_url}/form"
|
|
63
|
+
response = requests.post(url, headers=headers, data=json.dumps(data))
|
|
64
|
+
if response.ok:
|
|
65
|
+
return response.json()
|
|
66
|
+
raise BusinessException(response.json(), HTTPStatus.BAD_REQUEST)
|
|
67
|
+
|
|
68
|
+
def update_form(self, form_id, data, formio_token):
|
|
69
|
+
"""Put request to formio API to update form."""
|
|
70
|
+
headers = {"Content-Type": "application/json", "x-jwt-token": formio_token}
|
|
71
|
+
url = f"{self.base_url}/form/{form_id}"
|
|
72
|
+
response = requests.put(url, headers=headers, data=json.dumps(data))
|
|
73
|
+
if response.ok:
|
|
74
|
+
return response.json()
|
|
75
|
+
raise BusinessException(response.json(), HTTPStatus.BAD_REQUEST)
|
|
76
|
+
|
|
77
|
+
def get_role_ids(self):
|
|
78
|
+
"""Get request to Formio API to retrieve role ids."""
|
|
79
|
+
url = f"{self.base_url}/role"
|
|
80
|
+
headers = {"x-jwt-token": self.get_formio_access_token()}
|
|
81
|
+
current_app.logger.info("Role id fetching started...")
|
|
82
|
+
|
|
83
|
+
response = requests.get(url, headers=headers)
|
|
84
|
+
if response.ok:
|
|
85
|
+
current_app.logger.info("Role ids collected successfully...")
|
|
86
|
+
return response.json()
|
|
87
|
+
current_app.logger.error("Failed to fetch role ids !!!")
|
|
88
|
+
raise BusinessException(response.json(), HTTPStatus.SERVICE_UNAVAILABLE)
|
|
89
|
+
|
|
90
|
+
def get_user_resource_ids(self):
|
|
91
|
+
"""Get request to Formio API to retrieve user resource ids."""
|
|
92
|
+
url = f"{self.base_url}/user"
|
|
93
|
+
current_app.logger.info("Fetching user resource ids...")
|
|
94
|
+
response = requests.get(url)
|
|
95
|
+
if response.ok:
|
|
96
|
+
current_app.logger.info("User resource ids collected successfully.")
|
|
97
|
+
return response.json()
|
|
98
|
+
current_app.logger.error("Failed to fetch user resource ids!")
|
|
99
|
+
raise BusinessException(response.json(), HTTPStatus.SERVICE_UNAVAILABLE)
|
|
100
|
+
|
|
101
|
+
def get_form(self, data, formio_token):
|
|
102
|
+
"""Get request to formio API to get form details."""
|
|
103
|
+
headers = {"Content-Type": "application/json", "x-jwt-token": formio_token}
|
|
104
|
+
url = f"{self.base_url}/form/" + data["form_id"]
|
|
105
|
+
response = requests.get(url, headers=headers)
|
|
106
|
+
if response.ok:
|
|
107
|
+
return response.json()
|
|
108
|
+
raise BusinessException(response.json(), HTTPStatus.BAD_REQUEST)
|
|
109
|
+
|
|
110
|
+
def get_submission(self, data, formio_token):
|
|
111
|
+
"""Get request to formio API to get submission details."""
|
|
112
|
+
headers = {"Content-Type": "application/json", "x-jwt-token": formio_token}
|
|
113
|
+
url = (
|
|
114
|
+
f"{self.base_url}/form/" + data["form_id"] + "/submission/" + data["sub_id"]
|
|
115
|
+
)
|
|
116
|
+
response = requests.get(url, headers=headers, data=json.dumps(data))
|
|
117
|
+
if response.ok:
|
|
118
|
+
return response.json()
|
|
119
|
+
raise BusinessException(response.json(), HTTPStatus.BAD_REQUEST)
|
|
120
|
+
|
|
121
|
+
def post_submission(self, data, formio_token):
|
|
122
|
+
"""Post request to formio API to create submission details."""
|
|
123
|
+
headers = {"Content-Type": "application/json", "x-jwt-token": formio_token}
|
|
124
|
+
url = (
|
|
125
|
+
f"{self.base_url}/form/{data['formId']}/submission"
|
|
126
|
+
)
|
|
127
|
+
response = requests.post(url, headers=headers, data=json.dumps(data))
|
|
128
|
+
if response.ok:
|
|
129
|
+
return response.json()
|
|
130
|
+
raise BusinessException(response.json(), HTTPStatus.BAD_REQUEST)
|
|
131
|
+
|
|
132
|
+
def get_form_by_path(self, path_name: str, formio_token: str) -> dict:
|
|
133
|
+
"""Get request to formio API to get form details from path."""
|
|
134
|
+
headers = {"Content-Type": "application/json", "x-jwt-token": formio_token}
|
|
135
|
+
url = f"{self.base_url}/{path_name}"
|
|
136
|
+
response = requests.get(url, headers=headers)
|
|
137
|
+
if response.ok:
|
|
138
|
+
return response.json()
|
|
139
|
+
raise BusinessException(response.json(), HTTPStatus.BAD_REQUEST)
|
|
140
|
+
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""This module holds general utility functions and helpers for the main package."""
|
|
2
|
+
|
|
3
|
+
from .auth import auth, jwt
|
|
4
|
+
from .caching import cache
|
|
5
|
+
from .constants import (
|
|
6
|
+
ALLOW_ALL_APPLICATIONS,
|
|
7
|
+
ALLOW_ALL_ORIGINS,
|
|
8
|
+
ANONYMOUS_USER,
|
|
9
|
+
ADMIN_GROUP,
|
|
10
|
+
CLIENT_GROUP,
|
|
11
|
+
CORS_ORIGINS,
|
|
12
|
+
DEFAULT_PROCESS_KEY,
|
|
13
|
+
DEFAULT_PROCESS_NAME,
|
|
14
|
+
DESIGNER_GROUP,
|
|
15
|
+
DRAFT_APPLICATION_STATUS,
|
|
16
|
+
FILTER_MAPS,
|
|
17
|
+
NPIS_API_CORS_ORIGINS,
|
|
18
|
+
NPIS_ROLES,
|
|
19
|
+
KEYCLOAK_DASHBOARD_BASE_GROUP,
|
|
20
|
+
NEW_APPLICATION_STATUS,
|
|
21
|
+
REVIEWER_GROUP,
|
|
22
|
+
HTTP_TIMEOUT
|
|
23
|
+
)
|
|
24
|
+
from .enums import ApplicationSortingParameters
|
|
25
|
+
from .format import CustomFormatter
|
|
26
|
+
from .logging import setup_logging, log_bpm_error
|
|
27
|
+
from .profiler import profiletime
|
|
28
|
+
from .user_context import UserContext, user_context
|
|
29
|
+
from .util import (
|
|
30
|
+
cors_preflight,
|
|
31
|
+
get_form_and_submission_id_from_form_url,
|
|
32
|
+
get_role_ids_from_user_groups,
|
|
33
|
+
translate,
|
|
34
|
+
validate_sort_order_and_order_by,
|
|
35
|
+
)
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""Bring in the common JWT Manager and helper functions."""
|
|
2
|
+
|
|
3
|
+
from functools import wraps
|
|
4
|
+
from http import HTTPStatus
|
|
5
|
+
|
|
6
|
+
from flask import g, request, current_app
|
|
7
|
+
from flask_jwt_oidc import JwtManager
|
|
8
|
+
|
|
9
|
+
from jose import jwt as json_web_token
|
|
10
|
+
from jose.exceptions import JWTError
|
|
11
|
+
|
|
12
|
+
from ..exceptions import BusinessException
|
|
13
|
+
|
|
14
|
+
jwt = JwtManager() # pylint: disable=invalid-name
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Auth:
|
|
18
|
+
"""Extending JwtManager to include additional functionalities."""
|
|
19
|
+
|
|
20
|
+
@classmethod
|
|
21
|
+
def require(cls, f):
|
|
22
|
+
"""Validate the Bearer Token."""
|
|
23
|
+
|
|
24
|
+
@jwt.requires_auth
|
|
25
|
+
@wraps(f)
|
|
26
|
+
def decorated(*args, **kwargs):
|
|
27
|
+
g.authorization_header = request.headers.get("Authorization", None)
|
|
28
|
+
g.token_info = g.jwt_oidc_token_info
|
|
29
|
+
|
|
30
|
+
return f(*args, **kwargs)
|
|
31
|
+
|
|
32
|
+
return decorated
|
|
33
|
+
|
|
34
|
+
@classmethod
|
|
35
|
+
def has_one_of_roles(cls, roles):
|
|
36
|
+
"""Check that at least one of the realm roles are in the token.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
roles [str,]: Comma separated list of valid roles
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def decorated(f):
|
|
43
|
+
@Auth.require
|
|
44
|
+
@wraps(f)
|
|
45
|
+
def wrapper(*args, **kwargs):
|
|
46
|
+
if jwt.contains_role(roles):
|
|
47
|
+
return f(*args, **kwargs)
|
|
48
|
+
|
|
49
|
+
raise BusinessException("Access Denied", HTTPStatus.UNAUTHORIZED)
|
|
50
|
+
|
|
51
|
+
return wrapper
|
|
52
|
+
|
|
53
|
+
return decorated
|
|
54
|
+
|
|
55
|
+
@classmethod
|
|
56
|
+
def has_role(cls, role):
|
|
57
|
+
"""Method to validate the role."""
|
|
58
|
+
return jwt.validate_roles(role)
|
|
59
|
+
|
|
60
|
+
@classmethod
|
|
61
|
+
def require_custom(cls, f):
|
|
62
|
+
"""Validate custom form embed token."""
|
|
63
|
+
@wraps(f)
|
|
64
|
+
def decorated(*args, **kwargs):
|
|
65
|
+
token = jwt.get_token_auth_header()
|
|
66
|
+
try:
|
|
67
|
+
data = json_web_token.decode(
|
|
68
|
+
token,
|
|
69
|
+
algorithms="HS256",
|
|
70
|
+
key=current_app.config.get('FORM_EMBED_JWT_SECRET'),
|
|
71
|
+
)
|
|
72
|
+
g.authorization_header = token
|
|
73
|
+
g.token_info = g.jwt_oidc_token_info = data
|
|
74
|
+
except JWTError as err:
|
|
75
|
+
raise BusinessException("Invalid token", HTTPStatus.UNAUTHORIZED)
|
|
76
|
+
except Exception as err:
|
|
77
|
+
raise err
|
|
78
|
+
return f(*args, **kwargs)
|
|
79
|
+
return decorated
|
|
80
|
+
|
|
81
|
+
auth = (
|
|
82
|
+
Auth()
|
|
83
|
+
) # pylint: disable=invalid-name; lower case name as used by convention in most Flask apps
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""All constants for project."""
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
from dotenv import find_dotenv, load_dotenv
|
|
5
|
+
|
|
6
|
+
# this will load all the envars from a .env file located in the project root (api)
|
|
7
|
+
load_dotenv(find_dotenv())
|
|
8
|
+
|
|
9
|
+
NPIS_API_CORS_ORIGINS = os.getenv("NPIS_API_CORS_ORIGINS", "*")
|
|
10
|
+
ALLOW_ALL_ORIGINS = "*"
|
|
11
|
+
CORS_ORIGINS = []
|
|
12
|
+
if NPIS_API_CORS_ORIGINS != "*":
|
|
13
|
+
CORS_ORIGINS = NPIS_API_CORS_ORIGINS.split(",")
|
|
14
|
+
ADMIN_GROUP = "npis-admin"
|
|
15
|
+
DESIGNER_GROUP = "npis-designer"
|
|
16
|
+
REVIEWER_GROUP = "npis-reviewer"
|
|
17
|
+
CLIENT_GROUP = "npis-client"
|
|
18
|
+
NPIS_ROLES = [DESIGNER_GROUP, REVIEWER_GROUP, CLIENT_GROUP]
|
|
19
|
+
ALLOW_ALL_APPLICATIONS = "/npis/npis-reviewer/access-allow-applications"
|
|
20
|
+
|
|
21
|
+
NEW_APPLICATION_STATUS = "New"
|
|
22
|
+
DRAFT_APPLICATION_STATUS = "Draft"
|
|
23
|
+
KEYCLOAK_DASHBOARD_BASE_GROUP = "npis-analytics"
|
|
24
|
+
ANONYMOUS_USER = "Anonymous-user"
|
|
25
|
+
|
|
26
|
+
FILTER_MAPS = {
|
|
27
|
+
"application_id": {"field": "id", "operator": "eq"},
|
|
28
|
+
"application_name": {"field": "form_name", "operator": "ilike"},
|
|
29
|
+
"application_status": {"field": "application_status", "operator": "eq"},
|
|
30
|
+
"created_by": {"field": "created_by", "operator": "eq"},
|
|
31
|
+
"modified_from": {"field": "modified", "operator": "ge"},
|
|
32
|
+
"modified_to": {"field": "modified", "operator": "le"},
|
|
33
|
+
"created_from": {"field": "created", "operator": "ge"},
|
|
34
|
+
"created_to": {"field": "created", "operator": "le"},
|
|
35
|
+
"form_name": {"field": "form_name", "operator": "ilike"},
|
|
36
|
+
"id": {"field": "id", "operator": "eq"},
|
|
37
|
+
"form_type": {"field": "form_type", "operator": "eq"},
|
|
38
|
+
"can_bundle": {"field": "can_bundle", "operator": "eq"},
|
|
39
|
+
"is_bundle": {"field": "is_bundle", "operator": "eq"},
|
|
40
|
+
"title":{"field": "title", "operator": "ilike"},
|
|
41
|
+
"category":{"field": "category", "operator": "ilike"},
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
DEFAULT_PROCESS_KEY = "Defaultflow"
|
|
45
|
+
DEFAULT_PROCESS_NAME = "Default Flow"
|
|
46
|
+
HTTP_TIMEOUT = 30
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Enum User Definition."""
|
|
2
|
+
from enum import Enum, unique
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class FormProcessMapperStatus(Enum):
|
|
6
|
+
"""This enum provides the list of FormProcessMapper Status."""
|
|
7
|
+
|
|
8
|
+
ACTIVE = "active"
|
|
9
|
+
INACTIVE = "inactive"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class MetricsState(Enum):
|
|
13
|
+
"""This enum provides the list of states of Metrics."""
|
|
14
|
+
|
|
15
|
+
CREATED = "created"
|
|
16
|
+
MODIFIED = "modified"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ApplicationSortingParameters: # pylint: disable=too-few-public-methods
|
|
20
|
+
"""This enum provides the list of Sorting Parameters."""
|
|
21
|
+
|
|
22
|
+
Id = "id"
|
|
23
|
+
Created = "created"
|
|
24
|
+
Name = "applicationName"
|
|
25
|
+
Status = "applicationStatus"
|
|
26
|
+
Modified = "modified"
|
|
27
|
+
FormName = "formName"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@unique
|
|
31
|
+
class FormioRoles(Enum):
|
|
32
|
+
"""Roles and corresponding machine names."""
|
|
33
|
+
|
|
34
|
+
CLIENT = "npisClient"
|
|
35
|
+
REVIEWER = "npisReviewer"
|
|
36
|
+
DESIGNER = "administrator"
|
|
37
|
+
ANONYMOUS = "anonymous"
|
|
38
|
+
RESOURCE_ID = "RESOURCE_ID"
|
|
39
|
+
|
|
40
|
+
@classmethod
|
|
41
|
+
def contains(cls, item: str) -> bool:
|
|
42
|
+
"""Checks if the parameter exists in the enum."""
|
|
43
|
+
return item in [entry.value for entry in cls]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@unique
|
|
47
|
+
class DraftStatus(Enum):
|
|
48
|
+
"""Draft status and corresponding values."""
|
|
49
|
+
|
|
50
|
+
ACTIVE = 1
|
|
51
|
+
INACTIVE = 0
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class DraftSortingParameters: # pylint: disable=too-few-public-methods
|
|
55
|
+
"""This enum provides the list of Sorting Parameters."""
|
|
56
|
+
|
|
57
|
+
id = "id"
|
|
58
|
+
Created = "created"
|
|
59
|
+
Name = "DraftName"
|
|
60
|
+
Status = "applicationStatus"
|
|
61
|
+
Modified = "modified"
|
|
62
|
+
FormName = "formName"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@unique
|
|
66
|
+
class FilterStatus(Enum):
|
|
67
|
+
"""This enum provides the filter status."""
|
|
68
|
+
|
|
69
|
+
ACTIVE = "active"
|
|
70
|
+
INACTIVE = "inactive"
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Module handle the console display formatting."""
|
|
2
|
+
import logging
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class CustomFormatter(logging.Formatter):
|
|
6
|
+
"""Class extends the logging Formatter class to support custom colour messages."""
|
|
7
|
+
|
|
8
|
+
blue = "\x1b[34;21m"
|
|
9
|
+
grey = "\x1b[38;21m"
|
|
10
|
+
green = "\x1b[32;1m"
|
|
11
|
+
yellow = "\x1b[33;21m"
|
|
12
|
+
red = "\x1b[31;21m"
|
|
13
|
+
bold_red = "\x1b[31;1m"
|
|
14
|
+
reset = "\x1b[0m"
|
|
15
|
+
__format = (
|
|
16
|
+
"%(asctime)s - %(name)s - %(levelname)s - %(message)s (%(filename)s:%(lineno)d)"
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
FORMATS = {
|
|
20
|
+
logging.DEBUG: blue + __format + reset,
|
|
21
|
+
logging.INFO: green + __format + reset,
|
|
22
|
+
logging.WARNING: yellow + __format + reset,
|
|
23
|
+
logging.ERROR: red + __format + reset,
|
|
24
|
+
logging.CRITICAL: bold_red + __format + reset,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
def format(self, record):
|
|
28
|
+
"""Returns the formatted information."""
|
|
29
|
+
log_fmt = self.FORMATS.get(record.levelno)
|
|
30
|
+
formatter = logging.Formatter(log_fmt)
|
|
31
|
+
return formatter.format(record)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Centralized setup of logging for the service."""
|
|
2
|
+
|
|
3
|
+
import logging.config
|
|
4
|
+
import sys
|
|
5
|
+
from os import path
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def setup_logging(conf):
|
|
9
|
+
"""Create the services logger."""
|
|
10
|
+
if conf and path.isfile(conf):
|
|
11
|
+
logging.config.fileConfig(conf)
|
|
12
|
+
print(f"Configure logging, from conf:{conf}", file=sys.stdout)
|
|
13
|
+
else:
|
|
14
|
+
print(
|
|
15
|
+
f"Unable to configure logging, attempted conf:{conf}",
|
|
16
|
+
file=sys.stderr,
|
|
17
|
+
)
|
|
18
|
+
return logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def log_error(msg):
|
|
22
|
+
"""Log error."""
|
|
23
|
+
logging.error(msg)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def log_bpm_error(msg):
|
|
27
|
+
"""Log error."""
|
|
28
|
+
logging.error(msg)
|
|
29
|
+
logging.error(
|
|
30
|
+
"""The connection with Python and Camunda API is not proper.
|
|
31
|
+
Ensure you have passed env variables properly and
|
|
32
|
+
have set listener in Keycloak(camunda-rest-api)"""
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def log_info(msg):
|
|
37
|
+
"""Log info."""
|
|
38
|
+
logging.info(msg)
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""Utility functions to manage pdf generation using selenium chrome."""
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
import json
|
|
5
|
+
|
|
6
|
+
from flask import current_app, make_response
|
|
7
|
+
from selenium.common.exceptions import TimeoutException
|
|
8
|
+
from selenium.webdriver.chrome.options import Options
|
|
9
|
+
from selenium.webdriver.chrome.service import Service
|
|
10
|
+
from selenium.webdriver.common.by import By
|
|
11
|
+
from selenium.webdriver.support import expected_conditions as EC
|
|
12
|
+
from selenium.webdriver.support.ui import WebDriverWait
|
|
13
|
+
from seleniumwire import webdriver
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def send_devtools(driver, cmd, params=None):
|
|
17
|
+
"""Chrome dev tools execution function."""
|
|
18
|
+
resource = "/session/" + driver.session_id + "/chromium/send_command_and_get_result"
|
|
19
|
+
# pylint: disable=protected-access
|
|
20
|
+
url = driver.command_executor._url + resource
|
|
21
|
+
body = json.dumps({"cmd": cmd, "params": params})
|
|
22
|
+
# pylint: disable=protected-access
|
|
23
|
+
response = driver.command_executor._request("POST", url, body)
|
|
24
|
+
if response.get("status"):
|
|
25
|
+
raise Exception(response.get("value"))
|
|
26
|
+
return response.get("value")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# pylint: disable=R1710
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def get_pdf_from_html(path, chromedriver=None, p_options=None, args=None):
|
|
33
|
+
"""Load url in chrome web driver and print as pdf."""
|
|
34
|
+
|
|
35
|
+
def interceptor(request):
|
|
36
|
+
request.headers["Authorization"] = args["auth_token"]
|
|
37
|
+
|
|
38
|
+
if args is None:
|
|
39
|
+
args = {}
|
|
40
|
+
|
|
41
|
+
options = Options()
|
|
42
|
+
options.add_argument("--headless")
|
|
43
|
+
options.add_argument("--disable-gpu")
|
|
44
|
+
options.add_argument("--no-sandbox")
|
|
45
|
+
options.add_argument("--disable-dev-shm-usage")
|
|
46
|
+
options.add_argument("--run-all-compositor-stages-before-draw")
|
|
47
|
+
options.add_argument("--disable-logging")
|
|
48
|
+
options.add_argument("--log-level=3")
|
|
49
|
+
sel_options = {"request_storage_base_dir": "/tmp"}
|
|
50
|
+
|
|
51
|
+
service = Service(executable_path=chromedriver)
|
|
52
|
+
# pylint: disable=E1123
|
|
53
|
+
driver = webdriver.Chrome(
|
|
54
|
+
service=service, options=options, seleniumwire_options=sel_options
|
|
55
|
+
)
|
|
56
|
+
driver.set_window_size(1920, 1080)
|
|
57
|
+
|
|
58
|
+
if "auth_token" in args:
|
|
59
|
+
driver.request_interceptor = interceptor
|
|
60
|
+
|
|
61
|
+
if "timezone" in args and args["timezone"] is not None:
|
|
62
|
+
tz_params = {"timezoneId": args["timezone"]}
|
|
63
|
+
driver.execute_cdp_cmd("Emulation.setTimezoneOverride", tz_params)
|
|
64
|
+
|
|
65
|
+
driver.get(path)
|
|
66
|
+
|
|
67
|
+
try:
|
|
68
|
+
if "wait" in args:
|
|
69
|
+
delay = 30 # seconds
|
|
70
|
+
elem_loc = EC.presence_of_element_located((By.CLASS_NAME, args["wait"]))
|
|
71
|
+
WebDriverWait(driver, delay).until(elem_loc)
|
|
72
|
+
calculated_print_options = {
|
|
73
|
+
"landscape": False,
|
|
74
|
+
"displayHeaderFooter": False,
|
|
75
|
+
"printBackground": True,
|
|
76
|
+
"preferCSSPageSize": True,
|
|
77
|
+
}
|
|
78
|
+
if p_options is not None:
|
|
79
|
+
calculated_print_options.update(p_options)
|
|
80
|
+
result = send_devtools(driver, "Page.printToPDF", calculated_print_options)
|
|
81
|
+
driver.quit()
|
|
82
|
+
return base64.b64decode(result["data"])
|
|
83
|
+
|
|
84
|
+
except TimeoutException as err:
|
|
85
|
+
driver.quit()
|
|
86
|
+
current_app.logger.warning(err)
|
|
87
|
+
return False
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def pdf_response(result, file_name="Pdf.pdf"):
|
|
91
|
+
"""Render pdf response from html content."""
|
|
92
|
+
response = make_response(result)
|
|
93
|
+
response.headers["Content-Type"] = "application/pdf"
|
|
94
|
+
response.headers["Content-Disposition"] = "inline; filename=" + file_name
|
|
95
|
+
return response
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def save_pdf_local(result, file_name="Pdf.pdf"):
|
|
99
|
+
"""Save html content as pdf response."""
|
|
100
|
+
with open(file_name, "wb") as file:
|
|
101
|
+
file.write(result)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Utility function for profiling functions."""
|
|
2
|
+
import time
|
|
3
|
+
from functools import wraps
|
|
4
|
+
|
|
5
|
+
from flask import current_app
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def profiletime(profile_fn):
|
|
9
|
+
"""Function to profile time."""
|
|
10
|
+
|
|
11
|
+
@wraps(profile_fn)
|
|
12
|
+
def measure_time(*args, **kwargs):
|
|
13
|
+
"""Measure the API response time using time module."""
|
|
14
|
+
start_time = time.time()
|
|
15
|
+
result = profile_fn(*args, **kwargs)
|
|
16
|
+
end_time = time.time()
|
|
17
|
+
diff = end_time - start_time
|
|
18
|
+
|
|
19
|
+
current_app.logger.info(
|
|
20
|
+
f"API endpoint: {profile_fn.__qualname__} took {diff} seconds"
|
|
21
|
+
)
|
|
22
|
+
return result
|
|
23
|
+
|
|
24
|
+
return measure_time
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""
|
|
2
|
+
App initialization.
|
|
3
|
+
Functions to initialize app and startup
|
|
4
|
+
|
|
5
|
+
"""
|
|
6
|
+
from typing import Dict
|
|
7
|
+
from npis_api_utils.exceptions import BusinessException
|
|
8
|
+
from npis_api_utils.schemas import FormioRoleSchema
|
|
9
|
+
from npis_api_utils.services.external import FormioService
|
|
10
|
+
from npis_api_utils.utils import cache
|
|
11
|
+
from npis_api_utils.utils.enums import FormioRoles
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def setup_jwt_manager(app, jwt_manager):
|
|
15
|
+
"""Use flask app to configure the JWTManager to work for a particular Realm."""
|
|
16
|
+
|
|
17
|
+
def get_roles(a_dict):
|
|
18
|
+
resource = a_dict["resource_access"].get(app.config["JWT_OIDC_AUDIENCE"])
|
|
19
|
+
return resource["roles"] if resource else a_dict["roles"]
|
|
20
|
+
|
|
21
|
+
app.config["JWT_ROLE_CALLBACK"] = get_roles
|
|
22
|
+
jwt_manager.init_app(app)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def collect_role_ids(app):
|
|
26
|
+
"""Collect role ids from Form.io."""
|
|
27
|
+
try:
|
|
28
|
+
service = FormioService()
|
|
29
|
+
app.logger.info("Establishing new connection to formio...")
|
|
30
|
+
role_ids = FormioRoleSchema().load(service.get_role_ids(), many=True)
|
|
31
|
+
role_ids_filtered = list(filter(None, map(standardization_fn, role_ids)))
|
|
32
|
+
# Cache will be having infinite expiry
|
|
33
|
+
if role_ids:
|
|
34
|
+
cache.set(
|
|
35
|
+
"formio_role_ids",
|
|
36
|
+
role_ids_filtered,
|
|
37
|
+
timeout=0,
|
|
38
|
+
)
|
|
39
|
+
app.logger.info("Role ids saved to cache successfully.")
|
|
40
|
+
except BusinessException as err:
|
|
41
|
+
app.logger.error(err.error)
|
|
42
|
+
except Exception as err: # pylint: disable=broad-except
|
|
43
|
+
app.logger.error(err)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def collect_user_resource_ids(app):
|
|
47
|
+
"""Collects user resource ids from Form.io."""
|
|
48
|
+
try:
|
|
49
|
+
service = FormioService()
|
|
50
|
+
user_resource = service.get_user_resource_ids()
|
|
51
|
+
user_resource_id = user_resource["_id"]
|
|
52
|
+
if user_resource:
|
|
53
|
+
cache.set(
|
|
54
|
+
"user_resource_id",
|
|
55
|
+
user_resource_id,
|
|
56
|
+
timeout=0,
|
|
57
|
+
)
|
|
58
|
+
app.logger.info("User resource ids saved to cache successfully.")
|
|
59
|
+
except Exception as err: # pylint: disable=broad-except
|
|
60
|
+
app.logger.error(err)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def standardization_fn(item: Dict) -> Dict or None:
|
|
64
|
+
"""Updates the type value to enum key for standardization."""
|
|
65
|
+
if FormioRoles.contains(item["type"]):
|
|
66
|
+
item["type"] = FormioRoles(item["type"]).name
|
|
67
|
+
return item
|
|
68
|
+
return None
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""Translations dictionary."""
|
|
2
|
+
# pylint: skip-file
|
|
3
|
+
|
|
4
|
+
translations = {
|
|
5
|
+
"fr": {
|
|
6
|
+
"errors": "les erreurs",
|
|
7
|
+
"type": "taper",
|
|
8
|
+
"message": "message",
|
|
9
|
+
"Bad request error": "Mauvaise erreur de demande",
|
|
10
|
+
"Invalid request data object": "Objet de données de requête non valide",
|
|
11
|
+
"Invalid Token Error": "Erreur de jeton non valide",
|
|
12
|
+
"Invalid Request Object": "Objet de demande non valide",
|
|
13
|
+
"Required fields are not passed": "Les champs obligatoires ne sont pas passés",
|
|
14
|
+
"Access to NPIS API Denied. Check if the bearer token is passed for Authorization or has expired.": "Accès à l'API NPIS refusé. Vérifiez si le jeton du porteur est passé pour l'autorisation ou a expiré.",
|
|
15
|
+
"Authorization error": "Erreur d'autorisation",
|
|
16
|
+
"Permission denied": "Permission refusée",
|
|
17
|
+
"Invalid application request passed": "Demande de candidature invalide transmise",
|
|
18
|
+
"Invalid Request Object Passed": "Objet de demande non valide transmis",
|
|
19
|
+
"Welcome to NPIS API": "Bienvenue dans l'API NPIS",
|
|
20
|
+
"Dashboards not available": "Tableaux de bord non disponibles",
|
|
21
|
+
"Error": "Erreur",
|
|
22
|
+
"Dashboard not found": "Tableau de bord introuvable",
|
|
23
|
+
"Invalid request object passed for FormProcessmapper POST API": "Objet de demande non valide transmis pour l'API POST FormProcessmapper",
|
|
24
|
+
"Invalid response data": "Données de réponse non valides",
|
|
25
|
+
"Invalid request passed": "Demande invalide transmise",
|
|
26
|
+
"Invalid Request Object format": "Format d'objet de demande non valide",
|
|
27
|
+
"Missing from_date or to_date. Invalid request object for application metrics endpoint": "From_date ou to_date manquant. Objet de requête non valide pour le point de terminaison des métriques d'application",
|
|
28
|
+
"Error while getting application metrics": "Erreur lors de l'obtention des métriques d'application",
|
|
29
|
+
},
|
|
30
|
+
"zh-CN": {
|
|
31
|
+
"errors": "错误",
|
|
32
|
+
"type": "类型",
|
|
33
|
+
"message": "信息",
|
|
34
|
+
"Bad request error": "错误的请求错误",
|
|
35
|
+
"Invalid request data object": "请求数据对象无效",
|
|
36
|
+
"Invalid Token Error": "无效令牌错误",
|
|
37
|
+
"Invalid Request Object": "无效的请求对象",
|
|
38
|
+
"Required fields are not passed": "必填字段未通过",
|
|
39
|
+
"Access to NPIS API Denied. Check if the bearer token is passed for Authorization or has expired.": "拒绝访问 N API。检查不记名令牌是否已通过授权或已过期。",
|
|
40
|
+
"Authorization error": "授权错误",
|
|
41
|
+
"Permission denied": "“没有权限",
|
|
42
|
+
"Invalid application request passed": "通过了无效的应用程序请求",
|
|
43
|
+
"Invalid Request Object Passed": "传递的请求对象无效",
|
|
44
|
+
"Welcome to NPIS API": "欢迎使用 NPIS API",
|
|
45
|
+
"Dashboards not available": "仪表板不可用",
|
|
46
|
+
"Error": "错误",
|
|
47
|
+
"Dashboard not found": "未找到仪表板",
|
|
48
|
+
"Invalid request object passed for FormProcessmapper POST API": "为 FormProcessmapper POST API 传递的请求对象无效",
|
|
49
|
+
"Invalid response data": "无效的响应数据",
|
|
50
|
+
"Invalid request passed": "无效请求已通过",
|
|
51
|
+
"Invalid Request Object format": "请求对象格式无效",
|
|
52
|
+
"Missing from_date or to_date. Invalid request object for application metrics endpoint": "缺少 from_date 或 to_date。应用程序指标端点的请求对象无效",
|
|
53
|
+
"Error while getting application metrics": "获取应用程序指标时出错",
|
|
54
|
+
},
|
|
55
|
+
"bg": {
|
|
56
|
+
"errors": "errors",
|
|
57
|
+
"type": "Тип",
|
|
58
|
+
"message": "съобщение",
|
|
59
|
+
"Bad request error": "Грешка в лоша заявка",
|
|
60
|
+
"Invalid request data object": "Невалиден обект с данни за заявка",
|
|
61
|
+
"Invalid Token Error": "Грешка с невалиден токен",
|
|
62
|
+
"Invalid Request Object": "Невалиден обект на заявка",
|
|
63
|
+
"Required fields are not passed": "Задължителните полета не се предават",
|
|
64
|
+
"Access to NPIS API Denied. Check if the bearer token is passed for Authorization or has expired.": "Достъпът до API на NPIS е отказан. Проверете дали токенът на носител е предаден за оторизация или е изтекъл.",
|
|
65
|
+
"Authorization error": "Грешка в упълномощаването",
|
|
66
|
+
"Permission denied": "Permission denied",
|
|
67
|
+
"Invalid application request passed": "Невалидна заявка за приложение е преминала",
|
|
68
|
+
"Invalid Request Object Passed": "Предаден е невалиден обект на заявка",
|
|
69
|
+
"Welcome to NPIS API": "Добре дошли в API на NPIS",
|
|
70
|
+
"Dashboards not available": "Таблата за управление не са налични",
|
|
71
|
+
"Error": "Грешка",
|
|
72
|
+
"Dashboard not found": "Таблото за управление не е намерено",
|
|
73
|
+
"Invalid request object passed for FormProcessmapper POST API": "Предаден е невалиден обект на заявка за FormProcessmapper POST API",
|
|
74
|
+
"Invalid response data": "Невалидни данни за отговор",
|
|
75
|
+
"Invalid request passed": "Невалидна заявка е преминала",
|
|
76
|
+
"Invalid Request Object format": "Невалиден формат на обекта на заявка",
|
|
77
|
+
"Missing from_date or to_date. Invalid request object for application metrics endpoint": "Липсва от_дата или до_дата. Невалиден обект на заявка за крайна точка на показателите на приложението",
|
|
78
|
+
"Error while getting application metrics": "Грешка при получаване на показатели за приложението",
|
|
79
|
+
},
|
|
80
|
+
"pt": {
|
|
81
|
+
"errors": "erros",
|
|
82
|
+
"type": "modelo",
|
|
83
|
+
"message": "mensagem",
|
|
84
|
+
"Bad request error": "Erro de solicitação incorreta",
|
|
85
|
+
"Invalid request data object": "Objeto de dados de solicitação inválido",
|
|
86
|
+
"Invalid Token Error": "Erro de token inválido",
|
|
87
|
+
"Invalid Request Object": "Objeto de solicitação inválido",
|
|
88
|
+
"Required fields are not passed": "Campos obrigatórios não são passados",
|
|
89
|
+
"Access to NPIS API Denied. Check if the bearer token is passed for Authorization or has expired.": "Acesso negado à API do NPIS. Verifique se o token do portador foi aprovado para autorização ou expirou.",
|
|
90
|
+
"Authorization error": "Erro de autorização",
|
|
91
|
+
"Permission denied": "Permissão negada",
|
|
92
|
+
"Invalid application request passed": "Solicitação de inscrição inválida aprovada",
|
|
93
|
+
"Invalid Request Object Passed": "Objeto de solicitação inválido passado",
|
|
94
|
+
"Welcome to NPIS API": "Bem-vindo à API do NPIS",
|
|
95
|
+
"Dashboards not available": "Painéis não disponíveis",
|
|
96
|
+
"Error": "Erro",
|
|
97
|
+
"Dashboard not found": "Painel não encontrado",
|
|
98
|
+
"Invalid request object passed for FormProcessmapper POST API": "Objeto de solicitação inválido passado para API POST FormProcessmapper",
|
|
99
|
+
"Invalid response data": "Dados de resposta inválidos",
|
|
100
|
+
"Invalid request passed": "Solicitação inválida aprovada",
|
|
101
|
+
"Invalid Request Object format": "Formato de objeto de solicitação inválido",
|
|
102
|
+
"Missing from_date or to_date. Invalid request object for application metrics endpoint": "Está faltando from_date ou to_date. Objeto de solicitação inválido para o endpoint de métricas do aplicativo",
|
|
103
|
+
"Error while getting application metrics": "Erro ao obter as métricas do aplicativo",
|
|
104
|
+
},
|
|
105
|
+
"de": {
|
|
106
|
+
"errors": "Fehler",
|
|
107
|
+
"type": "Typ",
|
|
108
|
+
"message": "Botschaft",
|
|
109
|
+
"Bad request error": "Ungültiger Anforderungsfehler",
|
|
110
|
+
"Invalid request data object": "Ungültiges Anforderungsdatenobjekt",
|
|
111
|
+
"Invalid Token Error": "Ungültiger Token-Fehler",
|
|
112
|
+
"Invalid Request Object": "Ungültiges Anforderungsobjekt",
|
|
113
|
+
"Required fields are not passed": "Erforderliche Felder werden nicht übergeben",
|
|
114
|
+
"Access to NPIS API Denied. Check if the bearer token is passed for Authorization or has expired.": "Zugriff auf die Formsflow.ai-API verweigert. Überprüfen Sie, ob das Bearer-Token für die Autorisierung übergeben wurde oder abgelaufen ist.",
|
|
115
|
+
"Authorization error": "Autorisierungsfehlero",
|
|
116
|
+
"Permission denied": "Zugang verweigert",
|
|
117
|
+
"Invalid application request passed": "Ungültige Anwendungsanforderung bestanden",
|
|
118
|
+
"Invalid Request Object Passed": "Ungültiges Anforderungsobjekt übergeben",
|
|
119
|
+
"Welcome to NPIS API": "Willkommen bei der NPIS-API",
|
|
120
|
+
"Dashboards not available": "Dashboards nicht verfügbar",
|
|
121
|
+
"Error": "Fehler",
|
|
122
|
+
"Dashboard not found": "Dashboard nicht gefunden",
|
|
123
|
+
"Invalid request object passed for FormProcessmapper POST API": "Ungültiges Anforderungsobjekt für FormProcessmapper POST API übergeben",
|
|
124
|
+
"Invalid response data": "Ungültige Antwortdaten",
|
|
125
|
+
"Invalid request passed": "Ungültige Anfrage bestanden",
|
|
126
|
+
"Invalid Request Object format": "Ungültiges Anforderungsobjektformat",
|
|
127
|
+
"Missing from_date or to_date. Invalid request object for application metrics endpoint": "From_date oder to_date fehlen. Ungültiges Anforderungsobjekt für Endpunkt der Anwendungsmetriken",
|
|
128
|
+
"Error while getting application metrics": "Fehler beim Abrufen von Anwendungsmetriken",
|
|
129
|
+
},
|
|
130
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""User Context to hold request scoped variables."""
|
|
2
|
+
|
|
3
|
+
import functools
|
|
4
|
+
from typing import Dict, List
|
|
5
|
+
|
|
6
|
+
from flask import current_app, g, request
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _get_context():
|
|
10
|
+
"""Return User context."""
|
|
11
|
+
return UserContext()
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class UserContext: # pylint: disable=too-many-instance-attributes
|
|
15
|
+
"""Object to hold request scoped user context."""
|
|
16
|
+
|
|
17
|
+
def __init__(self):
|
|
18
|
+
"""Return a User Context object."""
|
|
19
|
+
token_info: Dict = _get_token_info()
|
|
20
|
+
self._tenant_key = token_info.get("tenantKey", None)
|
|
21
|
+
self._user_name = token_info.get("preferred_username", None)
|
|
22
|
+
self._bearer_token: str = _get_token()
|
|
23
|
+
self.token_info = token_info
|
|
24
|
+
self._email = token_info.get("email")
|
|
25
|
+
self._roles: list = token_info.get("roles", None) or token_info.get(
|
|
26
|
+
"role", None
|
|
27
|
+
)
|
|
28
|
+
self._groups: list = token_info.get("groups", None)
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def tenant_key(self) -> str:
|
|
32
|
+
"""Return the tenant key."""
|
|
33
|
+
return self._tenant_key
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def user_name(self) -> str:
|
|
37
|
+
"""Return the user name."""
|
|
38
|
+
return self._user_name
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def bearer_token(self) -> str:
|
|
42
|
+
"""Return the bearer_token."""
|
|
43
|
+
return self._bearer_token
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def email(self) -> str:
|
|
47
|
+
"""Return the email."""
|
|
48
|
+
return self._email
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def roles(self) -> List[str]:
|
|
52
|
+
"""Return the roles."""
|
|
53
|
+
return self._roles
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def group_or_roles(self) -> List[str]:
|
|
57
|
+
"""Return groups is env is using groups, else roles."""
|
|
58
|
+
return (
|
|
59
|
+
self._roles
|
|
60
|
+
if current_app.config.get("KEYCLOAK_ENABLE_CLIENT_AUTH")
|
|
61
|
+
else self._groups
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def user_context(function):
|
|
66
|
+
"""Add user context object as an argument to function."""
|
|
67
|
+
|
|
68
|
+
@functools.wraps(function)
|
|
69
|
+
def wrapper(*func_args, **func_kwargs):
|
|
70
|
+
context = _get_context()
|
|
71
|
+
func_kwargs["user"] = context
|
|
72
|
+
return function(*func_args, **func_kwargs)
|
|
73
|
+
|
|
74
|
+
return wrapper
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _get_token_info() -> Dict:
|
|
78
|
+
return g.jwt_oidc_token_info if g and "jwt_oidc_token_info" in g else {}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _get_token() -> str:
|
|
82
|
+
token: str = (
|
|
83
|
+
request.headers["Authorization"]
|
|
84
|
+
if request and "Authorization" in request.headers
|
|
85
|
+
else None
|
|
86
|
+
)
|
|
87
|
+
return token
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""Common utils.
|
|
2
|
+
|
|
3
|
+
CORS pre-flight decorator. A simple decorator to add the options
|
|
4
|
+
method to a Request Class.
|
|
5
|
+
camel_to_snake - Converts camel case to snake case.
|
|
6
|
+
validate_sort_order_and_order_by - Utility function to validate
|
|
7
|
+
if sort order and sort order by is correct.
|
|
8
|
+
translate - Translate the response to provided language
|
|
9
|
+
"""
|
|
10
|
+
import re
|
|
11
|
+
from typing import Tuple
|
|
12
|
+
|
|
13
|
+
from .constants import (
|
|
14
|
+
ALLOW_ALL_ORIGINS,
|
|
15
|
+
CLIENT_GROUP,
|
|
16
|
+
DESIGNER_GROUP,
|
|
17
|
+
REVIEWER_GROUP,
|
|
18
|
+
)
|
|
19
|
+
from .enums import (
|
|
20
|
+
ApplicationSortingParameters,
|
|
21
|
+
DraftSortingParameters,
|
|
22
|
+
FormioRoles,
|
|
23
|
+
)
|
|
24
|
+
from .translations.translations import translations
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def cors_preflight(methods: str = "GET"):
|
|
28
|
+
"""Render an option method on the class."""
|
|
29
|
+
|
|
30
|
+
def wrapper(f): # pylint: disable=invalid-name
|
|
31
|
+
def options(self, *args, **kwargs): # pylint: disable=unused-argument
|
|
32
|
+
return (
|
|
33
|
+
{"Allow": "GET"},
|
|
34
|
+
200,
|
|
35
|
+
{
|
|
36
|
+
"Access-Control-Allow-Origin": ALLOW_ALL_ORIGINS,
|
|
37
|
+
"Access-Control-Allow-Methods": methods,
|
|
38
|
+
"Access-Control-Allow-Headers": "Authorization, Content-Type",
|
|
39
|
+
},
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
setattr(f, "options", options)
|
|
43
|
+
return f
|
|
44
|
+
|
|
45
|
+
return wrapper
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def camel_to_snake(name: str) -> str:
|
|
49
|
+
"""Convert camel case to snake case."""
|
|
50
|
+
s_1 = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", name)
|
|
51
|
+
return re.sub("([a-z0-9])([A-Z])", r"\1_\2", s_1).lower()
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def validate_sort_order_and_order_by(order_by: str, sort_order: str) -> bool:
|
|
55
|
+
"""Validate sort order and order by."""
|
|
56
|
+
if order_by not in [
|
|
57
|
+
ApplicationSortingParameters.Id,
|
|
58
|
+
ApplicationSortingParameters.Name,
|
|
59
|
+
ApplicationSortingParameters.Status,
|
|
60
|
+
ApplicationSortingParameters.Modified,
|
|
61
|
+
ApplicationSortingParameters.FormName,
|
|
62
|
+
DraftSortingParameters.Name,
|
|
63
|
+
]:
|
|
64
|
+
order_by = None
|
|
65
|
+
else:
|
|
66
|
+
if order_by in [ApplicationSortingParameters.Name, DraftSortingParameters.Name]:
|
|
67
|
+
order_by = ApplicationSortingParameters.FormName
|
|
68
|
+
order_by = camel_to_snake(order_by)
|
|
69
|
+
if sort_order not in ["asc", "desc"]:
|
|
70
|
+
sort_order = None
|
|
71
|
+
return order_by, sort_order
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def translate(to_lang: str, data: dict) -> dict:
|
|
75
|
+
"""Translate the response to provided language.
|
|
76
|
+
|
|
77
|
+
will return the translated object if there is match
|
|
78
|
+
else return the original object
|
|
79
|
+
"""
|
|
80
|
+
try:
|
|
81
|
+
translated_data = {}
|
|
82
|
+
if to_lang not in translations:
|
|
83
|
+
raise KeyError
|
|
84
|
+
for key, value in data.items():
|
|
85
|
+
# if matching translation is present for either key / value,
|
|
86
|
+
# then translated string is used
|
|
87
|
+
# original string otherwise
|
|
88
|
+
translated_data[
|
|
89
|
+
translations[to_lang][key] if key in translations[to_lang] else key
|
|
90
|
+
] = (
|
|
91
|
+
translations[to_lang][value]
|
|
92
|
+
if value in translations[to_lang]
|
|
93
|
+
else value
|
|
94
|
+
)
|
|
95
|
+
return translated_data
|
|
96
|
+
except KeyError as err:
|
|
97
|
+
raise err
|
|
98
|
+
except Exception as err:
|
|
99
|
+
raise err
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def get_role_ids_from_user_groups(role_ids, user_role):
|
|
103
|
+
"""Filters out formio role ids specific to user groups."""
|
|
104
|
+
if role_ids is None or user_role is None:
|
|
105
|
+
return None
|
|
106
|
+
|
|
107
|
+
if DESIGNER_GROUP in user_role:
|
|
108
|
+
return role_ids
|
|
109
|
+
if REVIEWER_GROUP in user_role:
|
|
110
|
+
return filter_list_by_user_role(FormioRoles.REVIEWER.name, role_ids)
|
|
111
|
+
if CLIENT_GROUP in user_role:
|
|
112
|
+
return filter_list_by_user_role(FormioRoles.CLIENT.name, role_ids)
|
|
113
|
+
return None
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def filter_list_by_user_role(formio_role, role_ids):
|
|
117
|
+
"""Iterate over role_ids and return entries with matching formio role."""
|
|
118
|
+
return list(filter(lambda item: item["type"] == formio_role, role_ids))
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def get_form_and_submission_id_from_form_url(form_url: str) -> Tuple:
|
|
122
|
+
"""Retrieves the formid and submission id from the url parameters."""
|
|
123
|
+
form_id = form_url[form_url.find("/form/") + 6 : form_url.find("/submission/")]
|
|
124
|
+
submission_id = form_url[form_url.find("/submission/") + 12 : len(form_url)]
|
|
125
|
+
return (form_id, submission_id)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: npis-api-utils
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: NPIS api related libraries.
|
|
5
|
+
Home-page: https://github.com/katxeus/npis_api_utils
|
|
6
|
+
Author: JK
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
|
|
9
|
+
# NPIS UTILS
|
|
10
|
+
|
|
11
|
+
  
|
|
12
|
+
[](https://pycqa.github.io/isort/) [](https://github.com/psf/black)[](https://github.com/PyCQA/pylint)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
This Python package include all the libraries and utils needed for NPIS related services.
|
|
16
|
+
|
|
17
|
+
## Usage
|
|
18
|
+
|
|
19
|
+
Install using pip
|
|
20
|
+
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
MANIFEST.in
|
|
2
|
+
README.md
|
|
3
|
+
requirements.txt
|
|
4
|
+
setup.py
|
|
5
|
+
src/npis_api_utils/__init__.py
|
|
6
|
+
src/npis_api_utils.egg-info/PKG-INFO
|
|
7
|
+
src/npis_api_utils.egg-info/SOURCES.txt
|
|
8
|
+
src/npis_api_utils.egg-info/dependency_links.txt
|
|
9
|
+
src/npis_api_utils.egg-info/not-zip-safe
|
|
10
|
+
src/npis_api_utils.egg-info/requires.txt
|
|
11
|
+
src/npis_api_utils.egg-info/top_level.txt
|
|
12
|
+
src/npis_api_utils/exceptions/__init__.py
|
|
13
|
+
src/npis_api_utils/schemas/__init__.py
|
|
14
|
+
src/npis_api_utils/schemas/formio_roles.py
|
|
15
|
+
src/npis_api_utils/services/__init__.py
|
|
16
|
+
src/npis_api_utils/services/external/__init__.py
|
|
17
|
+
src/npis_api_utils/services/external/formio.py
|
|
18
|
+
src/npis_api_utils/utils/__init__.py
|
|
19
|
+
src/npis_api_utils/utils/auth.py
|
|
20
|
+
src/npis_api_utils/utils/caching.py
|
|
21
|
+
src/npis_api_utils/utils/constants.py
|
|
22
|
+
src/npis_api_utils/utils/enums.py
|
|
23
|
+
src/npis_api_utils/utils/format.py
|
|
24
|
+
src/npis_api_utils/utils/logging.py
|
|
25
|
+
src/npis_api_utils/utils/pdf.py
|
|
26
|
+
src/npis_api_utils/utils/profiler.py
|
|
27
|
+
src/npis_api_utils/utils/roles.py
|
|
28
|
+
src/npis_api_utils/utils/startup.py
|
|
29
|
+
src/npis_api_utils/utils/user_context.py
|
|
30
|
+
src/npis_api_utils/utils/util.py
|
|
31
|
+
src/npis_api_utils/utils/translations/__init__.py
|
|
32
|
+
src/npis_api_utils/utils/translations/translations.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Flask
|
|
2
|
+
Flask-Caching
|
|
3
|
+
Flask-Migrate
|
|
4
|
+
Flask-Moment
|
|
5
|
+
Flask-SQLAlchemy
|
|
6
|
+
PyJWT
|
|
7
|
+
Werkzeug
|
|
8
|
+
flask-jwt-oidc
|
|
9
|
+
flask-marshmallow
|
|
10
|
+
flask-restx
|
|
11
|
+
gunicorn
|
|
12
|
+
markupsafe
|
|
13
|
+
marshmallow-sqlalchemy
|
|
14
|
+
psycopg2-binary
|
|
15
|
+
python-dotenv
|
|
16
|
+
requests
|
|
17
|
+
selenium
|
|
18
|
+
selenium-wire
|
|
19
|
+
sqlalchemy_utils
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
npis_api_utils
|