Skip to content

Commit 6bfff61

Browse files
committed
Modernize SSDLC, add Python 3.11-3.15 to CI, and add mypy typing
1 parent be03fea commit 6bfff61

5 files changed

Lines changed: 41 additions & 27 deletions

File tree

.github/workflows/ci.yml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ jobs:
1313
runs-on: ubuntu-latest
1414
strategy:
1515
matrix:
16-
python-version: ['3.8', '3.9', '3.10']
16+
python-version: ['3.10', '3.11', '3.12', '3.13', '3.14', '3.15']
1717

1818
steps:
1919
- uses: actions/checkout@v4
@@ -32,6 +32,8 @@ jobs:
3232
python -m pip install tox tox-gh-actions
3333
- name: Lint
3434
if: matrix.python-version == '3.10'
35-
run: tox -e flake8
35+
run: |
36+
tox -e flake8
37+
tox -e mypy
3638
- name: Tests
3739
run: tox

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
FROM python:3.8-slim
1+
FROM python:3.14-slim-trixie
22

33
ENV APP_DIR=/usr/src/snappass
44

dev-requirements.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,6 @@ pytest-cov==5.0.0
77
tox==4.23.0
88
bumpversion==0.6.0
99
wheel==0.44.0
10+
mypy==1.11.1
11+
types-Flask==1.1.6
12+
types-redis==4.6.0.11

snappass/main.py

Lines changed: 26 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,12 @@
55

66
import redis
77
from flask import (
8-
abort, Flask, render_template, request, jsonify, make_response
8+
abort, Flask, render_template, request, jsonify, make_response, Response, Request
99
)
1010
from redis.exceptions import ConnectionError
1111
from urllib.parse import quote_plus, unquote_plus, urljoin, urlsplit
1212
# _ is required to get the Jinja templates translated
13-
from flask_babel import Babel, _ # noqa: F401
13+
from flask_babel import Babel, _ # type: ignore # noqa: F401
1414

1515
_no_ssl_env = os.environ.get('NO_SSL', 'False').lower()
1616
NO_SSL: bool = _no_ssl_env in ('true', '1', 't', 'y', 'yes')
@@ -40,12 +40,15 @@ def get_locale() -> typing.Optional[str]:
4040
babel = Babel(app, locale_selector=get_locale)
4141

4242
# Initialize Redis
43+
redis_client: redis.StrictRedis
4344
if os.environ.get('MOCK_REDIS'):
4445
from fakeredis import FakeStrictRedis
4546

46-
redis_client = FakeStrictRedis(version=(6, 2), protocol=2)
47+
redis_client = FakeStrictRedis(version=(6, 2), protocol=2) # type: ignore
4748
elif os.environ.get('REDIS_URL'):
48-
redis_client = redis.StrictRedis.from_url(os.environ.get('REDIS_URL'))
49+
redis_url = os.environ.get('REDIS_URL')
50+
assert redis_url is not None
51+
redis_client = redis.StrictRedis.from_url(redis_url)
4952
else:
5053
redis_host = os.environ.get('REDIS_HOST', 'localhost')
5154
redis_port = int(os.environ.get('REDIS_PORT', 6379))
@@ -64,7 +67,7 @@ def get_locale() -> typing.Optional[str]:
6467
MAX_TTL: int = DEFAULT_API_TTL
6568

6669

67-
def _request_has_trusted_host(req: request) -> bool:
70+
def _request_has_trusted_host(req: Request) -> bool:
6871
# When HOST_OVERRIDE is not configured the base URL is derived from the
6972
# request's Host header, which a client can spoof. Only loopback hosts are
7073
# trusted for that fallback (local/dev use); production should set
@@ -94,12 +97,12 @@ def inner(*args: typing.Any, **kwargs: typing.Any) -> typing.Any:
9497

9598

9699
def as_validation_problem(
97-
request: request,
100+
req: Request,
98101
problem_type: str,
99102
problem_title: str,
100103
invalid_params: typing.List[typing.Dict[str, str]]
101-
) -> make_response:
102-
base_url = set_base_url(request)
104+
) -> Response:
105+
base_url = set_base_url(req)
103106

