Skip to content

Initial release #1

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Mar 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 120 additions & 0 deletions .github/workflows/build_main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
name: Build and Release latest version

on:
push:
branches: main

jobs:
check_build_number:
runs-on: ubuntu-latest
outputs:
number: ${{ steps.check_number.outputs.number }}
new_release: ${{ steps.check_number.outputs.new_release }}

steps:
- name: Checkout VERSION
uses: actions/checkout@v3
with:
sparse-checkout: |
VERSION
sparse-checkout-cone-mode: false

- name: Check latest release and current version number
id: check_number
run: |
latest_release_json=$(curl -L \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer ${{ secrets.ACCESS_TOKEN }}" \
-H "X-GitHub-Api-Version: 2022-11-28" \
"https://api.github.com/repos/${GITHUB_REPOSITORY}/releases/latest")

latest_release_tag=$(echo "$latest_release_json" | jq -r .tag_name)
current_version=$(head -1 VERSION)

echo "Latest release version: $latest_release_tag"
echo "Current version: $current_version"

if [ "$latest_release_tag" == "$current_version" ]; then
echo "Release already exists"
echo "new_release=false" >> "$GITHUB_OUTPUT"
else
echo "New release"
echo "number=$(head -1 VERSION)" >> "$GITHUB_OUTPUT"
fi

build_executables:
needs: check_build_number
if: needs.check_build_number.outputs.new_release != 'false'
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
runs-on: ${{ matrix.os }}
env:
PYTHON_VERSION: "3.12"

steps:
- name: Checkout repository
uses: actions/checkout@v3

- name: Set up Python ${{ env.PYTHON_VERSION }}
uses: actions/setup-python@v3
with:
python-version: ${{ env.PYTHON_VERSION }}

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install pyinstaller

- name: Create a one-file bundled executable
run: pyinstaller -n app_${{ needs.check_build_number.outputs.number }}_${{ runner.os }} -F src/main.py

- name: Upload executable
uses: actions/upload-artifact@v4
with:
name: ${{ needs.check_build_number.outputs.number }}-${{ runner.os }}
path: dist/

release_build:
needs: [check_build_number, build_executables]
runs-on: ubuntu-latest
steps:
- name: Download artifacts
uses: actions/download-artifact@v4
with:
path: artifacts

- name: Archive artifacts
run: |
find "artifacts" -type f | while read -r file; do
filename=$(basename "$file" .exe)
zip -j "$(dirname "$file")/$filename.zip" "$file"
rm "$file"
done

- name: Create Release
id: current_release
run: |
release_json=$(curl -L -f \
-X POST \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer ${{ secrets.ACCESS_TOKEN }}" \
-H "X-GitHub-Api-Version: 2022-11-28" \
https://api.github.com/repos/${GITHUB_REPOSITORY}/releases \
-d '{"tag_name":"${{ needs.check_build_number.outputs.number }}","name":"${{ needs.check_build_number.outputs.number }}"}')

echo "release_id=$(echo $release_json | jq -r .id)" >> $GITHUB_OUTPUT

- name: Upload a release assets
run: |
find "artifacts" -type f | while read -r file; do
curl -L -f \
-X POST \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer ${{ secrets.ACCESS_TOKEN }}" \
-H "X-GitHub-Api-Version: 2022-11-28" \
-H "Content-Type: application/octet-stream" \
"https://uploads.github.com/repos/${GITHUB_REPOSITORY}/releases/${{ steps.current_release.outputs.release_id }}/assets?name=$(basename "$file")" \
--data-binary "@$file"
echo "Uploaded: $(basename "$file")"
done
30 changes: 30 additions & 0 deletions .github/workflows/linting.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: Linting

on:
push:
branches: dev
pull_request:
branches: dev, main

env:
PYTHON_VERSION: "3.12"

jobs:
pylint_check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3

- name: Set up Python ${{ env.PYTHON_VERSION }}
uses: actions/setup-python@v3
with:
python-version: ${{ env.PYTHON_VERSION }}

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install pylint

- name: Analysing the code with pylint
run: |
pylint $(git ls-files 'src/*.py')
24 changes: 23 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,23 @@
# basic-python-http-server
# Basic Python HTTP Server (BPHS)

Command-line static HTTP server build upon Python [`HTTPServer`][1] from [standard library][2].

No additional dependencies required.

## Installation

One-file bundled executable can be downloaded from the **Releases** section.

## Usage

```txt
bphs [-h] [-p] [-d] [-l]

-h, --help show help message and exit
-p, --port port to use [8080]
-d, --dir directory to serve [current directory]
-l, --listing enable directory listing
```

[1]: https://github.com/python/cpython/blob/3.12/Lib/http/server.py
[2]: https://docs.python.org/3/library/http.server.html
1 change: 1 addition & 0 deletions VERSION
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
1.0.0
99 changes: 99 additions & 0 deletions src/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
""" Basic Python HTTP Server """

import os
import logging
import argparse
import itertools
from io import BytesIO
from pathlib import Path
from functools import partial
from socketserver import ThreadingMixIn
from http.server import HTTPServer, SimpleHTTPRequestHandler


logging.basicConfig(level=logging.INFO,
format='%(asctime)s | %(levelname)s | %(message)s')


class CustomHelpFormatter(argparse.HelpFormatter):
""" Custom Help Formatter to fix additional spaces that appear if metavar is empty """

def _format_action_invocation(self, action: argparse.Action) -> str:
default_format = super()._format_action_invocation(action)
return default_format.replace(" ,", ",")


class ThreadingBasicServer(ThreadingMixIn, HTTPServer):
""" Enable threading for HTTP Server """


class BasicHTTPRequestHandler(SimpleHTTPRequestHandler):
""" Custom Request Handler """

def __init__(self, *handler_args, **handler_kwargs) -> None:
self.dir_listing = handler_kwargs.pop('dir_listing', False)
super().__init__(*handler_args, **handler_kwargs)
self.follow_symlinks = False

# https://en.wikipedia.org/wiki/List_of_Unicode_characters#Control_codes
_control_char_table = str.maketrans({c: fr'\x{c:02x}' for c in
itertools.chain(range(0x20), range(0x7f, 0xa0))})
_control_char_table[ord('\\')] = r'\\'

def log_message(self, *log_args) -> None:
""" Custom log message formatter """
message: str = log_args[0] % log_args[1:]
logging.info("%s - - %s",
self.address_string(),
message.translate(self._control_char_table))

def list_directory(self, path: str | os.PathLike[str]) -> BytesIO | None:
""" Add control over directory listing """
if not self.dir_listing:
self.send_error(403, "Directory listing is disabled")
return None
return super().list_directory(path)


def basic_http_server(port: int, public_dir: Path, dir_listing: bool) -> None:
""" Starts a basic HTTP server """
if not public_dir.exists() or not public_dir.is_dir():
logging.error("Directory \"%s\" doesn't exist", public_dir)
return

logging.info("Initializing Basic HTTP Server")
try:
httpd = ThreadingBasicServer(("", port), partial(
BasicHTTPRequestHandler, directory=public_dir, dir_listing=dir_listing))

logging.info("Available on port %s", port)
httpd.serve_forever()
except PermissionError as error:
logging.error("%s. Port is already in use?", error)


def parse_arguments() -> argparse.Namespace:
""" Parses command-line arguments """
parser = argparse.ArgumentParser(
prog="bphs", description="Basic Python HTTP Server",
formatter_class=CustomHelpFormatter
)

parser.add_argument("-p", "--port", metavar="",
default=8080, type=int,
help="port to use [8080]")
parser.add_argument("-d", "--dir", metavar="",
default=Path(os.getcwd()), type=Path,
help="directory to serve [current directory]")
parser.add_argument("-l", "--listing", action="store_true",
help="enable directory listing")
return parser.parse_args()


if __name__ == "__main__":
args = parse_arguments()

try:
basic_http_server(args.port, args.dir, args.listing)
except KeyboardInterrupt:
logging.info("Basic HTTP Server stopped")
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy