Skip to content

Commit

Permalink
Merge ef45f10 into f198706
Browse files Browse the repository at this point in the history
  • Loading branch information
sarnold authored Oct 18, 2022
2 parents f198706 + ef45f10 commit 8cccb13
Show file tree
Hide file tree
Showing 15 changed files with 160 additions and 55 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ jobs:
strategy:
fail-fast: false
matrix:
os: [ubuntu-20.04, macos-latest, windows-latest]
python-version: [3.7, 3.8, 3.9, '3.10']
os: [ubuntu-18.04, macos-12, windows-latest]
python-version: [3.6, 3.7, 3.9, '3.10']
steps:
- name: Set git crlf/eol
run: |
Expand Down
14 changes: 10 additions & 4 deletions .github/workflows/coverage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -134,22 +134,28 @@ jobs:
PIP_DOWNLOAD_CACHE: ${{ github.workspace }}/../.pip_download_cache

steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
with:
fetch-depth: 0

- uses: actions/setup-python@v2
- uses: actions/setup-python@v4
with:
python-version: ${{ env.PYTHON }}
cache: 'pip' # caching pip dependencies

- name: Add python requirements
run: |
python -m pip install --upgrade pip
pip install tox
- name: Generate coverage and fix pkg name
- name: Setup old python for test
uses: actions/setup-python@v4
with:
python-version: 3.6

- name: Generate coverage
run: |
tox -e py
tox -e coverage,py36,py39
- name: Code Coverage Summary Report (data)
uses: irongut/CodeCoverageSummary@v1.1.0
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/wheels.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ jobs:
strategy:
fail-fast: false
matrix:
os: [ubuntu-20.04, macos-latest, windows-latest]
python-version: [3.7, 3.8, 3.9, '3.10']
os: [ubuntu-20.04, macos-11, windows-2019]
python-version: [3.6, 3.7, 3.9, '3.10']

steps:
- name: Set git crlf/eol
Expand All @@ -46,7 +46,7 @@ jobs:
tox -e build,check
- name: Upload artifacts
if: matrix.python-version == 3.8 && runner.os == 'Linux'
if: matrix.python-version == 3.9 && runner.os == 'Linux'
uses: actions/upload-artifact@v2
with:
name: wheels
Expand Down
7 changes: 4 additions & 3 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -163,13 +163,14 @@ To run pylint::
To install the latest release, eg with your own ``tox.ini`` file in
another project, use something like this::

$ pip install -U -f https://github.com/sarnold/pyserv/releases/ pyserv
$ pip install https://github.com/sarnold/pyserv/releases/download/1.2.4/pyserv-1.2.4-py3-none-any.whl

If you have a ``requirements.txt`` file, you can add something like this::

-f https://github.com/sarnold/pyserv/releases/
pyserv>=1.2.3
pyserv @ https://github.com/sarnold/pyserv/releases/download/1.2.4/pyserv-1.2.4.tar.gz

Note the newest pip versions may no longer work using ``-f`` with just
the GH "releases" path to get the latest release from Github.

.. _Tox: https://github.com/tox-dev/tox

Expand Down
6 changes: 6 additions & 0 deletions mypy.ini
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,9 @@ warn_unused_configs = True

[mypy-appdirs]
ignore_missing_imports = True

[mypy-daemon]
ignore_missing_imports = True

[mypy-daemon.parent_logger]
ignore_missing_imports = True
8 changes: 5 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
requires = [
"setuptools>=42",
"versioningit >= 1.1.1",
"wheel",
]

build-backend = "setuptools.build_meta"
Expand All @@ -17,7 +16,10 @@ markers = "subscript"

