safeuploads 1.0.0__tar.gz → 1.0.1__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.
- safeuploads-1.0.1/.forgejo/scripts/check_conventional_commits.py +163 -0
- safeuploads-1.0.1/.forgejo/workflows/conventional-commits.yml +44 -0
- safeuploads-1.0.1/.forgejo/workflows/docs.yml +51 -0
- safeuploads-1.0.1/.forgejo/workflows/fuzz.yml +44 -0
- safeuploads-1.0.1/.forgejo/workflows/lint-and-test.yml +86 -0
- safeuploads-1.0.1/.forgejo/workflows/publish.yml +63 -0
- safeuploads-1.0.1/.forgejo/workflows/renovate.yml +37 -0
- {safeuploads-1.0.0 → safeuploads-1.0.1}/.github/copilot-instructions.md +21 -0
- {safeuploads-1.0.0 → safeuploads-1.0.1}/.github/workflows/lint&test.yml +1 -1
- safeuploads-1.0.1/CONTRIBUTING.md +82 -0
- {safeuploads-1.0.0 → safeuploads-1.0.1}/PKG-INFO +10 -12
- {safeuploads-1.0.0 → safeuploads-1.0.1}/README.md +9 -11
- {safeuploads-1.0.0 → safeuploads-1.0.1}/docs/index.md +10 -13
- {safeuploads-1.0.0 → safeuploads-1.0.1}/docs/rate-limiting.md +1 -1
- {safeuploads-1.0.0 → safeuploads-1.0.1}/examples/fastapi_example.py +2 -2
- {safeuploads-1.0.0 → safeuploads-1.0.1}/mkdocs.yml +1 -1
- {safeuploads-1.0.0 → safeuploads-1.0.1}/pyproject.toml +1 -1
- safeuploads-1.0.1/renovate.json +35 -0
- {safeuploads-1.0.0 → safeuploads-1.0.1}/safeuploads/file_validator.py +3 -1
- safeuploads-1.0.1/uv.lock +422 -0
- safeuploads-1.0.0/.github/CODEOWNERS +0 -15
- safeuploads-1.0.0/.github/FUNDING.yml +0 -13
- safeuploads-1.0.0/.github/dependabot.yml +0 -20
- safeuploads-1.0.0/.github/workflows/docs.yml +0 -49
- safeuploads-1.0.0/CONTRIBUTING.md +0 -47
- safeuploads-1.0.0/uv.lock +0 -419
- {safeuploads-1.0.0 → safeuploads-1.0.1}/.github/agents/se-gitops-ci-specialist.agent.md +0 -0
- {safeuploads-1.0.0 → safeuploads-1.0.1}/.github/agents/se-security-reviewer.agent.md +0 -0
- {safeuploads-1.0.0 → safeuploads-1.0.1}/.github/agents/test-implementor.agent.md +0 -0
- {safeuploads-1.0.0 → safeuploads-1.0.1}/.github/agents/test-reviewer.agent.md +0 -0
- {safeuploads-1.0.0 → safeuploads-1.0.1}/.github/instructions/agents.instructions.md +0 -0
- {safeuploads-1.0.0 → safeuploads-1.0.1}/.github/instructions/github-actions-ci-cd-best-practices.instructions.md +0 -0
- {safeuploads-1.0.0 → safeuploads-1.0.1}/.github/instructions/instructions.instructions.md +0 -0
- {safeuploads-1.0.0 → safeuploads-1.0.1}/.github/instructions/prompt.instructions.md +0 -0
- {safeuploads-1.0.0 → safeuploads-1.0.1}/.github/instructions/python.instructions.md +0 -0
- {safeuploads-1.0.0 → safeuploads-1.0.1}/.github/instructions/security-and-owasp.instructions.md +0 -0
- {safeuploads-1.0.0 → safeuploads-1.0.1}/.github/workflows/fuzz.yml +0 -0
- {safeuploads-1.0.0 → safeuploads-1.0.1}/.github/workflows/publish.yml +0 -0
- {safeuploads-1.0.0 → safeuploads-1.0.1}/.gitignore +0 -0
- {safeuploads-1.0.0 → safeuploads-1.0.1}/CODE_OF_CONDUCT.md +0 -0
- {safeuploads-1.0.0 → safeuploads-1.0.1}/LICENSE.md +0 -0
- {safeuploads-1.0.0 → safeuploads-1.0.1}/SECURITY.md +0 -0
- {safeuploads-1.0.0 → safeuploads-1.0.1}/docs/api.md +0 -0
- {safeuploads-1.0.0 → safeuploads-1.0.1}/docs/security/architecture.md +0 -0
- {safeuploads-1.0.0 → safeuploads-1.0.1}/docs/security/integration-checklist.md +0 -0
- {safeuploads-1.0.0 → safeuploads-1.0.1}/docs/security/threat-model.md +0 -0
- {safeuploads-1.0.0 → safeuploads-1.0.1}/examples/README.md +0 -0
- {safeuploads-1.0.0 → safeuploads-1.0.1}/safeuploads/__init__.py +0 -0
- {safeuploads-1.0.0 → safeuploads-1.0.1}/safeuploads/audit.py +0 -0
- {safeuploads-1.0.0 → safeuploads-1.0.1}/safeuploads/config.py +0 -0
- {safeuploads-1.0.0 → safeuploads-1.0.1}/safeuploads/enums.py +0 -0
- {safeuploads-1.0.0 → safeuploads-1.0.1}/safeuploads/exceptions.py +0 -0
- {safeuploads-1.0.0 → safeuploads-1.0.1}/safeuploads/inspectors/__init__.py +0 -0
- {safeuploads-1.0.0 → safeuploads-1.0.1}/safeuploads/inspectors/content_inspector.py +0 -0
- {safeuploads-1.0.0 → safeuploads-1.0.1}/safeuploads/inspectors/gzip_inspector.py +0 -0
- {safeuploads-1.0.0 → safeuploads-1.0.1}/safeuploads/inspectors/zip_inspector.py +0 -0
- {safeuploads-1.0.0 → safeuploads-1.0.1}/safeuploads/protocols.py +0 -0
- {safeuploads-1.0.0 → safeuploads-1.0.1}/safeuploads/utils.py +0 -0
- {safeuploads-1.0.0 → safeuploads-1.0.1}/safeuploads/validators/__init__.py +0 -0
- {safeuploads-1.0.0 → safeuploads-1.0.1}/safeuploads/validators/base.py +0 -0
- {safeuploads-1.0.0 → safeuploads-1.0.1}/safeuploads/validators/compression_validator.py +0 -0
- {safeuploads-1.0.0 → safeuploads-1.0.1}/safeuploads/validators/extension_validator.py +0 -0
- {safeuploads-1.0.0 → safeuploads-1.0.1}/safeuploads/validators/unicode_validator.py +0 -0
- {safeuploads-1.0.0 → safeuploads-1.0.1}/safeuploads/validators/windows_validator.py +0 -0
- {safeuploads-1.0.0 → safeuploads-1.0.1}/safeuploads/validators/xml_validator.py +0 -0
- {safeuploads-1.0.0 → safeuploads-1.0.1}/tests/__init__.py +0 -0
- {safeuploads-1.0.0 → safeuploads-1.0.1}/tests/conftest.py +0 -0
- {safeuploads-1.0.0 → safeuploads-1.0.1}/tests/fuzz/__init__.py +0 -0
- {safeuploads-1.0.0 → safeuploads-1.0.1}/tests/fuzz/test_fuzz_config.py +0 -0
- {safeuploads-1.0.0 → safeuploads-1.0.1}/tests/fuzz/test_fuzz_filenames.py +0 -0
- {safeuploads-1.0.0 → safeuploads-1.0.1}/tests/fuzz/test_fuzz_images.py +0 -0
- {safeuploads-1.0.0 → safeuploads-1.0.1}/tests/fuzz/test_fuzz_zip.py +0 -0
- {safeuploads-1.0.0 → safeuploads-1.0.1}/tests/inspectors/__init__.py +0 -0
- {safeuploads-1.0.0 → safeuploads-1.0.1}/tests/inspectors/test_content_inspector.py +0 -0
- {safeuploads-1.0.0 → safeuploads-1.0.1}/tests/inspectors/test_gzip_inspector.py +0 -0
- {safeuploads-1.0.0 → safeuploads-1.0.1}/tests/inspectors/test_zip_inspector.py +0 -0
- {safeuploads-1.0.0 → safeuploads-1.0.1}/tests/test_audit.py +0 -0
- {safeuploads-1.0.0 → safeuploads-1.0.1}/tests/test_config.py +0 -0
- {safeuploads-1.0.0 → safeuploads-1.0.1}/tests/test_config_validation.py +0 -0
- {safeuploads-1.0.0 → safeuploads-1.0.1}/tests/test_enums.py +0 -0
- {safeuploads-1.0.0 → safeuploads-1.0.1}/tests/test_exceptions.py +0 -0
- {safeuploads-1.0.0 → safeuploads-1.0.1}/tests/test_file_validator.py +0 -0
- {safeuploads-1.0.0 → safeuploads-1.0.1}/tests/test_performance.py +0 -0
- {safeuploads-1.0.0 → safeuploads-1.0.1}/tests/test_utils.py +0 -0
- {safeuploads-1.0.0 → safeuploads-1.0.1}/tests/test_validate_activity_file.py +0 -0
- {safeuploads-1.0.0 → safeuploads-1.0.1}/tests/test_validate_gzip_file.py +0 -0
- {safeuploads-1.0.0 → safeuploads-1.0.1}/tests/validators/__init__.py +0 -0
- {safeuploads-1.0.0 → safeuploads-1.0.1}/tests/validators/test_compression_validator.py +0 -0
- {safeuploads-1.0.0 → safeuploads-1.0.1}/tests/validators/test_extension_validator.py +0 -0
- {safeuploads-1.0.0 → safeuploads-1.0.1}/tests/validators/test_unicode_validator.py +0 -0
- {safeuploads-1.0.0 → safeuploads-1.0.1}/tests/validators/test_windows_validator.py +0 -0
- {safeuploads-1.0.0 → safeuploads-1.0.1}/tests/validators/test_xml_validator.py +0 -0
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Validate that messages follow the Conventional Commits 1.0.0 specification.
|
|
3
|
+
|
|
4
|
+
Reads one or more messages (either from CLI arguments or, when ``--stdin`` is
|
|
5
|
+
passed, one message per line from standard input) and exits non-zero if any of
|
|
6
|
+
them is invalid.
|
|
7
|
+
|
|
8
|
+
Spec reference: https://www.conventionalcommits.org/en/v1.0.0/
|
|
9
|
+
|
|
10
|
+
Usage::
|
|
11
|
+
|
|
12
|
+
check_conventional_commits.py "feat(api): add endpoint"
|
|
13
|
+
git log --format=%s base..head | check_conventional_commits.py --stdin
|
|
14
|
+
|
|
15
|
+
Designed to run with the Python interpreter already available on the Forgejo
|
|
16
|
+
runner image. Uses only the standard library so there is no third-party
|
|
17
|
+
supply-chain surface.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import argparse
|
|
23
|
+
import re
|
|
24
|
+
import sys
|
|
25
|
+
from collections.abc import Iterable
|
|
26
|
+
|
|
27
|
+
# Allowed types. This whitelist is a *project policy* layered on top of the
|
|
28
|
+
# Conventional Commits 1.0.0 spec (the spec recommends but does not mandate a
|
|
29
|
+
# fixed set of types). Keep this list in sync with the project's commit
|
|
30
|
+
# conventions documented in ``.github/copilot-instructions.md``.
|
|
31
|
+
#
|
|
32
|
+
# Per the spec, types are matched case-insensitively.
|
|
33
|
+
ALLOWED_TYPES: tuple[str, ...] = (
|
|
34
|
+
"build",
|
|
35
|
+
"chore",
|
|
36
|
+
"ci",
|
|
37
|
+
"docs",
|
|
38
|
+
"feat",
|
|
39
|
+
"fix",
|
|
40
|
+
"perf",
|
|
41
|
+
"refactor",
|
|
42
|
+
"revert",
|
|
43
|
+
"style",
|
|
44
|
+
"test",
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
# Subject pattern: ``type(scope)!: description``. Scope and ``!`` are optional.
|
|
48
|
+
# Per CC 1.0.0:
|
|
49
|
+
# - the type is a noun (matched case-insensitively against ALLOWED_TYPES);
|
|
50
|
+
# - the scope is any noun describing a section of the codebase, enclosed in
|
|
51
|
+
# parentheses, so we accept any non-empty string that is not a closing
|
|
52
|
+
# parenthesis;
|
|
53
|
+
# - the description is free-form text.
|
|
54
|
+
_HEADER_RE = re.compile(
|
|
55
|
+
r"""
|
|
56
|
+
^
|
|
57
|
+
(?P<type>[A-Za-z]+)
|
|
58
|
+
(?:\((?P<scope>[^)]+)\))?
|
|
59
|
+
(?P<breaking>!)?
|
|
60
|
+
:\
|
|
61
|
+
(?P<description>.+?)
|
|
62
|
+
\s*$
|
|
63
|
+
""",
|
|
64
|
+
re.VERBOSE,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _validate_message(message: str) -> list[str]:
|
|
69
|
+
"""Return a list of validation errors for ``message`` (empty if valid)."""
|
|
70
|
+
errors: list[str] = []
|
|
71
|
+
|
|
72
|
+
# Conventional Commits only constrains the first line (the header).
|
|
73
|
+
header = message.splitlines()[0] if message else ""
|
|
74
|
+
|
|
75
|
+
if not header.strip():
|
|
76
|
+
return ["message is empty"]
|
|
77
|
+
|
|
78
|
+
match = _HEADER_RE.match(header)
|
|
79
|
+
if match is None:
|
|
80
|
+
errors.append(
|
|
81
|
+
"header does not match '<type>(<scope>)!: <description>' "
|
|
82
|
+
"(see https://www.conventionalcommits.org/en/v1.0.0/)"
|
|
83
|
+
)
|
|
84
|
+
return errors
|
|
85
|
+
|
|
86
|
+
commit_type = match.group("type")
|
|
87
|
+
|
|
88
|
+
# Project policy: restrict to a known set of types. Types are matched
|
|
89
|
+
# case-insensitively per the Conventional Commits spec.
|
|
90
|
+
if commit_type.lower() not in ALLOWED_TYPES:
|
|
91
|
+
errors.append(
|
|
92
|
+
f"type '{commit_type}' is not allowed; "
|
|
93
|
+
f"use one of: {', '.join(ALLOWED_TYPES)}"
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
return errors
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _safe_log_value(value: str) -> str:
|
|
100
|
+
"""Return ``value`` escaped for safe CI log output."""
|
|
101
|
+
return value.encode("unicode_escape").decode("ascii")
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _iter_messages(args: argparse.Namespace) -> Iterable[str]:
|
|
105
|
+
if args.stdin:
|
|
106
|
+
for line in sys.stdin:
|
|
107
|
+
stripped = line.rstrip("\n")
|
|
108
|
+
if stripped:
|
|
109
|
+
yield stripped
|
|
110
|
+
else:
|
|
111
|
+
yield from args.messages
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def main(argv: list[str] | None = None) -> int:
|
|
115
|
+
parser = argparse.ArgumentParser(
|
|
116
|
+
description="Validate Conventional Commits headers.",
|
|
117
|
+
)
|
|
118
|
+
parser.add_argument(
|
|
119
|
+
"messages",
|
|
120
|
+
nargs="*",
|
|
121
|
+
help="One or more commit messages (or PR titles) to validate.",
|
|
122
|
+
)
|
|
123
|
+
parser.add_argument(
|
|
124
|
+
"--stdin",
|
|
125
|
+
action="store_true",
|
|
126
|
+
help="Read messages from standard input, one per line.",
|
|
127
|
+
)
|
|
128
|
+
args = parser.parse_args(argv)
|
|
129
|
+
|
|
130
|
+
if not args.stdin and not args.messages:
|
|
131
|
+
parser.error("provide at least one message or use --stdin")
|
|
132
|
+
|
|
133
|
+
failures = 0
|
|
134
|
+
checked = 0
|
|
135
|
+
for message in _iter_messages(args):
|
|
136
|
+
checked += 1
|
|
137
|
+
errors = _validate_message(message)
|
|
138
|
+
safe_message = _safe_log_value(message)
|
|
139
|
+
if errors:
|
|
140
|
+
failures += 1
|
|
141
|
+
print(f"INVALID: {safe_message}", file=sys.stderr)
|
|
142
|
+
for err in errors:
|
|
143
|
+
print(f" - {err}", file=sys.stderr)
|
|
144
|
+
else:
|
|
145
|
+
print(f"OK: {safe_message}")
|
|
146
|
+
|
|
147
|
+
if checked == 0:
|
|
148
|
+
print("No messages to validate.", file=sys.stderr)
|
|
149
|
+
return 1
|
|
150
|
+
|
|
151
|
+
if failures:
|
|
152
|
+
print(
|
|
153
|
+
f"\n{failures} of {checked} message(s) failed Conventional Commits "
|
|
154
|
+
"validation.",
|
|
155
|
+
file=sys.stderr,
|
|
156
|
+
)
|
|
157
|
+
return 1
|
|
158
|
+
|
|
159
|
+
return 0
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
if __name__ == "__main__":
|
|
163
|
+
sys.exit(main())
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
name: Conventional Commits
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
types: [opened, edited, synchronize, reopened]
|
|
6
|
+
|
|
7
|
+
permissions:
|
|
8
|
+
contents: read
|
|
9
|
+
pull-requests: read
|
|
10
|
+
|
|
11
|
+
concurrency:
|
|
12
|
+
group: ${{ forgejo.workflow }}-${{ forgejo.event.pull_request.number }}
|
|
13
|
+
cancel-in-progress: true
|
|
14
|
+
|
|
15
|
+
jobs:
|
|
16
|
+
validate:
|
|
17
|
+
runs-on: codeberg-small
|
|
18
|
+
|
|
19
|
+
steps:
|
|
20
|
+
- name: Checkout code
|
|
21
|
+
uses: https://data.forgejo.org/actions/checkout@v6
|
|
22
|
+
with:
|
|
23
|
+
fetch-depth: 0
|
|
24
|
+
|
|
25
|
+
- name: Validate PR title
|
|
26
|
+
# The PR title is passed through an environment variable to prevent
|
|
27
|
+
# shell injection via crafted titles (see security-and-owasp guidelines).
|
|
28
|
+
env:
|
|
29
|
+
PR_TITLE: ${{ forgejo.event.pull_request.title }}
|
|
30
|
+
run: |
|
|
31
|
+
python3 .forgejo/scripts/check_conventional_commits.py "$PR_TITLE"
|
|
32
|
+
|
|
33
|
+
- name: Validate commits in PR
|
|
34
|
+
# Validates every commit subject between the PR base and head. Use the
|
|
35
|
+
# base/head SHAs from the event payload so we don't depend on the local
|
|
36
|
+
# checkout ref layout.
|
|
37
|
+
env:
|
|
38
|
+
BASE_SHA: ${{ forgejo.event.pull_request.base.sha }}
|
|
39
|
+
HEAD_SHA: ${{ forgejo.event.pull_request.head.sha }}
|
|
40
|
+
run: |
|
|
41
|
+
set -euo pipefail
|
|
42
|
+
git fetch --no-tags origin "$BASE_SHA" "$HEAD_SHA"
|
|
43
|
+
git log --format=%s "$BASE_SHA".."$HEAD_SHA" \
|
|
44
|
+
| python3 .forgejo/scripts/check_conventional_commits.py --stdin
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
name: Deploy documentation
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_dispatch:
|
|
5
|
+
push:
|
|
6
|
+
branches:
|
|
7
|
+
- main
|
|
8
|
+
- master
|
|
9
|
+
paths:
|
|
10
|
+
- 'docs/**'
|
|
11
|
+
- 'mkdocs.yml'
|
|
12
|
+
- 'safeuploads/**/*.py'
|
|
13
|
+
- '.forgejo/workflows/docs.yml'
|
|
14
|
+
|
|
15
|
+
permissions:
|
|
16
|
+
contents: write
|
|
17
|
+
|
|
18
|
+
concurrency:
|
|
19
|
+
group: ${{ forgejo.workflow }}-${{ forgejo.ref }}
|
|
20
|
+
cancel-in-progress: true
|
|
21
|
+
|
|
22
|
+
jobs:
|
|
23
|
+
|
|
24
|
+
deploy:
|
|
25
|
+
|
|
26
|
+
runs-on: codeberg-small
|
|
27
|
+
|
|
28
|
+
steps:
|
|
29
|
+
- name: Checkout code
|
|
30
|
+
uses: https://data.forgejo.org/actions/checkout@v4
|
|
31
|
+
with:
|
|
32
|
+
fetch-depth: 0
|
|
33
|
+
|
|
34
|
+
- name: Set up Python
|
|
35
|
+
uses: https://data.forgejo.org/actions/setup-python@v5
|
|
36
|
+
with:
|
|
37
|
+
python-version: '3.13'
|
|
38
|
+
cache: pip
|
|
39
|
+
|
|
40
|
+
- name: Install documentation dependencies
|
|
41
|
+
run: pip install mkdocs-material mkdocstrings[python]
|
|
42
|
+
|
|
43
|
+
- name: Build documentation
|
|
44
|
+
run: mkdocs build --site-dir site
|
|
45
|
+
|
|
46
|
+
- name: Deploy documentation to Codeberg Pages
|
|
47
|
+
uses: https://codeberg.org/git-pages/action@v2
|
|
48
|
+
with:
|
|
49
|
+
site: https://${{ forge.repository_owner }}.codeberg.page/${{ forge.event.repository.name }}/
|
|
50
|
+
token: ${{ forge.token }}
|
|
51
|
+
source: site/
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
name: Scheduled Fuzz
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
schedule:
|
|
5
|
+
# Every Sunday at 03:00 UTC
|
|
6
|
+
- cron: "0 3 * * 0"
|
|
7
|
+
workflow_dispatch:
|
|
8
|
+
|
|
9
|
+
# Cancel in-progress runs for the same branch to avoid wasted
|
|
10
|
+
# runner minutes.
|
|
11
|
+
concurrency:
|
|
12
|
+
group: ${{ forge.workflow }}-${{ forge.ref }}
|
|
13
|
+
cancel-in-progress: true
|
|
14
|
+
|
|
15
|
+
jobs:
|
|
16
|
+
fuzz:
|
|
17
|
+
name: Fuzz
|
|
18
|
+
runs-on: codeberg-medium-lazy
|
|
19
|
+
|
|
20
|
+
steps:
|
|
21
|
+
- name: Checkout repository
|
|
22
|
+
uses: https://data.forgejo.org/actions/checkout@v4
|
|
23
|
+
|
|
24
|
+
- name: Install system dependencies
|
|
25
|
+
run: sudo apt-get update && sudo apt-get install -y libmagic1
|
|
26
|
+
|
|
27
|
+
- name: Set up Python 3.13
|
|
28
|
+
uses: https://data.forgejo.org/actions/setup-python@v5
|
|
29
|
+
with:
|
|
30
|
+
python-version: "3.13"
|
|
31
|
+
|
|
32
|
+
- name: Install uv
|
|
33
|
+
run: pip install uv
|
|
34
|
+
|
|
35
|
+
- name: Install dependencies
|
|
36
|
+
run: uv sync --frozen --dev
|
|
37
|
+
|
|
38
|
+
- name: Run fuzz tests
|
|
39
|
+
run: >
|
|
40
|
+
uv run pytest -m fuzz
|
|
41
|
+
--hypothesis-seed=0
|
|
42
|
+
-x
|
|
43
|
+
--tb=short
|
|
44
|
+
-q
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
name: Lint & Test
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
# Cancel in-progress runs for the same branch/PR to avoid wasted
|
|
10
|
+
# runner minutes and race conditions on shared resources.
|
|
11
|
+
concurrency:
|
|
12
|
+
group: ${{ forge.workflow }}-${{ forge.ref }}
|
|
13
|
+
cancel-in-progress: true
|
|
14
|
+
|
|
15
|
+
jobs:
|
|
16
|
+
# ----------------------------------------------------------------
|
|
17
|
+
# Lint
|
|
18
|
+
# Runs ruff on all Python sources. Fails fast so test job is only
|
|
19
|
+
# triggered when the code is clean.
|
|
20
|
+
# ----------------------------------------------------------------
|
|
21
|
+
lint:
|
|
22
|
+
name: Lint
|
|
23
|
+
runs-on: codeberg-tiny
|
|
24
|
+
|
|
25
|
+
steps:
|
|
26
|
+
- name: Checkout repository
|
|
27
|
+
uses: https://data.forgejo.org/actions/checkout@v4
|
|
28
|
+
|
|
29
|
+
- name: Set up Python 3.13
|
|
30
|
+
uses: https://data.forgejo.org/actions/setup-python@v5
|
|
31
|
+
with:
|
|
32
|
+
python-version: "3.13"
|
|
33
|
+
|
|
34
|
+
- name: Install uv
|
|
35
|
+
run: pip install uv
|
|
36
|
+
|
|
37
|
+
- name: Install dependencies
|
|
38
|
+
run: uv sync --frozen --dev
|
|
39
|
+
|
|
40
|
+
- name: Lint with ruff
|
|
41
|
+
run: uv run ruff check safeuploads tests examples
|
|
42
|
+
|
|
43
|
+
- name: Check formatting with ruff
|
|
44
|
+
run: uv run ruff format --check safeuploads tests examples
|
|
45
|
+
|
|
46
|
+
# ----------------------------------------------------------------
|
|
47
|
+
# Test
|
|
48
|
+
# Runs the full pytest suite with coverage. Only runs after lint
|
|
49
|
+
# passes so we don't waste runner minutes on broken code.
|
|
50
|
+
# ----------------------------------------------------------------
|
|
51
|
+
test:
|
|
52
|
+
name: Test
|
|
53
|
+
runs-on: codeberg-medium
|
|
54
|
+
needs: lint
|
|
55
|
+
|
|
56
|
+
steps:
|
|
57
|
+
- name: Checkout repository
|
|
58
|
+
uses: https://data.forgejo.org/actions/checkout@v4
|
|
59
|
+
|
|
60
|
+
- name: Install system dependencies
|
|
61
|
+
run: sudo apt-get update && sudo apt-get install -y libmagic1
|
|
62
|
+
|
|
63
|
+
- name: Set up Python 3.13
|
|
64
|
+
uses: https://data.forgejo.org/actions/setup-python@v5
|
|
65
|
+
with:
|
|
66
|
+
python-version: "3.13"
|
|
67
|
+
|
|
68
|
+
- name: Install uv
|
|
69
|
+
run: pip install uv
|
|
70
|
+
|
|
71
|
+
- name: Install dependencies
|
|
72
|
+
run: uv sync --frozen --dev
|
|
73
|
+
|
|
74
|
+
- name: Audit Python dependencies
|
|
75
|
+
run: uvx pip-audit --require-hashes --disable-pip -r <(uv export --frozen --no-header --no-emit-project --no-dev)
|
|
76
|
+
|
|
77
|
+
- name: Run tests with coverage
|
|
78
|
+
run: uv run pytest --cov-fail-under=90
|
|
79
|
+
|
|
80
|
+
- name: Upload coverage report
|
|
81
|
+
uses: https://data.forgejo.org/forgejo/upload-artifact@v4
|
|
82
|
+
if: always()
|
|
83
|
+
with:
|
|
84
|
+
name: safeuploads-coverage
|
|
85
|
+
path: .coverage
|
|
86
|
+
retention-days: 7
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_dispatch:
|
|
5
|
+
inputs:
|
|
6
|
+
confirm:
|
|
7
|
+
description: "Type the package version to confirm publish"
|
|
8
|
+
required: true
|
|
9
|
+
type: string
|
|
10
|
+
|
|
11
|
+
# Only one publish at a time.
|
|
12
|
+
concurrency:
|
|
13
|
+
group: publish
|
|
14
|
+
cancel-in-progress: false
|
|
15
|
+
|
|
16
|
+
jobs:
|
|
17
|
+
publish:
|
|
18
|
+
name: Build & Publish
|
|
19
|
+
runs-on: codeberg-medium
|
|
20
|
+
|
|
21
|
+
steps:
|
|
22
|
+
- name: Checkout repository
|
|
23
|
+
uses: https://data.forgejo.org/actions/checkout@v4
|
|
24
|
+
|
|
25
|
+
- name: Verify version matches input
|
|
26
|
+
run: |
|
|
27
|
+
PKG_VERSION=$(python3 -c "
|
|
28
|
+
import tomllib, pathlib
|
|
29
|
+
data = tomllib.loads(
|
|
30
|
+
pathlib.Path('pyproject.toml').read_text()
|
|
31
|
+
)
|
|
32
|
+
print(data['project']['version'])
|
|
33
|
+
")
|
|
34
|
+
if [ "$PKG_VERSION" != "${{ inputs.confirm }}" ]; then
|
|
35
|
+
echo "::error::Input version ${{ inputs.confirm }} does not match pyproject.toml version $PKG_VERSION"
|
|
36
|
+
exit 1
|
|
37
|
+
fi
|
|
38
|
+
echo "Publishing version $PKG_VERSION"
|
|
39
|
+
|
|
40
|
+
- name: Install system dependencies
|
|
41
|
+
run: sudo apt-get update && sudo apt-get install -y libmagic1
|
|
42
|
+
|
|
43
|
+
- name: Set up Python 3.13
|
|
44
|
+
uses: https://data.forgejo.org/actions/setup-python@v5
|
|
45
|
+
with:
|
|
46
|
+
python-version: "3.13"
|
|
47
|
+
|
|
48
|
+
- name: Install uv
|
|
49
|
+
run: pip install uv
|
|
50
|
+
|
|
51
|
+
- name: Install dependencies
|
|
52
|
+
run: uv sync --frozen --dev
|
|
53
|
+
|
|
54
|
+
- name: Run tests before publish
|
|
55
|
+
run: uv run pytest --cov-fail-under=90
|
|
56
|
+
|
|
57
|
+
- name: Build package
|
|
58
|
+
run: uv build
|
|
59
|
+
|
|
60
|
+
- name: Publish to PyPI
|
|
61
|
+
env:
|
|
62
|
+
UV_PUBLISH_TOKEN: ${{ secrets.PYPI_API_TOKEN }}
|
|
63
|
+
run: uv publish
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
name: Dependency updates
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_dispatch:
|
|
5
|
+
schedule:
|
|
6
|
+
- cron: '0 4 * * 0'
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: write
|
|
10
|
+
pull-requests: write
|
|
11
|
+
issues: write
|
|
12
|
+
|
|
13
|
+
concurrency:
|
|
14
|
+
group: ${{ forgejo.workflow }}-${{ forgejo.ref }}
|
|
15
|
+
cancel-in-progress: false
|
|
16
|
+
|
|
17
|
+
jobs:
|
|
18
|
+
|
|
19
|
+
renovate:
|
|
20
|
+
|
|
21
|
+
runs-on: codeberg-small
|
|
22
|
+
|
|
23
|
+
steps:
|
|
24
|
+
- name: Checkout code
|
|
25
|
+
uses: https://data.forgejo.org/actions/checkout@v4
|
|
26
|
+
with:
|
|
27
|
+
fetch-depth: 1
|
|
28
|
+
|
|
29
|
+
- name: Run Renovate
|
|
30
|
+
uses: docker://ghcr.io/renovatebot/renovate:latest
|
|
31
|
+
env:
|
|
32
|
+
LOG_LEVEL: info
|
|
33
|
+
RENOVATE_CONFIG_FILE: renovate.json
|
|
34
|
+
RENOVATE_ENDPOINT: https://codeberg.org/api/v1/
|
|
35
|
+
RENOVATE_PLATFORM: gitea
|
|
36
|
+
RENOVATE_REPOSITORIES: ${{ forgejo.repository }}
|
|
37
|
+
RENOVATE_TOKEN: ${{ secrets.RENOVATE_TOKEN }}
|
|
@@ -70,3 +70,24 @@ class MyClass:
|
|
|
70
70
|
## Documentation & Examples
|
|
71
71
|
- Update docstrings and README examples when public APIs change (e.g., `FileValidator`, utility helpers).
|
|
72
72
|
- Demonstrate usage snippets with async FastAPI context where applicable.
|
|
73
|
+
|
|
74
|
+
## Commits logic
|
|
75
|
+
|
|
76
|
+
Committing should use clear messages following [Conventional Commits](https://www.conventionalcommits.org/) format:
|
|
77
|
+
|
|
78
|
+
**Format:** `<type>(<scope>)!: <description>` — `(<scope>)` and the breaking-change `!` are optional.
|
|
79
|
+
|
|
80
|
+
The following rules are enforced automatically on every PR (against the PR title and every commit subject) by `.forgejo/workflows/conventional-commits.yml`. Validation follows the [Conventional Commits 1.0.0](https://www.conventionalcommits.org/en/v1.0.0/) spec, with a single project-policy addition (the allowed-type whitelist). Generated commit messages must comply:
|
|
81
|
+
|
|
82
|
+
- **Header format:** `<type>(<scope>)!: <description>` — the `(<scope>)` and breaking-change `!` are optional, and a space is required after the colon.
|
|
83
|
+
- **Allowed types (project policy):** `build`, `chore`, `ci`, `docs`, `feat`, `fix`, `perf`, `refactor`, `revert`, `style`, `test`. Types are matched case-insensitively per the spec.
|
|
84
|
+
- **Scope** (optional) is any non-empty string (no parentheses), per the spec.
|
|
85
|
+
- **Description** is free-form text, per the spec (no lowercase requirement, no trailing-period restriction, no length limit).
|
|
86
|
+
- **Breaking changes** are marked with `!` after the type/scope (e.g. `feat(api)!: ...`) or a `BREAKING CHANGE:` footer in the commit body.
|
|
87
|
+
|
|
88
|
+
Examples:
|
|
89
|
+
- `feat: add GPX max speed parsing`
|
|
90
|
+
- `fix(garmin): handle multi-segment GPX distance correctly`
|
|
91
|
+
- `docs: update development instructions`
|
|
92
|
+
- `test(activities): add regression test for GPX segment handling`
|
|
93
|
+
- `refactor(api)!: rename Activity.distance to total_distance`
|
|
@@ -82,7 +82,7 @@ jobs:
|
|
|
82
82
|
run: uv sync --frozen --dev
|
|
83
83
|
|
|
84
84
|
- name: Audit Python dependencies
|
|
85
|
-
run: uvx pip-audit --require-hashes --disable-pip -r <(uv export --frozen --no-header --no-emit-project)
|
|
85
|
+
run: uvx pip-audit --require-hashes --disable-pip -r <(uv export --frozen --no-header --no-emit-project --no-dev)
|
|
86
86
|
|
|
87
87
|
- name: Run tests with coverage
|
|
88
88
|
run: uv run pytest --cov-fail-under=90
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# Contributing to Endurain
|
|
2
|
+
|
|
3
|
+
Thank you for considering contributing to Endurain! Before diving in, please read these guidelines carefully. They exist to make the process sustainable for everyone.
|
|
4
|
+
|
|
5
|
+
## A Note on Maintainership
|
|
6
|
+
|
|
7
|
+
Endurain is maintained by a single person in their spare time. This means review bandwidth is genuinely limited. Following these guidelines isn't bureaucracy, it's what allows contributions to actually get merged rather than sitting in a queue indefinitely.
|
|
8
|
+
|
|
9
|
+
## Before You Write Any Code
|
|
10
|
+
|
|
11
|
+
**Open an issue first.** For anything beyond a small bug fix, typo, or documentation improvement, please open an issue and wait for a response before writing code. This takes minutes and can save you hours of work on something that won't be merged because it conflicts with planned direction, existing work, or project scope.
|
|
12
|
+
|
|
13
|
+
If an issue already exists, comment on it to signal your intent so work isn't duplicated.
|
|
14
|
+
|
|
15
|
+
## Pull Request Size — The Most Important Rule
|
|
16
|
+
|
|
17
|
+
**Keep PRs small and focused on a single concern.**
|
|
18
|
+
|
|
19
|
+
As a rule of thumb:
|
|
20
|
+
|
|
21
|
+
- **Target under 300 lines changed** (excluding lock files, generated code, and migrations)
|
|
22
|
+
- **One PR = one thing.** Don't bundle a bug fix with a refactor with a new feature
|
|
23
|
+
- If your change is naturally large (e.g. a new integration), break it into a chain of smaller PRs that can each be reviewed and merged independently
|
|
24
|
+
|
|
25
|
+
PRs that are too large to review efficiently will be asked to be split before they receive a review. This isn't a judgment on the quality of the work, it's a maintainability constraint.
|
|
26
|
+
|
|
27
|
+
**Excluded from the line count:** `uv.lock`, migration files, and other generated or vendored files.
|
|
28
|
+
|
|
29
|
+
## How to Contribute
|
|
30
|
+
|
|
31
|
+
### Bug Fixes
|
|
32
|
+
|
|
33
|
+
- Check if an issue already exists before opening a new one
|
|
34
|
+
- Include clear steps to reproduce in the issue
|
|
35
|
+
- Small, targeted fixes are the fastest path to a merge
|
|
36
|
+
|
|
37
|
+
### Documentation
|
|
38
|
+
|
|
39
|
+
- Always welcome and rarely requires prior discussion
|
|
40
|
+
- Improvements to the docs site, inline code comments, and the README all count
|
|
41
|
+
- Keep the same tone and structure as existing docs
|
|
42
|
+
|
|
43
|
+
### New Features
|
|
44
|
+
|
|
45
|
+
- **Always discuss in an issue first** — this is required, not optional
|
|
46
|
+
- Features that haven't been discussed and approved in an issue may be closed without review, regardless of quality
|
|
47
|
+
|
|
48
|
+
### Refactors and Code Quality
|
|
49
|
+
|
|
50
|
+
- Must be discussed in an issue first
|
|
51
|
+
- Pure refactor PRs (no behaviour change) are easiest to review. Keep them separate from feature or fix PRs
|
|
52
|
+
- Include a clear explanation of what improved and why
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
## Getting Started
|
|
56
|
+
|
|
57
|
+
1. **Fork the repository** on Codeberg
|
|
58
|
+
2. **Clone your fork** locally:
|
|
59
|
+
```bash
|
|
60
|
+
git clone https://codeberg.org/YOUR_USERNAME/endurain.git
|
|
61
|
+
```
|
|
62
|
+
3. **Create a branch** and use a descriptive name:
|
|
63
|
+
```bash
|
|
64
|
+
git checkout -b fix/garmin-sync-timeout
|
|
65
|
+
# or
|
|
66
|
+
git checkout -b feat/gpx-export
|
|
67
|
+
```
|
|
68
|
+
4. **Make your changes**, committing with clear messages following [Conventional Commits](https://www.conventionalcommits.org/):
|
|
69
|
+
```
|
|
70
|
+
fix: handle timeout on Garmin sync
|
|
71
|
+
feat: add GPX export for activities
|
|
72
|
+
docs: clarify Docker environment variables
|
|
73
|
+
```
|
|
74
|
+
5. **Push and open a PR** against the `master` branch, filling in the PR template completely
|
|
75
|
+
|
|
76
|
+
## Response Time Expectations
|
|
77
|
+
|
|
78
|
+
Reviews may take days to weeks depending on availability. A PR sitting without a response is not a rejection. Please feel free to leave a polite ping after two weeks if there's been no activity.
|
|
79
|
+
|
|
80
|
+
## Thank You
|
|
81
|
+
|
|
82
|
+
Even small contributions make a real difference. Thank you for taking the time to improve Endurain for everyone who self-hosts it.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: safeuploads
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.1
|
|
4
4
|
Summary: A comprehensive file security system for validating uploads and preventing attacks
|
|
5
5
|
Author-email: João Vitória Silva <joao@endurain.com>
|
|
6
6
|
License: MIT
|
|
@@ -14,9 +14,13 @@ Description-Content-Type: text/markdown
|
|
|
14
14
|
|
|
15
15
|
# safeuploads
|
|
16
16
|
|
|
17
|
-
](https://codeberg.org/endurain-project/safeuploads/src/branch/main/LICENSE.md)
|
|
18
|
+
[](https://codeberg.org/endurain-project/safeuploads/releases)
|
|
19
|
+
[](https://pypi.org/project/safeuploads/)
|
|
20
|
+
[](https://pypi.org/project/safeuploads/)
|
|
21
|
+
[](https://pypi.org/project/safeuploads/)
|
|
22
|
+
[](https://endurain-project.codeberg.page/safeuploads/)
|
|
23
|
+
[](https://codeberg.org/endurain-project/safeuploads)
|
|
20
24
|
|
|
21
25
|
Secure file upload validation for Python 3.13+ applications. Catches dangerous filenames, malicious extensions, Windows reserved names, and compression-based attacks before you accept an upload.
|
|
22
26
|
|
|
@@ -130,13 +134,7 @@ except FileValidationError as err:
|
|
|
130
134
|
|
|
131
135
|
## Documentation
|
|
132
136
|
|
|
133
|
-
Full documentation is available at the [safeuploads docs site](https://endurain-project.
|
|
134
|
-
|
|
135
|
-
## Sponsors
|
|
136
|
-
|
|
137
|
-
A huge thank you to the project sponsors! Your support helps keep this project going.
|
|
138
|
-
|
|
139
|
-
Consider [sponsoring safeuploads on GitHub](https://github.com/sponsors/endurain-project) to ensure continuous development.
|
|
137
|
+
Full documentation is available at the [safeuploads docs site](https://endurain-project.codeberg.page/safeuploads/).
|
|
140
138
|
|
|
141
139
|
## License
|
|
142
140
|
|
|
@@ -147,5 +145,5 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file
|
|
|
147
145
|
Contributions welcome! See [Contributing Guidelines](CONTRIBUTING.md) for guidelines.
|
|
148
146
|
|
|
149
147
|
<div align="center">
|
|
150
|
-
<sub>Built with ❤️ from Portugal | Part of the <a href="https://
|
|
148
|
+
<sub>Built with ❤️ from Portugal | Part of the <a href="https://codeberg.org/endurain-project">Endurain</a> ecosystem</sub>
|
|
151
149
|
</div>
|