104107
problem = {
105108
"type": f"{base_url}{problem_type}",
@@ -110,12 +113,12 @@ def as_validation_problem(
110113

111114

112115
def as_not_found_problem(
113-
request: request,
116+
req: Request,
114117
problem_type: str,
115118
problem_title: str,
116119
invalid_params: typing.List[typing.Dict[str, str]]
117-
) -> make_response:
118-
base_url = set_base_url(request)
120+
) -> Response:
121+
base_url = set_base_url(req)
119122

120123
problem = {
121124
"type": f"{base_url}{problem_type}",
@@ -128,7 +131,7 @@ def as_not_found_problem(
128131
def as_problem_response(
129132
problem: typing.Dict[str, typing.Any],
130133
status_code: typing.Optional[int] = None
131-
) -> make_response:
134+
) -> Response:
132135
if not isinstance(status_code, int) or not status_code:
133136
status_code = 400
134137

@@ -190,7 +193,7 @@ def clean_input() -> typing.Tuple[int, str]:
190193
return TIME_CONVERSION[time_period], password
191194

192195

193-
def set_base_url(req: request) -> str:
196+
def set_base_url(req: Request) -> str:
194197
scheme = 'http' if NO_SSL else 'https'
195198
if HOST_OVERRIDE:
196199
base_url = f'{scheme}://{HOST_OVERRIDE}/'
@@ -206,12 +209,12 @@ def set_base_url(req: request) -> str:
206209

207210

208211
@app.route('/', methods=['GET'])
209-
def index():
212+
def index() -> str:
210213
return render_template('set_password.html')
211214

212215

213216
@app.route('/', methods=['POST'])
214-
def handle_password():
217+
def handle_password() -> typing.Union[Response, str, typing.Tuple[str, int]]:
215218
password = request.form.get('password')
216219
ttl = request.form.get('ttl')
217220
if password and ttl and not empty(password) and not empty(ttl):
@@ -234,7 +237,7 @@ def handle_password():
234237

235238

236239
@app.route('/api/set_password/', methods=['POST'])
237-
def api_handle_password():
240+
def api_handle_password() -> Response:
238241
password = request.json.get('password')
239242
ttl = int(request.json.get('ttl', DEFAULT_API_TTL))
240243
if password and isinstance(ttl, int) and ttl <= MAX_TTL:
@@ -247,7 +250,7 @@ def api_handle_password():
247250

248251

249252
@app.route('/api/v2/passwords', methods=['POST'])
250-
def api_v2_set_password():
253+
def api_v2_set_password() -> Response:
251254
password = request.json.get('password')
252255
ttl = int(request.json.get('ttl', DEFAULT_API_TTL))
253256

@@ -294,7 +297,7 @@ def api_v2_set_password():
294297

295298

296299
@app.route('/api/v2/passwords/<token>', methods=['HEAD'])
297-
def api_v2_check_password(token: str):
300+
def api_v2_check_password(token: str) -> typing.Tuple[str, int]:
298301
token = unquote_plus(token)
299302
if not password_exists(token):
300303
# Return NotFound, to indicate that password does not exist
@@ -305,7 +308,7 @@ def api_v2_check_password(token: str):
305308

306309

307310
@app.route('/api/v2/passwords/<token>', methods=['GET'])
308-
def api_v2_retrieve_password(token: str):
311+
def api_v2_retrieve_password(token: str) -> Response:
309312
token = unquote_plus(token)
310313
password = get_password(token)
311314
if not password:
@@ -322,7 +325,7 @@ def api_v2_retrieve_password(token: str):
322325

323326

324327
@app.route('/<password_key>', methods=['GET'])
325-
def preview_password(password_key: str):
328+
def preview_password(password_key: str) -> typing.Union[str, typing.Tuple[str, int]]:
326329
password_key = unquote_plus(password_key)
327330
if not password_exists(password_key):
328331
return render_template('expired.html'), 404
@@ -331,7 +334,7 @@ def preview_password(password_key: str):
331334

332335

333336
@app.route('/<password_key>', methods=['POST'])
334-
def show_password(password_key: str):
337+
def show_password(password_key: str) -> typing.Union[str, typing.Tuple[str, int]]:
335338
password_key = unquote_plus(password_key)
336339
password = get_password(password_key)
337340
if not password:
@@ -342,12 +345,12 @@ def show_password(password_key: str):
342345

343346
@app.route('/_/_/health', methods=['GET'])
344347
@check_redis_alive
345-
def health_check():
348+
def health_check() -> typing.Dict[typing.Any, typing.Any]:
346349
return {}
347350

348351

349352
@check_redis_alive
350-
def main():
353+
def main() -> None:
351354
app.run(host=os.environ.get('SNAPPASS_BIND_ADDRESS', '0.0.0.0'),
352355
port=int(os.environ.get('SNAPPASS_PORT', 5000)))
353356

tox.ini

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
[tox]
2-
envlist = py38, py39, py310, flake8
2+
envlist = py310, py311, py312, py313, py314, py315, flake8, mypy
3+
skip_missing_interpreters = true
34

45
[testenv]
56
setenv =
@@ -14,3 +15,8 @@ commands =
1415
commands =
1516
pip install -r dev-requirements.txt
1617
flake8
18+
19+
[testenv:mypy]
20+
commands =
21+
pip install -r dev-requirements.txt
22+
mypy snappass

0 commit comments

Comments
 (0)