Skip to content

Commit 010ab4b

Browse files
authored
Merge pull request #1 from evgenii-d/dev
Initial release
2 parents 13192e1 + 5b62394 commit 010ab4b

File tree

5 files changed

+273
-1
lines changed

5 files changed

+273
-1
lines changed

.github/workflows/build_main.yml

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
name: Build and Release latest version
2+
3+
on:
4+
push:
5+
branches: main
6+
7+
jobs:
8+
check_build_number:
9+
runs-on: ubuntu-latest
10+
outputs:
11+
number: ${{ steps.check_number.outputs.number }}
12+
new_release: ${{ steps.check_number.outputs.new_release }}
13+
14+
steps:
15+
- name: Checkout VERSION
16+
uses: actions/checkout@v3
17+
with:
18+
sparse-checkout: |
19+
VERSION
20+
sparse-checkout-cone-mode: false
21+
22+
- name: Check latest release and current version number
23+
id: check_number
24+
run: |
25+
latest_release_json=$(curl -L \
26+
-H "Accept: application/vnd.github+json" \
27+
-H "Authorization: Bearer ${{ secrets.ACCESS_TOKEN }}" \
28+
-H "X-GitHub-Api-Version: 2022-11-28" \
29+
"https://api.github.com/repos/${GITHUB_REPOSITORY}/releases/latest")
30+
31+
latest_release_tag=$(echo "$latest_release_json" | jq -r .tag_name)
32+
current_version=$(head -1 VERSION)
33+
34+
echo "Latest release version: $latest_release_tag"
35+
echo "Current version: $current_version"
36+
37+
if [ "$latest_release_tag" == "$current_version" ]; then
38+
echo "Release already exists"
39+
echo "new_release=false" >> "$GITHUB_OUTPUT"
40+
else
41+
echo "New release"
42+
echo "number=$(head -1 VERSION)" >> "$GITHUB_OUTPUT"
43+
fi
44+
45+
build_executables:
46+
needs: check_build_number
47+
if: needs.check_build_number.outputs.new_release != 'false'
48+
strategy:
49+
matrix:
50+
os: [ubuntu-latest, windows-latest]
51+
runs-on: ${{ matrix.os }}
52+
env:
53+
PYTHON_VERSION: "3.12"
54+
55+
steps:
56+
- name: Checkout repository
57+
uses: actions/checkout@v3
58+
59+
- name: Set up Python ${{ env.PYTHON_VERSION }}
60+
uses: actions/setup-python@v3
61+
with:
62+
python-version: ${{ env.PYTHON_VERSION }}
63+
64+
- name: Install dependencies
65+
run: |
66+
python -m pip install --upgrade pip
67+
pip install pyinstaller
68+
69+
- name: Create a one-file bundled executable
70+
run: pyinstaller -n app_${{ needs.check_build_number.outputs.number }}_${{ runner.os }} -F src/main.py
71+
72+
- name: Upload executable
73+
uses: actions/upload-artifact@v4
74+
with:
75+
name: ${{ needs.check_build_number.outputs.number }}-${{ runner.os }}
76+
path: dist/
77+
78+
release_build:
79+
needs: [check_build_number, build_executables]
80+
runs-on: ubuntu-latest
81+
steps:
82+
- name: Download artifacts
83+
uses: actions/download-artifact@v4
84+
with:
85+
path: artifacts
86+
87+
- name: Archive artifacts
88+
run: |
89+
find "artifacts" -type f | while read -r file; do
90+
filename=$(basename "$file" .exe)
91+
zip -j "$(dirname "$file")/$filename.zip" "$file"
92+
rm "$file"
93+
done
94+
95+
- name: Create Release
96+
id: current_release
97+
run: |
98+
release_json=$(curl -L -f \
99+
-X POST \
100+
-H "Accept: application/vnd.github+json" \
101+
-H "Authorization: Bearer ${{ secrets.ACCESS_TOKEN }}" \
102+
-H "X-GitHub-Api-Version: 2022-11-28" \
103+
https://api.github.com/repos/${GITHUB_REPOSITORY}/releases \
104+
-d '{"tag_name":"${{ needs.check_build_number.outputs.number }}","name":"${{ needs.check_build_number.outputs.number }}"}')
105+
106+
echo "release_id=$(echo $release_json | jq -r .id)" >> $GITHUB_OUTPUT
107+
108+
- name: Upload a release assets
109+
run: |
110+
find "artifacts" -type f | while read -r file; do
111+
curl -L -f \
112+
-X POST \
113+
-H "Accept: application/vnd.github+json" \
114+
-H "Authorization: Bearer ${{ secrets.ACCESS_TOKEN }}" \
115+
-H "X-GitHub-Api-Version: 2022-11-28" \
116+
-H "Content-Type: application/octet-stream" \
117+
"https://uploads.github.com/repos/${GITHUB_REPOSITORY}/releases/${{ steps.current_release.outputs.release_id }}/assets?name=$(basename "$file")" \
118+
--data-binary "@$file"
119+
echo "Uploaded: $(basename "$file")"
120+
done