[tool.coverage.run]
branch = true
source = ["pyserv"]
source = [
"pyserv/",
".tox/py*/lib/python*/site-packages/",
]
omit = [
"tests",
".tox",
Expand All @@ -27,7 +29,7 @@ omit = [
source = ["pyserv"]

[tool.coverage.report]
fail_under = 85
fail_under = 80
show_missing = true
exclude_lines = [
"pragma: no cover",
Expand Down
24 changes: 19 additions & 5 deletions pyserv/__init__.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
"""Simple HTTP server classes with GET path rewriting and request/header logging."""
"""
Simple HTTP server classes with GET path rewriting and request/header logging.
"""

import logging
import sys
import threading
from functools import partial
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
from http.server import HTTPServer, SimpleHTTPRequestHandler
from socketserver import ThreadingMixIn
from urllib.parse import urlparse

from ._version import __version__
Expand All @@ -15,10 +19,10 @@

def munge_url(ota_url):
"""
Parse the url sent by OTA command for file path and netloc.
Parse the url sent by OTA command for file path and host string.
:param ota_url: (possibly) broken GET path
:return tuple: netloc and path from `urlparse`
:return tuple: netloc and path from ``urlparse``
"""
url_data = urlparse(str(ota_url))
file_path = url_data.path
Expand All @@ -28,6 +32,13 @@ def munge_url(ota_url):
return host_str, file_path


class ThreadingHTTPServer(ThreadingMixIn, HTTPServer):
"""
Backwards-compatible server class for Python <3.7 on older distros,
eg, Ubuntu bionic LTS.
"""


class GetHandler(SimpleHTTPRequestHandler):
"""
Munge the incoming request path from Dialog OTA. Runs `urlparse` on
Expand Down Expand Up @@ -78,7 +89,10 @@ def __init__(self, iface, port, directory):
self.iface = iface
self.port = int(port)
self.directory = directory
self.handler = partial(GetHandler, directory=self.directory)
if sys.version_info < (3, 7):
self.handler = GetHandler
else:
self.handler = partial(GetHandler, directory=self.directory)
self.server = ThreadingHTTPServer((self.iface, self.port), self.handler)

def run(self):
Expand Down
11 changes: 9 additions & 2 deletions pyserv/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@
"""

import logging
import os
import sys
from pathlib import Path

from . import GetServer
from .settings import DEBUG
from .settings import DEBUG, DOCROOT

LVL_NAME = 'DEBUG' if DEBUG else 'INFO'

Expand All @@ -25,21 +27,26 @@ def serv_init(iface, port, directory):
return httpd_handler


def serv_run(iface='', port=8080, directory='.'): # pragma: no cover
def serv_run(iface='', port=8080, directory=DOCROOT): # pragma: no cover
"""
Run in foreground command wrapper for console entry point;
init logging and server, run the server, stop the server.
:param iface: server listen interface
:param port: server listen port
"""
start_dir = Path.cwd()
path_diff = start_dir.name != Path(directory).name
if sys.version_info < (3, 7) and path_diff:
os.chdir(directory)
httpd = serv_init(iface, port, directory)
logging.info('Starting HTTP SERVER at %s:%s', iface, port)
try:
httpd.start()
httpd.join()
except KeyboardInterrupt:
httpd.stop()
os.chdir(start_dir)
print("\nExiting ...")


Expand Down
14 changes: 8 additions & 6 deletions pyserv/settings.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"""
Pyserv default settings for daemon mode.
Pyserv default settings for server and daemon modes.
"""
import importlib
import os
Expand Down Expand Up @@ -60,12 +60,12 @@ def show_uservars():
Display defaults and (possibly) overridden host paths and environment
variables.
"""
print("Python version:", sys.version)
print(f"Python version: {sys.version}")
print("-" * 79)
print(f"pyserv {version}")

iface = 'all' if not IFACE else IFACE
dirnames = ['log_dir', 'pid_dir', 'doc_dir']
dirnames = ['log_dir', 'pid_dir', 'work_dir']
modname = 'pyserv.settings'
try:
mod = importlib.import_module(modname)
Expand All @@ -79,18 +79,20 @@ def show_uservars():
print(f" DEBUG: {DEBUG}")
print(f" PORT: {PORT}")
print(f" IFACE: {iface}")
print(f" LPNAME: {LPNAME}")
print(f" LOG: {LOG}")
print(f" PID: {PID}")
print(f" DOCROOT: {DOCROOT}")
print("-" * 79)

except (ImportError, NameError) as exc:
print("FAILED:", repr(exc))
print(f"FAILED: {repr(exc)}")


DEBUG = os.getenv('DEBUG', default=None)
PORT = os.getenv('PORT', default='8080')
IFACE = os.getenv('IFACE', default='127.0.0.1')
LOG = os.getenv('LOG', default=str(get_userdirs()[0].joinpath('httpd.log')))
PID = os.getenv('PID', default=str(get_userdirs()[1].joinpath('httpd.pid')))
LPNAME = os.getenv('LPNAME', default='httpd')
LOG = os.getenv('LOG', default=str(get_userdirs()[0].joinpath(f'{LPNAME}.log')))
PID = os.getenv('PID', default=str(get_userdirs()[1].joinpath(f'{LPNAME}.pid')))
DOCROOT = os.getenv('DOCROOT', default=str(get_userdirs()[2]))
3 changes: 1 addition & 2 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
# daemon requirements, useful for tox/pip
-f https://github.com/sarnold/python-daemonizer/releases/
daemonizer>=0.3.4; platform_system!="Windows"
daemonizer @ git+https://github.com/sarnold/python-daemonizer.git@0.3.5#5f6bc3c80a90344b2c8e4cc24ed0b8c098a7af50; platform_system!="Windows"
appdirs
28 changes: 17 additions & 11 deletions scripts/httpdaemon
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
#!/usr/bin/env python3
"""
Http daemon script using pyserv (see settings.py for env vars).
"""

import argparse
import datetime
import logging
import os
import sys
from datetime import timezone
from pathlib import Path

from daemon import Daemon
Expand All @@ -29,25 +30,30 @@ from pyserv.settings import (
logger = logging.getLogger(__name__)


class servDaemon(Daemon):
class ServDaemon(Daemon):
"""
Init daemon with custom run/cleanup methods, pass user vars to the
server.
"""
servd = None

def run(self):
"""
Daemon needs a run method. In this case we need to instantiate
our GetServer obj here, ie, *after* the Daemon object.
"""
servd = GetServer(IFACE, PORT, DOCROOT)
servd.start()
if sys.version_info < (3, 7):
os.chdir(DOCROOT)
self.servd = GetServer(IFACE, PORT, DOCROOT)
self.servd.start()

def cleanup(self):
"""And we need a cleanup method."""
servd.stop()
self.servd.stop()


if __name__ == "__main__":
"""
Collect and process environment vars, init directories if needed,
setup logging. Check if platform is POSIX, then start the daemon.
"""

if not platform_check():
raise OSError(f'Incompatible platform type "{sys.platform}"')

Expand All @@ -67,7 +73,7 @@ if __name__ == "__main__":
setup_logging(DEBUG, Path(LOG), 'httpd')
# printout()

d = servDaemon(
d = ServDaemon(
Path(PID),
home_dir=DOCROOT,
verbose=0,
Expand Down
3 changes: 2 additions & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ classifiers =
Intended Audience :: Developers
Programming Language :: Python :: 3
Programming Language :: Python :: 3 :: Only
Programming Language :: Python :: 3.6
Programming Language :: Python :: 3.7
Programming Language :: Python :: 3.8
Programming Language :: Python :: 3.9
Expand All @@ -28,7 +29,7 @@ keywords =
httpd

[options]
python_requires = >= 3.7
python_requires = >= 3.6
install_requires =
appdirs
daemonizer @ git+https://github.com/sarnold/python-daemonizer.git@0.3.5#5f6bc3c80a90344b2c8e4cc24ed0b8c098a7af50
Expand Down
25 changes: 22 additions & 3 deletions tests/test_handler.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,43 @@
# -*- coding: utf-8 -*-
import pathlib
import sys
import unittest
import urllib.request

import httptest
import pytest

from pyserv import GetHandler

FILE_PATH = pathlib.Path(__file__)
REAL_PATH = pathlib.Path('requirements.txt')

class TestHTTPTestMethods(unittest.TestCase):

class TestHTTPHandler(unittest.TestCase):

@pytest.mark.skipif(sys.version_info < (3, 7), reason="requires python3.7 or higher")
@httptest.Server(
lambda *args: GetHandler(
*args, directory=FILE_PATH.parent
)
)
def test_call_response(self, ts=httptest.NoServer()):
def test_call_response_dir(self, ts=httptest.NoServer()):
with urllib.request.urlopen(ts.url() + FILE_PATH.name) as f:
self.assertEqual(f.read().decode('utf-8'), FILE_PATH.read_text())

@httptest.Server(GetHandler)
def test_url(self, ts=httptest.NoServer()):
'''
Make sure the Server.url() method works.
'''
url = ts.url()
self.assertIn(':', url)
self.assertEqual(':'.join(url.split(':')[:-1]), 'http://localhost')

@httptest.Server(GetHandler)
def test_call_response_no_dir(self, ts=httptest.NoServer()):
with urllib.request.urlopen(ts.url() + REAL_PATH.name) as f:
self.assertEqual(f.read().decode('utf-8'), REAL_PATH.read_text())


if __name__ == '__main__':
unittest.main()
Loading

0 comments on commit 8cccb13

Please sign in to comment.