Skip to content

Commit 9356cf2

Browse files
authored
Merge branch 'main' into import-risk-excel
2 parents b640b84 + 8a0b50a commit 9356cf2

39 files changed

+2110
-312
lines changed

.dockerignore

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
*$py.class
2+
*.egg
3+
*.egg-info
4+
*.pyc
5+
*.sqlite
6+
*.sublime-project
7+
*.sublime-workspace
8+
*~
9+
.*.sw?
10+
.coverage
11+
.DS_Store
12+
.mypy_cache/
13+
.pytest_cache/
14+
.sw?
15+
.tox/
16+
build/
17+
coverage
18+
coverage.xml
19+
Data.fs*
20+
data/
21+
development*.ini
22+
dist/
23+
venv/
24+
htmlcov/
25+
nosetests.xml
26+
production.ini
27+
pyvenv.cfg
28+
test
29+
tmp/

Dockerfile

+12-6
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,21 @@
1-
FROM python:3.12.2-alpine3.19
1+
FROM python:3.12-slim
22

33
# Set the working directory
44
WORKDIR /app
55

6-
# Install PostgreSQL development packages required for psycopg
7-
RUN apk add --no-cache postgresql-dev gcc python3-dev musl-dev
6+
# Install PostgreSQL development packages and other dependencies required for psycopg
7+
RUN apt-get update && \
8+
apt-get install -y --no-install-recommends \
9+
postgresql-server-dev-all \
10+
gcc \
11+
python3-dev \
12+
build-essential \
13+
linux-headers-amd64 && \
14+
apt-get clean && \
15+
rm -rf /var/lib/apt/lists/*
816

917
# Copy the current directory contents into the container at /app
1018
COPY . /app
1119

1220
# Install any needed packages specified in requirements.txt
13-
RUN pip install --no-cache-dir -r requirements.txt
14-
15-
ENTRYPOINT [ "pserve" ]
21+
RUN python -m pip install --no-cache-dir -r requirements.txt

development.ini.example

+3
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ pyramid.available_languages =
2121

2222
sqlalchemy.url = sqlite:///%(here)s/riskmatrix.sqlite
2323

24+
openai_api_key=
25+
anthropic_api_key=
26+
2427
session.type = file
2528
session.data_dir = %(here)s/data/sessions/data
2629
session.lock_dir = %(here)s/data/sessions/lock

requirements.txt

+6-1
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,12 @@ zope.event==5.0
4848
zope.interface==6.1
4949
zope.schema==7.0.1
5050
zope.sqlalchemy==3.1
51-
psycopg2-binary==2.9.9
51+
numpy==1.26.3
52+
openai==1.12.0
53+
plotly==5.18.0
54+
anthropic
5255
redis[hiredis]==5.0.3
56+
uwsgi==2.0.25.1
57+
psycopg2-binary==2.9.9
5358

5459
-e .

src/riskmatrix/__init__.py

+43-1
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,18 @@
22
from pyramid.config import Configurator
33
from pyramid_beaker import session_factory_from_settings
44
from typing import Any
5+
from email.headerregistry import Address
6+
from pyramid.settings import asbool
7+
from .mail import PostmarkMailer
58

69
from riskmatrix.flash import MessageQueue
710
from riskmatrix.i18n import LocaleNegotiator
811
from riskmatrix.layouts.steps import show_steps
912
from riskmatrix.route_factories import root_factory
1013
from riskmatrix.security import authenticated_user
1114
from riskmatrix.security_policy import SessionSecurityPolicy
15+
from openai import OpenAI
16+
from anthropic import Anthropic
1217

1318

1419
from typing import TYPE_CHECKING
@@ -22,6 +27,19 @@
2227
def includeme(config: Configurator) -> None:
2328
settings = config.registry.settings
2429

30+
default_sender = settings.get(
31+
'email.default_sender',
32+
33+
)
34+
token = settings.get('mail.postmark_token', '')
35+
stream = settings.get('mail.postmark_stream', 'development')
36+
blackhole = asbool(settings.get('mail.postmark_blackhole', False))
37+
config.registry.registerUtility(PostmarkMailer(
38+
Address(addr_spec=default_sender),
39+
token,
40+
stream,
41+
blackhole=blackhole
42+
))
2543
config.include('pyramid_beaker')
2644
config.include('pyramid_chameleon')
2745
config.include('pyramid_layout')
@@ -65,11 +83,35 @@ def main(
6583
environment=sentry_environment,
6684
integrations=[PyramidIntegration(), SqlalchemyIntegration()],
6785
traces_sample_rate=1.0,
68-
profiles_sample_rate=0.25,
86+
profiles_sample_rate=1.0,
87+
enable_tracing=True,
88+
send_default_pii=True
6989
)
90+
print("configured sentry")
91+
print(sentry_dsn)
7092

7193
with Configurator(settings=settings, root_factory=root_factory) as config:
7294
includeme(config)
7395

96+
if openai_apikey := settings.get('openai_api_key'):
97+
98+
openai_client = OpenAI(
99+
api_key=openai_apikey
100+
)
101+
config.add_request_method(
102+
lambda r: openai_client,
103+
'openai',
104+
reify=True
105+
)
106+
if anthropic_apikey := settings.get('anthropic_api_key'):
107+
anthropic_client = Anthropic(
108+
api_key=anthropic_apikey
109+
)
110+
config.add_request_method(
111+
lambda r: anthropic_client,
112+
'anthropic',
113+
reify=True
114+
)
115+
74116
app = config.make_wsgi_app()
75117
return Fanstatic(app, versioning=True)

src/riskmatrix/data_table.py

-1
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,6 @@ def cell(self, data: Any) -> str:
119119
params = {}
120120
if 'class_name' in self.options:
121121
params['class'] = self.options['class_name']
122-
123122
if callable(self.sort_key):
124123
params['data_order'] = self.sort_key(data)
125124
return f'<td {html_params(**params)}>{self.format_data(data)}</td>'

src/riskmatrix/layouts/layout.pt

+4-1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@
1616
<script type="text/javascript" src="${layout.static_url('riskmatrix:static/js/bundle.min.js')}"></script>
1717
<script type="text/javascript" src="${layout.static_url('riskmatrix:static/js/sentry.js')}"></script>
1818
</tal:block>
19+
<script type="text/javascript" src="${layout.static_url('riskmatrix:static/js/plotly.min.js')}"></script>
20+
<script type="text/javascript" src="${layout.static_url('riskmatrix:static/js/marked.min.js')}"></script>
21+
1922
<title>RiskMatrix<tal:b tal:condition="exists:title"> — ${title}</tal:b></title>
2023

2124
</head>
@@ -58,7 +61,7 @@
5861
</h2>
5962
<!-- Text -->
6063
<p class="text-white-60">
61-
We need to come up <br>with a slogan
64+
Lean Risk Management
6265
</p>
6366

6467
</div>

src/riskmatrix/layouts/navbar.py

+7-6
Original file line numberDiff line numberDiff line change
@@ -63,15 +63,11 @@ def __html__(self) -> str:
6363
def navbar(context: object, request: 'IRequest') -> 'RenderData':
6464
return {
6565
'entries': [
66-
NavbarEntry(
67-
request,
68-
_('Organization'),
69-
request.route_url('organization')
70-
),
7166
NavbarEntry(
7267
request,
7368
_('Risk Catalog'),
74-
request.route_url('risk_catalog')
69+
request.route_url('risk_catalog'),
70+
lambda request, url: request.path_url.startswith(request.route_url('risk_catalog'))
7571
),
7672
NavbarEntry(
7773
request,
@@ -84,5 +80,10 @@ def navbar(context: object, request: 'IRequest') -> 'RenderData':
8480
request.route_url('assessment'),
8581
lambda request, url: request.show_steps,
8682
),
83+
NavbarEntry(
84+
request,
85+
_('Organization'),
86+
request.route_url('organization')
87+
),
8788
]
8889
}

src/riskmatrix/mail/__init__.py

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
from .exceptions import InactiveRecipient
2+
from .exceptions import MailConnectionError
3+
from .exceptions import MailError
4+
from .interfaces import IMailer
5+
from .mailer import PostmarkMailer
6+
from .types import MailState
7+
8+
__all__ = (
9+
'IMailer',
10+
'InactiveRecipient',
11+
'MailConnectionError',
12+
'MailError',
13+
'MailState',
14+
'PostmarkMailer',
15+
)

src/riskmatrix/mail/exceptions.py

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
class MailError(Exception):
2+
pass
3+
4+
5+
class MailConnectionError(MailError, ConnectionError):
6+
pass
7+
8+
9+
class InactiveRecipient(MailError):
10+
pass

src/riskmatrix/mail/interfaces.py

+91
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
from typing import Any, TYPE_CHECKING
2+
from zope.interface import Interface
3+
4+
if TYPE_CHECKING:
5+
from collections.abc import Sequence
6+
from email.headerregistry import Address
7+
from .types import MailState
8+
from .types import MailParams
9+
from .types import TemplateMailParams
10+
from ..certificate.interfaces import ITemplate
11+
from ..models import Organization
12+
from ..types import JSONObject
13+
MailID = Any
14+
15+
16+
class IMailer(Interface): # pragma: no cover
17+
18+
# NOTE: We would like to say that kwargs is OptionalMailParams
19+
# however there is no way in mypy to express that yet.
20+
def send(sender: 'Address | None',
21+
receivers: 'Address | Sequence[Address]',
22+
subject: str,
23+
content: str,
24+
**kwargs: Any) -> 'MailID':
25+
"""
26+
Send a single email.
27+
28+
Returns a message uuid.
29+
"""
30+
pass
31+
32+
def bulk_send(mails: list['MailParams']
33+
) -> list['MailID | MailState']:
34+
"""
35+
Send multiple emails. "mails" is a list of dicts containing
36+
the arguments to an individual send call.
37+
38+
Returns a list of message uuids and their success/failure states
39+
in the same order as the sending list.
40+
"""
41+
pass
42+
43+
# NOTE: We would like to say that kwargs is OptionalTemplateMailParams
44+
# however there is no way in mypy to express that yet.
45+
def send_template(sender: 'Address | None',
46+
receivers: 'Address | Sequence[Address]',
47+
template: str,
48+
data: 'JSONObject',
49+
**kwargs: Any) -> 'MailID':
50+
"""
51+
Send a single email using a template using its id/name.
52+
"data" contains the template specific data.
53+
54+
Returns a message uuid.
55+
"""
56+
pass
57+
58+
def bulk_send_template(mails: list['TemplateMailParams'],
59+
default_template: str | None = None,
60+
) -> list['MailID | MailState']:
61+
"""
62+
Send multiple template emails using the same template.
63+
64+
Returns a list of message uuids. If a message failed to be sent
65+
the uuid will be replaced by a MailState value.
66+
"""
67+
pass
68+
69+
def template_exists(alias: str) -> bool:
70+
"""
71+
Returns whether a template by the given alias exists.
72+
"""
73+
pass
74+
75+
def create_or_update_template(
76+
template: 'ITemplate',
77+
organization: 'Organization | None' = None,
78+
) -> list[str]:
79+
"""
80+
Creates or updates a mailer template based on a certificate template.
81+
82+
Returns a list of errors. If the list is empty, it was successful.
83+
"""
84+
pass
85+
86+
def delete_template(template: 'ITemplate') -> list[str]:
87+
"""
88+
Deletes a mailer template based on a certificate template.
89+
90+
Returns a list of errors. If the list is empty, it was successful.
91+
"""

0 commit comments

Comments
 (0)