.github/workflows/linting.yml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
name: Linting
2+
3+
on:
4+
push:
5+
branches: dev
6+
pull_request:
7+
branches: dev, main
8+
9+
env:
10+
PYTHON_VERSION: "3.12"
11+
12+
jobs:
13+
pylint_check:
14+
runs-on: ubuntu-latest
15+
steps:
16+
- uses: actions/checkout@v3
17+
18+
- name: Set up Python ${{ env.PYTHON_VERSION }}
19+
uses: actions/setup-python@v3
20+
with:
21+
python-version: ${{ env.PYTHON_VERSION }}
22+
23+
- name: Install dependencies
24+
run: |
25+
python -m pip install --upgrade pip
26+
pip install pylint
27+
28+
- name: Analysing the code with pylint
29+
run: |
30+
pylint $(git ls-files 'src/*.py')

README.md

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,23 @@
1-
# basic-python-http-server
1+
# Basic Python HTTP Server (BPHS)
2+
3+
Command-line static HTTP server build upon Python [`HTTPServer`][1] from [standard library][2].
4+
5+
No additional dependencies required.
6+
7+
## Installation
8+
9+
One-file bundled executable can be downloaded from the **Releases** section.
10+
11+
## Usage
12+
13+
```txt
14+
bphs [-h] [-p] [-d] [-l]
15+
16+
-h, --help show help message and exit
17+
-p, --port port to use [8080]
18+
-d, --dir directory to serve [current directory]
19+
-l, --listing enable directory listing
20+
```
21+
22+
[1]: https://github.com/python/cpython/blob/3.12/Lib/http/server.py
23+
[2]: https://docs.python.org/3/library/http.server.html

VERSION

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
1.0.0

src/main.py

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
""" Basic Python HTTP Server """
2+
3+
import os
4+
import logging
5+
import argparse
6+
import itertools
7+
from io import BytesIO
8+
from pathlib import Path
9+
from functools import partial
10+
from socketserver import ThreadingMixIn
11+
from http.server import HTTPServer, SimpleHTTPRequestHandler
12+
13+
14+
logging.basicConfig(level=logging.INFO,
15+
format='%(asctime)s | %(levelname)s | %(message)s')
16+
17+
18+
class CustomHelpFormatter(argparse.HelpFormatter):
19+
""" Custom Help Formatter to fix additional spaces that appear if metavar is empty """
20+
21+
def _format_action_invocation(self, action: argparse.Action) -> str:
22+
default_format = super()._format_action_invocation(action)
23+
return default_format.replace(" ,", ",")
24+
25+
26+
class ThreadingBasicServer(ThreadingMixIn, HTTPServer):
27+
""" Enable threading for HTTP Server """
28+
29+
30+
class BasicHTTPRequestHandler(SimpleHTTPRequestHandler):
31+
""" Custom Request Handler """
32+
33+
def __init__(self, *handler_args, **handler_kwargs) -> None:
34+
self.dir_listing = handler_kwargs.pop('dir_listing', False)
35+
super().__init__(*handler_args, **handler_kwargs)
36+
self.follow_symlinks = False
37+
38+
# https://en.wikipedia.org/wiki/List_of_Unicode_characters#Control_codes
39+
_control_char_table = str.maketrans({c: fr'\x{c:02x}' for c in
40+
itertools.chain(range(0x20), range(0x7f, 0xa0))})
41+
_control_char_table[ord('\\')] = r'\\'
42+
43+
def log_message(self, *log_args) -> None:
44+
""" Custom log message formatter """
45+
message: str = log_args[0] % log_args[1:]
46+
logging.info("%s - - %s",
47+
self.address_string(),
48+
message.translate(self._control_char_table))
49+
50+
def list_directory(self, path: str | os.PathLike[str]) -> BytesIO | None:
51+
""" Add control over directory listing """
52+
if not self.dir_listing:
53+
self.send_error(403, "Directory listing is disabled")
54+
return None
55+
return super().list_directory(path)
56+
57+
58+
def basic_http_server(port: int, public_dir: Path, dir_listing: bool) -> None:
59+
""" Starts a basic HTTP server """
60+
if not public_dir.exists() or not public_dir.is_dir():
61+
logging.error("Directory \"%s\" doesn't exist", public_dir)
62+
return
63+
64+
logging.info("Initializing Basic HTTP Server")
65+
try:
66+
httpd = ThreadingBasicServer(("", port), partial(
67+
BasicHTTPRequestHandler, directory=public_dir, dir_listing=dir_listing))
68+
69+
logging.info("Available on port %s", port)
70+
httpd.serve_forever()
71+
except PermissionError as error:
72+
logging.error("%s. Port is already in use?", error)
73+
74+
75+
def parse_arguments() -> argparse.Namespace:
76+
""" Parses command-line arguments """
77+
parser = argparse.ArgumentParser(
78+
prog="bphs", description="Basic Python HTTP Server",
79+
formatter_class=CustomHelpFormatter
80+
)
81+
82+
parser.add_argument("-p", "--port", metavar="",
83+
default=8080, type=int,
84+
help="port to use [8080]")
85+
parser.add_argument("-d", "--dir", metavar="",
86+
default=Path(os.getcwd()), type=Path,
87+
help="directory to serve [current directory]")
88+
parser.add_argument("-l", "--listing", action="store_true",
89+
help="enable directory listing")
90+
return parser.parse_args()
91+
92+
93+
if __name__ == "__main__":
94+
args = parse_arguments()
95+
96+
try:
97+
basic_http_server(args.port, args.dir, args.listing)
98+
except KeyboardInterrupt:
99+
logging.info("Basic HTTP Server stopped")

0 commit comments

Comments
 (0)