Compare commits
46 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c43147a016 | ||
|
|
ca65b59d66 | ||
|
|
d9fb1e5343 | ||
|
|
0f654784d8 | ||
|
|
9ed25b875e | ||
|
|
3c1b8e10bd | ||
|
|
b70106b17b | ||
|
|
b6a95f34a8 | ||
|
|
cd39addea8 | ||
|
|
fa365b0149 | ||
|
|
09e5431071 | ||
|
|
c8d958daaf | ||
|
|
06820fbe03 | ||
|
|
529e0a8d1c | ||
|
|
aea7b3809e | ||
|
|
bcef270a3d | ||
|
|
5f696e77e7 | ||
|
|
17364edf80 | ||
|
|
1bb84f9ee1 | ||
|
|
be94572e4d | ||
|
|
27bf079b4d | ||
|
|
ec89785428 | ||
|
|
0e6180daf6 | ||
|
|
10cfca75d4 | ||
|
|
ff165dd59c | ||
|
|
b2b01aaaa9 | ||
|
|
14d6bef9e2 | ||
|
|
736471afe1 | ||
|
|
3d4a64916d | ||
|
|
d943404f72 | ||
|
|
420ef62f8a | ||
|
|
9b16e16f5b | ||
|
|
aaa640e303 | ||
|
|
4cc9503303 | ||
|
|
863ac1e73c | ||
|
|
2f1c484c7b | ||
|
|
bc9c844e60 | ||
|
|
1b9371a943 | ||
|
|
dc9f83cd1a | ||
|
|
9b06f5a3e4 | ||
|
|
b00f70f1c4 | ||
|
|
b88bfa53ef | ||
|
|
f7d20b5fe6 | ||
|
|
e49a5ed0b3 | ||
|
|
2cc63fe366 | ||
|
|
1c38bc8ca6 |
49 changed files with 905 additions and 815 deletions
11
.gitattributes
vendored
Normal file
11
.gitattributes
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
# SPDX-FileCopyrightText: 2024 Justin Myers for Adafruit Industries
|
||||
#
|
||||
# SPDX-License-Identifier: Unlicense
|
||||
|
||||
.py text eol=lf
|
||||
.rst text eol=lf
|
||||
.txt text eol=lf
|
||||
.yaml text eol=lf
|
||||
.toml text eol=lf
|
||||
.license text eol=lf
|
||||
.md text eol=lf
|
||||
|
|
@ -1,42 +1,21 @@
|
|||
# SPDX-FileCopyrightText: 2020 Diego Elio Pettenò
|
||||
# SPDX-FileCopyrightText: 2024 Justin Myers for Adafruit Industries
|
||||
#
|
||||
# SPDX-License-Identifier: Unlicense
|
||||
|
||||
repos:
|
||||
- repo: https://github.com/python/black
|
||||
rev: 23.3.0
|
||||
hooks:
|
||||
- id: black
|
||||
- repo: https://github.com/fsfe/reuse-tool
|
||||
rev: v1.1.2
|
||||
hooks:
|
||||
- id: reuse
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.4.0
|
||||
rev: v4.5.0
|
||||
hooks:
|
||||
- id: check-yaml
|
||||
- id: end-of-file-fixer
|
||||
- id: trailing-whitespace
|
||||
- repo: https://github.com/pycqa/pylint
|
||||
rev: v2.17.4
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.3.4
|
||||
hooks:
|
||||
- id: pylint
|
||||
name: pylint (library code)
|
||||
types: [python]
|
||||
args:
|
||||
- --disable=consider-using-f-string
|
||||
exclude: "^(docs/|examples/|tests/|setup.py$)"
|
||||
- id: pylint
|
||||
name: pylint (example code)
|
||||
description: Run pylint rules on "examples/*.py" files
|
||||
types: [python]
|
||||
files: "^examples/"
|
||||
args:
|
||||
- --disable=missing-docstring,invalid-name,consider-using-f-string,duplicate-code
|
||||
- id: pylint
|
||||
name: pylint (test code)
|
||||
description: Run pylint rules on "tests/*.py" files
|
||||
types: [python]
|
||||
files: "^tests/"
|
||||
args:
|
||||
- --disable=missing-docstring,consider-using-f-string,duplicate-code
|
||||
- id: ruff-format
|
||||
- id: ruff
|
||||
args: ["--fix"]
|
||||
- repo: https://github.com/fsfe/reuse-tool
|
||||
rev: v3.0.1
|
||||
hooks:
|
||||
- id: reuse
|
||||
|
|
|
|||
399
.pylintrc
399
.pylintrc
|
|
@ -1,399 +0,0 @@
|
|||
# SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries
|
||||
#
|
||||
# SPDX-License-Identifier: Unlicense
|
||||
|
||||
[MASTER]
|
||||
|
||||
# A comma-separated list of package or module names from where C extensions may
|
||||
# be loaded. Extensions are loading into the active Python interpreter and may
|
||||
# run arbitrary code
|
||||
extension-pkg-whitelist=
|
||||
|
||||
# Add files or directories to the ignore-list. They should be base names, not
|
||||
# paths.
|
||||
ignore=CVS
|
||||
|
||||
# Add files or directories matching the regex patterns to the ignore-list. The
|
||||
# regex matches against base names, not paths.
|
||||
ignore-patterns=
|
||||
|
||||
# Python code to execute, usually for sys.path manipulation such as
|
||||
# pygtk.require().
|
||||
#init-hook=
|
||||
|
||||
# Use multiple processes to speed up Pylint.
|
||||
jobs=1
|
||||
|
||||
# List of plugins (as comma separated values of python modules names) to load,
|
||||
# usually to register additional checkers.
|
||||
load-plugins=pylint.extensions.no_self_use
|
||||
|
||||
# Pickle collected data for later comparisons.
|
||||
persistent=yes
|
||||
|
||||
# Specify a configuration file.
|
||||
#rcfile=
|
||||
|
||||
# Allow loading of arbitrary C extensions. Extensions are imported into the
|
||||
# active Python interpreter and may run arbitrary code.
|
||||
unsafe-load-any-extension=no
|
||||
|
||||
|
||||
[MESSAGES CONTROL]
|
||||
|
||||
# Only show warnings with the listed confidence levels. Leave empty to show
|
||||
# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED
|
||||
confidence=
|
||||
|
||||
# Disable the message, report, category or checker with the given id(s). You
|
||||
# can either give multiple identifiers separated by comma (,) or put this
|
||||
# option multiple times (only on the command line, not in the configuration
|
||||
# file where it should appear only once).You can also use "--disable=all" to
|
||||
# disable everything first and then reenable specific checks. For example, if
|
||||
# you want to run only the similarities checker, you can use "--disable=all
|
||||
# --enable=similarities". If you want to run only the classes checker, but have
|
||||
# no Warning level messages displayed, use"--disable=all --enable=classes
|
||||
# --disable=W"
|
||||
# disable=import-error,raw-checker-failed,bad-inline-option,locally-disabled,file-ignored,suppressed-message,useless-suppression,deprecated-pragma,deprecated-str-translate-call
|
||||
disable=raw-checker-failed,bad-inline-option,locally-disabled,file-ignored,suppressed-message,useless-suppression,deprecated-pragma,import-error,pointless-string-statement,unspecified-encoding
|
||||
|
||||
# Enable the message, report, category or checker with the given id(s). You can
|
||||
# either give multiple identifier separated by comma (,) or put this option
|
||||
# multiple time (only on the command line, not in the configuration file where
|
||||
# it should appear only once). See also the "--disable" option for examples.
|
||||
enable=
|
||||
|
||||
|
||||
[REPORTS]
|
||||
|
||||
# Python expression which should return a note less than 10 (10 is the highest
|
||||
# note). You have access to the variables errors warning, statement which
|
||||
# respectively contain the number of errors / warnings messages and the total
|
||||
# number of statements analyzed. This is used by the global evaluation report
|
||||
# (RP0004).
|
||||
evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
|
||||
|
||||
# Template used to display messages. This is a python new-style format string
|
||||
# used to format the message information. See doc for all details
|
||||
#msg-template=
|
||||
|
||||
# Set the output format. Available formats are text, parseable, colorized, json
|
||||
# and msvs (visual studio).You can also give a reporter class, eg
|
||||
# mypackage.mymodule.MyReporterClass.
|
||||
output-format=text
|
||||
|
||||
# Tells whether to display a full report or only the messages
|
||||
reports=no
|
||||
|
||||
# Activate the evaluation score.
|
||||
score=yes
|
||||
|
||||
|
||||
[REFACTORING]
|
||||
|
||||
# Maximum number of nested blocks for function / method body
|
||||
max-nested-blocks=5
|
||||
|
||||
|
||||
[LOGGING]
|
||||
|
||||
# Logging modules to check that the string format arguments are in logging
|
||||
# function parameter format
|
||||
logging-modules=logging
|
||||
|
||||
|
||||
[SPELLING]
|
||||
|
||||
# Spelling dictionary name. Available dictionaries: none. To make it working
|
||||
# install python-enchant package.
|
||||
spelling-dict=
|
||||
|
||||
# List of comma separated words that should not be checked.
|
||||
spelling-ignore-words=
|
||||
|
||||
# A path to a file that contains private dictionary; one word per line.
|
||||
spelling-private-dict-file=
|
||||
|
||||
# Tells whether to store unknown words to indicated private dictionary in
|
||||
# --spelling-private-dict-file option instead of raising a message.
|
||||
spelling-store-unknown-words=no
|
||||
|
||||
|
||||
[MISCELLANEOUS]
|
||||
|
||||
# List of note tags to take in consideration, separated by a comma.
|
||||
# notes=FIXME,XXX,TODO
|
||||
notes=FIXME,XXX
|
||||
|
||||
|
||||
[TYPECHECK]
|
||||
|
||||
# List of decorators that produce context managers, such as
|
||||
# contextlib.contextmanager. Add to this list to register other decorators that
|
||||
# produce valid context managers.
|
||||
contextmanager-decorators=contextlib.contextmanager
|
||||
|
||||
# List of members which are set dynamically and missed by pylint inference
|
||||
# system, and so shouldn't trigger E1101 when accessed. Python regular
|
||||
# expressions are accepted.
|
||||
generated-members=
|
||||
|
||||
# Tells whether missing members accessed in mixin class should be ignored. A
|
||||
# mixin class is detected if its name ends with "mixin" (case insensitive).
|
||||
ignore-mixin-members=yes
|
||||
|
||||
# This flag controls whether pylint should warn about no-member and similar
|
||||
# checks whenever an opaque object is returned when inferring. The inference
|
||||
# can return multiple potential results while evaluating a Python object, but
|
||||
# some branches might not be evaluated, which results in partial inference. In
|
||||
# that case, it might be useful to still emit no-member and other checks for
|
||||
# the rest of the inferred objects.
|
||||
ignore-on-opaque-inference=yes
|
||||
|
||||
# List of class names for which member attributes should not be checked (useful
|
||||
# for classes with dynamically set attributes). This supports the use of
|
||||
# qualified names.
|
||||
ignored-classes=optparse.Values,thread._local,_thread._local
|
||||
|
||||
# List of module names for which member attributes should not be checked
|
||||
# (useful for modules/projects where namespaces are manipulated during runtime
|
||||
# and thus existing member attributes cannot be deduced by static analysis. It
|
||||
# supports qualified module names, as well as Unix pattern matching.
|
||||
ignored-modules=board
|
||||
|
||||
# Show a hint with possible names when a member name was not found. The aspect
|
||||
# of finding the hint is based on edit distance.
|
||||
missing-member-hint=yes
|
||||
|
||||
# The minimum edit distance a name should have in order to be considered a
|
||||
# similar match for a missing member name.
|
||||
missing-member-hint-distance=1
|
||||
|
||||
# The total number of similar names that should be taken in consideration when
|
||||
# showing a hint for a missing member.
|
||||
missing-member-max-choices=1
|
||||
|
||||
|
||||
[VARIABLES]
|
||||
|
||||
# List of additional names supposed to be defined in builtins. Remember that
|
||||
# you should avoid to define new builtins when possible.
|
||||
additional-builtins=
|
||||
|
||||
# Tells whether unused global variables should be treated as a violation.
|
||||
allow-global-unused-variables=yes
|
||||
|
||||
# List of strings which can identify a callback function by name. A callback
|
||||
# name must start or end with one of those strings.
|
||||
callbacks=cb_,_cb
|
||||
|
||||
# A regular expression matching the name of dummy variables (i.e. expectedly
|
||||
# not used).
|
||||
dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_
|
||||
|
||||
# Argument names that match this expression will be ignored. Default to name
|
||||
# with leading underscore
|
||||
ignored-argument-names=_.*|^ignored_|^unused_
|
||||
|
||||
# Tells whether we should check for unused import in __init__ files.
|
||||
init-import=no
|
||||
|
||||
# List of qualified module names which can have objects that can redefine
|
||||
# builtins.
|
||||
redefining-builtins-modules=six.moves,future.builtins
|
||||
|
||||
|
||||
[FORMAT]
|
||||
|
||||
# Expected format of line ending, e.g. empty (any line ending), LF or CRLF.
|
||||
# expected-line-ending-format=
|
||||
expected-line-ending-format=LF
|
||||
|
||||
# Regexp for a line that is allowed to be longer than the limit.
|
||||
ignore-long-lines=^\s*(# )?<?https?://\S+>?$
|
||||
|
||||
# Number of spaces of indent required inside a hanging or continued line.
|
||||
indent-after-paren=4
|
||||
|
||||
# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
|
||||
# tab).
|
||||
indent-string=' '
|
||||
|
||||
# Maximum number of characters on a single line.
|
||||
max-line-length=100
|
||||
|
||||
# Maximum number of lines in a module
|
||||
max-module-lines=1000
|
||||
|
||||
# Allow the body of a class to be on the same line as the declaration if body
|
||||
# contains single statement.
|
||||
single-line-class-stmt=no
|
||||
|
||||
# Allow the body of an if to be on the same line as the test if there is no
|
||||
# else.
|
||||
single-line-if-stmt=no
|
||||
|
||||
|
||||
[SIMILARITIES]
|
||||
|
||||
# Ignore comments when computing similarities.
|
||||
ignore-comments=yes
|
||||
|
||||
# Ignore docstrings when computing similarities.
|
||||
ignore-docstrings=yes
|
||||
|
||||
# Ignore imports when computing similarities.
|
||||
ignore-imports=yes
|
||||
|
||||
# Minimum lines number of a similarity.
|
||||
min-similarity-lines=12
|
||||
|
||||
|
||||
[BASIC]
|
||||
|
||||
# Regular expression matching correct argument names
|
||||
argument-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$
|
||||
|
||||
# Regular expression matching correct attribute names
|
||||
attr-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$
|
||||
|
||||
# Bad variable names which should always be refused, separated by a comma
|
||||
bad-names=foo,bar,baz,toto,tutu,tata
|
||||
|
||||
# Regular expression matching correct class attribute names
|
||||
class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$
|
||||
|
||||
# Regular expression matching correct class names
|
||||
# class-rgx=[A-Z_][a-zA-Z0-9]+$
|
||||
class-rgx=[A-Z_][a-zA-Z0-9_]+$
|
||||
|
||||
# Regular expression matching correct constant names
|
||||
const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$
|
||||
|
||||
# Minimum line length for functions/classes that require docstrings, shorter
|
||||
# ones are exempt.
|
||||
docstring-min-length=-1
|
||||
|
||||
# Regular expression matching correct function names
|
||||
function-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$
|
||||
|
||||
# Good variable names which should always be accepted, separated by a comma
|
||||
# good-names=i,j,k,ex,Run,_
|
||||
good-names=r,g,b,w,i,j,k,n,x,y,z,ex,ok,Run,_
|
||||
|
||||
# Include a hint for the correct naming format with invalid-name
|
||||
include-naming-hint=no
|
||||
|
||||
# Regular expression matching correct inline iteration names
|
||||
inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$
|
||||
|
||||
# Regular expression matching correct method names
|
||||
method-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$
|
||||
|
||||
# Regular expression matching correct module names
|
||||
module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
|
||||
|
||||
# Colon-delimited sets of names that determine each other's naming style when
|
||||
# the name regexes allow several styles.
|
||||
name-group=
|
||||
|
||||
# Regular expression which should only match function or class names that do
|
||||
# not require a docstring.
|
||||
no-docstring-rgx=^_
|
||||
|
||||
# List of decorators that produce properties, such as abc.abstractproperty. Add
|
||||
# to this list to register other decorators that produce valid properties.
|
||||
property-classes=abc.abstractproperty
|
||||
|
||||
# Regular expression matching correct variable names
|
||||
variable-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$
|
||||
|
||||
|
||||
[IMPORTS]
|
||||
|
||||
# Allow wildcard imports from modules that define __all__.
|
||||
allow-wildcard-with-all=no
|
||||
|
||||
# Analyse import fallback blocks. This can be used to support both Python 2 and
|
||||
# 3 compatible code, which means that the block might have code that exists
|
||||
# only in one or another interpreter, leading to false positives when analysed.
|
||||
analyse-fallback-blocks=no
|
||||
|
||||
# Deprecated modules which should not be used, separated by a comma
|
||||
deprecated-modules=optparse,tkinter.tix
|
||||
|
||||
# Create a graph of external dependencies in the given file (report RP0402 must
|
||||
# not be disabled)
|
||||
ext-import-graph=
|
||||
|
||||
# Create a graph of every (i.e. internal and external) dependencies in the
|
||||
# given file (report RP0402 must not be disabled)
|
||||
import-graph=
|
||||
|
||||
# Create a graph of internal dependencies in the given file (report RP0402 must
|
||||
# not be disabled)
|
||||
int-import-graph=
|
||||
|
||||
# Force import order to recognize a module as part of the standard
|
||||
# compatibility libraries.
|
||||
known-standard-library=
|
||||
|
||||
# Force import order to recognize a module as part of a third party library.
|
||||
known-third-party=enchant
|
||||
|
||||
|
||||
[CLASSES]
|
||||
|
||||
# List of method names used to declare (i.e. assign) instance attributes.
|
||||
defining-attr-methods=__init__,__new__,setUp
|
||||
|
||||
# List of member names, which should be excluded from the protected access
|
||||
# warning.
|
||||
exclude-protected=_asdict,_fields,_replace,_source,_make
|
||||
|
||||
# List of valid names for the first argument in a class method.
|
||||
valid-classmethod-first-arg=cls
|
||||
|
||||
# List of valid names for the first argument in a metaclass class method.
|
||||
valid-metaclass-classmethod-first-arg=mcs
|
||||
|
||||
|
||||
[DESIGN]
|
||||
|
||||
# Maximum number of arguments for function / method
|
||||
max-args=5
|
||||
|
||||
# Maximum number of attributes for a class (see R0902).
|
||||
# max-attributes=7
|
||||
max-attributes=11
|
||||
|
||||
# Maximum number of boolean expressions in a if statement
|
||||
max-bool-expr=5
|
||||
|
||||
# Maximum number of branch for function / method body
|
||||
max-branches=12
|
||||
|
||||
# Maximum number of locals for function / method body
|
||||
max-locals=15
|
||||
|
||||
# Maximum number of parents for a class (see R0901).
|
||||
max-parents=7
|
||||
|
||||
# Maximum number of public methods for a class (see R0904).
|
||||
max-public-methods=20
|
||||
|
||||
# Maximum number of return / yield for function / method body
|
||||
max-returns=6
|
||||
|
||||
# Maximum number of statements in function / method body
|
||||
max-statements=50
|
||||
|
||||
# Minimum number of public methods for a class (see R0903).
|
||||
min-public-methods=1
|
||||
|
||||
|
||||
[EXCEPTIONS]
|
||||
|
||||
# Exceptions that will emit a warning when being caught. Defaults to
|
||||
# "Exception"
|
||||
overgeneral-exceptions=builtins.Exception
|
||||
|
|
@ -8,8 +8,11 @@
|
|||
# Required
|
||||
version: 2
|
||||
|
||||
sphinx:
|
||||
configuration: docs/conf.py
|
||||
|
||||
build:
|
||||
os: ubuntu-20.04
|
||||
os: ubuntu-lts-latest
|
||||
tools:
|
||||
python: "3"
|
||||
|
||||
|
|
|
|||
|
|
@ -17,9 +17,9 @@ Introduction
|
|||
:alt: Build Status
|
||||
|
||||
|
||||
.. image:: https://img.shields.io/badge/code%20style-black-000000.svg
|
||||
:target: https://github.com/psf/black
|
||||
:alt: Code Style: Black
|
||||
.. image:: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json
|
||||
:target: https://github.com/astral-sh/ruff
|
||||
:alt: Code Style: Ruff
|
||||
|
||||
HTTP Server for CircuitPython.
|
||||
|
||||
|
|
@ -32,6 +32,7 @@ HTTP Server for CircuitPython.
|
|||
- Supports URL parameters and wildcard URLs.
|
||||
- Supports HTTP Basic and Bearer Authentication on both server and route per level.
|
||||
- Supports Websockets and Server-Sent Events.
|
||||
- Limited support for HTTPS (only on selected microcontrollers with enough memory e.g. ESP32-S3).
|
||||
|
||||
|
||||
Dependencies
|
||||
|
|
|
|||
|
|
@ -25,70 +25,70 @@ __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_HTTPServer.git"
|
|||
|
||||
from .authentication import (
|
||||
Basic,
|
||||
Token,
|
||||
Bearer,
|
||||
Token,
|
||||
check_authentication,
|
||||
require_authentication,
|
||||
)
|
||||
from .exceptions import (
|
||||
ServerStoppedError,
|
||||
AuthenticationError,
|
||||
BackslashInPathError,
|
||||
FileNotExistsError,
|
||||
InvalidPathError,
|
||||
ParentDirectoryReferenceError,
|
||||
BackslashInPathError,
|
||||
ServerStoppedError,
|
||||
ServingFilesDisabledError,
|
||||
FileNotExistsError,
|
||||
)
|
||||
from .headers import Headers
|
||||
from .methods import (
|
||||
GET,
|
||||
POST,
|
||||
PUT,
|
||||
CONNECT,
|
||||
DELETE,
|
||||
PATCH,
|
||||
GET,
|
||||
HEAD,
|
||||
OPTIONS,
|
||||
PATCH,
|
||||
POST,
|
||||
PUT,
|
||||
TRACE,
|
||||
CONNECT,
|
||||
)
|
||||
from .mime_types import MIMETypes
|
||||
from .request import QueryParams, FormData, Request
|
||||
from .request import FormData, QueryParams, Request
|
||||
from .response import (
|
||||
Response,
|
||||
FileResponse,
|
||||
ChunkedResponse,
|
||||
FileResponse,
|
||||
JSONResponse,
|
||||
Redirect,
|
||||
Response,
|
||||
SSEResponse,
|
||||
Websocket,
|
||||
)
|
||||
from .route import Route, as_route
|
||||
from .server import (
|
||||
Server,
|
||||
NO_REQUEST,
|
||||
CONNECTION_TIMED_OUT,
|
||||
NO_REQUEST,
|
||||
REQUEST_HANDLED_NO_RESPONSE,
|
||||
REQUEST_HANDLED_RESPONSE_SENT,
|
||||
Server,
|
||||
)
|
||||
from .status import (
|
||||
Status,
|
||||
SWITCHING_PROTOCOLS_101,
|
||||
OK_200,
|
||||
CREATED_201,
|
||||
ACCEPTED_202,
|
||||
NO_CONTENT_204,
|
||||
PARTIAL_CONTENT_206,
|
||||
MOVED_PERMANENTLY_301,
|
||||
FOUND_302,
|
||||
TEMPORARY_REDIRECT_307,
|
||||
PERMANENT_REDIRECT_308,
|
||||
BAD_REQUEST_400,
|
||||
UNAUTHORIZED_401,
|
||||
CREATED_201,
|
||||
FORBIDDEN_403,
|
||||
NOT_FOUND_404,
|
||||
METHOD_NOT_ALLOWED_405,
|
||||
TOO_MANY_REQUESTS_429,
|
||||
FOUND_302,
|
||||
INTERNAL_SERVER_ERROR_500,
|
||||
METHOD_NOT_ALLOWED_405,
|
||||
MOVED_PERMANENTLY_301,
|
||||
NO_CONTENT_204,
|
||||
NOT_FOUND_404,
|
||||
NOT_IMPLEMENTED_501,
|
||||
OK_200,
|
||||
PARTIAL_CONTENT_206,
|
||||
PERMANENT_REDIRECT_308,
|
||||
SERVICE_UNAVAILABLE_503,
|
||||
SWITCHING_PROTOCOLS_101,
|
||||
TEMPORARY_REDIRECT_307,
|
||||
TOO_MANY_REQUESTS_429,
|
||||
UNAUTHORIZED_401,
|
||||
Status,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
"""
|
||||
|
||||
try:
|
||||
from typing import Union, List
|
||||
from typing import List, Union
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
|
@ -40,15 +40,13 @@ class Token:
|
|||
return f"{self.prefix} {self._value}"
|
||||
|
||||
|
||||
class Bearer(Token): # pylint: disable=too-few-public-methods
|
||||
class Bearer(Token):
|
||||
"""Represents HTTP Bearer Token Authentication."""
|
||||
|
||||
prefix = "Bearer"
|
||||
|
||||
|
||||
def check_authentication(
|
||||
request: Request, auths: List[Union[Basic, Token, Bearer]]
|
||||
) -> bool:
|
||||
def check_authentication(request: Request, auths: List[Union[Basic, Token, Bearer]]) -> bool:
|
||||
"""
|
||||
Returns ``True`` if request is authorized by any of the authentications, ``False`` otherwise.
|
||||
|
||||
|
|
@ -65,9 +63,7 @@ def check_authentication(
|
|||
return any(auth_header == str(auth) for auth in auths)
|
||||
|
||||
|
||||
def require_authentication(
|
||||
request: Request, auths: List[Union[Basic, Token, Bearer]]
|
||||
) -> None:
|
||||
def require_authentication(request: Request, auths: List[Union[Basic, Token, Bearer]]) -> None:
|
||||
"""
|
||||
Checks if the request is authorized and raises ``AuthenticationError`` if not.
|
||||
|
||||
|
|
|
|||
|
|
@ -93,9 +93,7 @@ class Headers(_IFieldStorage):
|
|||
return default
|
||||
return header_value.split(";")[0].strip('" ')
|
||||
|
||||
def get_parameter(
|
||||
self, name: str, parameter: str, default: str = None
|
||||
) -> Union[str, None]:
|
||||
def get_parameter(self, name: str, parameter: str, default: str = None) -> Union[str, None]:
|
||||
"""
|
||||
Returns the value of the given parameter for the given header name, or default if not found.
|
||||
|
||||
|
|
@ -124,16 +122,12 @@ class Headers(_IFieldStorage):
|
|||
|
||||
def update(self, headers: Dict[str, str]):
|
||||
"""Updates the headers with the given dict."""
|
||||
return self._storage.update(
|
||||
{key.lower(): [value] for key, value in headers.items()}
|
||||
)
|
||||
return self._storage.update({key.lower(): [value] for key, value in headers.items()})
|
||||
|
||||
def copy(self):
|
||||
"""Returns a copy of the headers."""
|
||||
return Headers(
|
||||
"\r\n".join(
|
||||
f"{key}: {value}" for key in self.fields for value in self.get_list(key)
|
||||
)
|
||||
"\r\n".join(f"{key}: {value}" for key in self.fields for value in self.get_list(key))
|
||||
)
|
||||
|
||||
def __getitem__(self, name: str):
|
||||
|
|
|
|||
|
|
@ -8,11 +8,59 @@
|
|||
"""
|
||||
|
||||
try:
|
||||
from typing import List, Dict, Union, Any
|
||||
from typing import Any, Dict, List, Tuple, Union
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
class _ISocket:
|
||||
"""A class for typing necessary methods for a socket object."""
|
||||
|
||||
def accept(self) -> Tuple["_ISocket", Tuple[str, int]]: ...
|
||||
|
||||
def bind(self, address: Tuple[str, int]) -> None: ...
|
||||
|
||||
def setblocking(self, flag: bool) -> None: ...
|
||||
|
||||
def settimeout(self, value: "Union[float, None]") -> None: ...
|
||||
|
||||
def setsockopt(self, level: int, optname: int, value: int) -> None: ...
|
||||
|
||||
def listen(self, backlog: int) -> None: ...
|
||||
|
||||
def send(self, data: bytes) -> int: ...
|
||||
|
||||
def recv_into(self, buffer: memoryview, nbytes: int) -> int: ...
|
||||
|
||||
def close(self) -> None: ...
|
||||
|
||||
|
||||
class _ISocketPool:
|
||||
"""A class to typing necessary methods and properties for a socket pool object."""
|
||||
|
||||
AF_INET: int
|
||||
SO_REUSEADDR: int
|
||||
SOCK_STREAM: int
|
||||
SOL_SOCKET: int
|
||||
|
||||
def socket(
|
||||
self,
|
||||
family: int = ...,
|
||||
type: int = ...,
|
||||
proto: int = ...,
|
||||
) -> _ISocket: ...
|
||||
|
||||
def getaddrinfo(
|
||||
self,
|
||||
host: str,
|
||||
port: int,
|
||||
family: int = ...,
|
||||
type: int = ...,
|
||||
proto: int = ...,
|
||||
flags: int = ...,
|
||||
) -> Tuple[int, int, int, str, Tuple[str, int]]: ...
|
||||
|
||||
|
||||
class _IFieldStorage:
|
||||
"""Interface with shared methods for QueryParams, FormData and Headers."""
|
||||
|
||||
|
|
@ -62,7 +110,7 @@ class _IFieldStorage:
|
|||
return key in self._storage
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"{self.__class__.__name__}({repr(self._storage)})"
|
||||
return f"<{self.__class__.__name__} {repr(self._storage)}>"
|
||||
|
||||
|
||||
def _encode_html_entities(value: Union[str, None]) -> Union[str, None]:
|
||||
|
|
@ -81,9 +129,7 @@ def _encode_html_entities(value: Union[str, None]) -> Union[str, None]:
|
|||
|
||||
|
||||
class _IXSSSafeFieldStorage(_IFieldStorage):
|
||||
def get(
|
||||
self, field_name: str, default: Any = None, *, safe=True
|
||||
) -> Union[Any, None]:
|
||||
def get(self, field_name: str, default: Any = None, *, safe=True) -> Union[Any, None]:
|
||||
if safe:
|
||||
return _encode_html_entities(super().get(field_name, default))
|
||||
|
||||
|
|
@ -92,9 +138,7 @@ class _IXSSSafeFieldStorage(_IFieldStorage):
|
|||
|
||||
def get_list(self, field_name: str, *, safe=True) -> List[Any]:
|
||||
if safe:
|
||||
return [
|
||||
_encode_html_entities(value) for value in super().get_list(field_name)
|
||||
]
|
||||
return [_encode_html_entities(value) for value in super().get_list(field_name)]
|
||||
|
||||
_debug_warning_nonencoded_output()
|
||||
return super().get_list(field_name)
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@
|
|||
* Author(s): Michał Pokusa
|
||||
"""
|
||||
|
||||
|
||||
GET = "GET"
|
||||
|
||||
POST = "POST"
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
"""
|
||||
|
||||
try:
|
||||
from typing import List, Dict
|
||||
from typing import Dict, List
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
|
|
|||
|
|
@ -8,9 +8,7 @@
|
|||
"""
|
||||
|
||||
try:
|
||||
from typing import List, Dict, Tuple, Union, Any, TYPE_CHECKING
|
||||
from socket import socket
|
||||
from socketpool import SocketPool
|
||||
from typing import TYPE_CHECKING, Any, Dict, List, Tuple, Union
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .server import Server
|
||||
|
|
@ -20,8 +18,8 @@ except ImportError:
|
|||
import json
|
||||
|
||||
from .headers import Headers
|
||||
from .interfaces import _IFieldStorage, _IXSSSafeFieldStorage
|
||||
from .methods import POST, PUT, PATCH, DELETE
|
||||
from .interfaces import _IFieldStorage, _ISocket, _IXSSSafeFieldStorage
|
||||
from .methods import DELETE, PATCH, POST, PUT
|
||||
|
||||
|
||||
class QueryParams(_IXSSSafeFieldStorage):
|
||||
|
|
@ -56,9 +54,7 @@ class QueryParams(_IXSSSafeFieldStorage):
|
|||
def _add_field_value(self, field_name: str, value: str) -> None:
|
||||
super()._add_field_value(field_name, value)
|
||||
|
||||
def get(
|
||||
self, field_name: str, default: str = None, *, safe=True
|
||||
) -> Union[str, None]:
|
||||
def get(self, field_name: str, default: str = None, *, safe=True) -> Union[str, None]:
|
||||
return super().get(field_name, default, safe=safe)
|
||||
|
||||
def get_list(self, field_name: str, *, safe=True) -> List[str]:
|
||||
|
|
@ -94,9 +90,7 @@ class File:
|
|||
content: Union[str, bytes]
|
||||
"""Content of the file."""
|
||||
|
||||
def __init__(
|
||||
self, filename: str, content_type: str, content: Union[str, bytes]
|
||||
) -> None:
|
||||
def __init__(self, filename: str, content_type: str, content: Union[str, bytes]) -> None:
|
||||
self.filename = filename
|
||||
self.content_type = content_type
|
||||
self.content = content
|
||||
|
|
@ -114,11 +108,7 @@ class File:
|
|||
with open(file.filename, "wb") as f:
|
||||
f.write(file.content_bytes)
|
||||
"""
|
||||
return (
|
||||
self.content.encode("utf-8")
|
||||
if isinstance(self.content, str)
|
||||
else self.content
|
||||
)
|
||||
return self.content.encode("utf-8") if isinstance(self.content, str) else self.content
|
||||
|
||||
@property
|
||||
def size(self) -> int:
|
||||
|
|
@ -127,11 +117,11 @@ class File:
|
|||
|
||||
def __repr__(self) -> str:
|
||||
filename, content_type, size = (
|
||||
repr(self.filename),
|
||||
repr(self.content_type),
|
||||
repr(self.size),
|
||||
self.filename,
|
||||
self.content_type,
|
||||
self.size,
|
||||
)
|
||||
return f"{self.__class__.__name__}({filename=}, {content_type=}, {size=})"
|
||||
return f"<{self.__class__.__name__} {filename=}, {content_type=}, {size=}>"
|
||||
|
||||
|
||||
class Files(_IFieldStorage):
|
||||
|
|
@ -179,11 +169,11 @@ class FormData(_IXSSSafeFieldStorage):
|
|||
|
||||
@staticmethod
|
||||
def _check_is_supported_content_type(content_type: str) -> None:
|
||||
return content_type in (
|
||||
return content_type in {
|
||||
"application/x-www-form-urlencoded",
|
||||
"multipart/form-data",
|
||||
"text/plain",
|
||||
)
|
||||
}
|
||||
|
||||
def __init__(self, data: bytes, headers: Headers, *, debug: bool = False) -> None:
|
||||
self._storage = {}
|
||||
|
|
@ -233,9 +223,7 @@ class FormData(_IXSSSafeFieldStorage):
|
|||
# TODO: Other text content types (e.g. application/json) should be decoded as well and
|
||||
|
||||
if filename is not None:
|
||||
self.files._add_field_value( # pylint: disable=protected-access
|
||||
field_name, File(filename, content_type, value)
|
||||
)
|
||||
self.files._add_field_value(field_name, File(filename, content_type, value))
|
||||
else:
|
||||
self._add_field_value(field_name, value)
|
||||
|
||||
|
|
@ -260,10 +248,10 @@ class FormData(_IXSSSafeFieldStorage):
|
|||
|
||||
def __repr__(self) -> str:
|
||||
class_name = self.__class__.__name__
|
||||
return f"{class_name}({repr(self._storage)}, files={repr(self.files._storage)})"
|
||||
return f"<{class_name} {repr(self._storage)}, files={repr(self.files._storage)}>"
|
||||
|
||||
|
||||
class Request: # pylint: disable=too-many-instance-attributes
|
||||
class Request:
|
||||
"""
|
||||
Incoming request, constructed from raw incoming bytes.
|
||||
It is passed as first argument to all route handlers.
|
||||
|
|
@ -274,7 +262,7 @@ class Request: # pylint: disable=too-many-instance-attributes
|
|||
Server object that received the request.
|
||||
"""
|
||||
|
||||
connection: Union["SocketPool.Socket", "socket.socket"]
|
||||
connection: _ISocket
|
||||
"""
|
||||
Socket object used to send and receive data on the connection.
|
||||
"""
|
||||
|
|
@ -325,7 +313,7 @@ class Request: # pylint: disable=too-many-instance-attributes
|
|||
def __init__(
|
||||
self,
|
||||
server: "Server",
|
||||
connection: Union["SocketPool.Socket", "socket.socket"],
|
||||
connection: _ISocket,
|
||||
client_address: Tuple[str, int],
|
||||
raw_request: bytes = None,
|
||||
) -> None:
|
||||
|
|
@ -367,9 +355,7 @@ class Request: # pylint: disable=too-many-instance-attributes
|
|||
|
||||
return {
|
||||
name: value.strip('"')
|
||||
for name, value in [
|
||||
cookie.strip().split("=", 1) for cookie in cookie_header.split(";")
|
||||
]
|
||||
for name, value in [cookie.strip().split("=", 1) for cookie in cookie_header.split(";")]
|
||||
}
|
||||
|
||||
@property
|
||||
|
|
@ -443,7 +429,7 @@ class Request: # pylint: disable=too-many-instance-attributes
|
|||
"""
|
||||
return (
|
||||
json.loads(self.body)
|
||||
if (self.body and self.method in (POST, PUT, PATCH, DELETE))
|
||||
if (self.body and self.method in {POST, PUT, PATCH, DELETE})
|
||||
else None
|
||||
)
|
||||
|
||||
|
|
@ -467,9 +453,7 @@ class Request: # pylint: disable=too-many-instance-attributes
|
|||
) -> Tuple[str, str, QueryParams, str, Headers]:
|
||||
"""Parse HTTP Start line to method, path, query_params and http_version."""
|
||||
|
||||
start_line, headers_string = (
|
||||
header_bytes.decode("utf-8").strip().split("\r\n", 1)
|
||||
)
|
||||
start_line, headers_string = header_bytes.decode("utf-8").strip().split("\r\n", 1)
|
||||
|
||||
method, path, http_version = start_line.strip().split()
|
||||
|
||||
|
|
@ -481,6 +465,10 @@ class Request: # pylint: disable=too-many-instance-attributes
|
|||
|
||||
return method, path, query_params, http_version, headers
|
||||
|
||||
def __repr__(self) -> str:
|
||||
path = self.path + (f"?{self.query_params}" if self.query_params else "")
|
||||
return f'<{self.__class__.__name__} "{self.method} {path}">'
|
||||
|
||||
|
||||
def _debug_unsupported_form_content_type(content_type: str) -> None:
|
||||
"""Warns when an unsupported form content type is used."""
|
||||
|
|
|
|||
|
|
@ -8,16 +8,14 @@
|
|||
"""
|
||||
|
||||
try:
|
||||
from typing import Optional, Dict, Union, Tuple, Generator, Any
|
||||
from socket import socket
|
||||
from socketpool import SocketPool
|
||||
from typing import Any, Dict, Generator, Optional, Tuple, Union
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
import os
|
||||
import json
|
||||
import os
|
||||
from binascii import b2a_base64
|
||||
from errno import EAGAIN, ECONNRESET, ETIMEDOUT, ENOTCONN
|
||||
from errno import EAGAIN, ECONNRESET, ENOTCONN, ETIMEDOUT
|
||||
|
||||
try:
|
||||
try:
|
||||
|
|
@ -35,21 +33,22 @@ from .exceptions import (
|
|||
FileNotExistsError,
|
||||
ParentDirectoryReferenceError,
|
||||
)
|
||||
from .headers import Headers
|
||||
from .interfaces import _ISocket
|
||||
from .mime_types import MIMETypes
|
||||
from .request import Request
|
||||
from .status import (
|
||||
Status,
|
||||
SWITCHING_PROTOCOLS_101,
|
||||
OK_200,
|
||||
MOVED_PERMANENTLY_301,
|
||||
FOUND_302,
|
||||
TEMPORARY_REDIRECT_307,
|
||||
MOVED_PERMANENTLY_301,
|
||||
OK_200,
|
||||
PERMANENT_REDIRECT_308,
|
||||
SWITCHING_PROTOCOLS_101,
|
||||
TEMPORARY_REDIRECT_307,
|
||||
Status,
|
||||
)
|
||||
from .headers import Headers
|
||||
|
||||
|
||||
class Response: # pylint: disable=too-few-public-methods
|
||||
class Response:
|
||||
"""
|
||||
Response to a given `Request`. Use in `Server.route` handler functions.
|
||||
|
||||
|
|
@ -63,7 +62,7 @@ class Response: # pylint: disable=too-few-public-methods
|
|||
return Response(request, body='Some content', content_type="text/plain")
|
||||
"""
|
||||
|
||||
def __init__( # pylint: disable=too-many-arguments
|
||||
def __init__(
|
||||
self,
|
||||
request: Request,
|
||||
body: Union[str, bytes] = "",
|
||||
|
|
@ -85,9 +84,7 @@ class Response: # pylint: disable=too-few-public-methods
|
|||
self._request = request
|
||||
self._body = body
|
||||
self._status = status if isinstance(status, Status) else Status(*status)
|
||||
self._headers = (
|
||||
headers.copy() if isinstance(headers, Headers) else Headers(headers)
|
||||
)
|
||||
self._headers = headers.copy() if isinstance(headers, Headers) else Headers(headers)
|
||||
self._cookies = cookies.copy() if cookies else {}
|
||||
self._content_type = content_type
|
||||
self._size = 0
|
||||
|
|
@ -99,13 +96,9 @@ class Response: # pylint: disable=too-few-public-methods
|
|||
) -> None:
|
||||
headers = self._headers.copy()
|
||||
|
||||
response_message_header = (
|
||||
f"HTTP/1.1 {self._status.code} {self._status.text}\r\n"
|
||||
)
|
||||
response_message_header = f"HTTP/1.1 {self._status.code} {self._status.text}\r\n"
|
||||
|
||||
headers.setdefault(
|
||||
"Content-Type", content_type or self._content_type or MIMETypes.DEFAULT
|
||||
)
|
||||
headers.setdefault("Content-Type", content_type or self._content_type or MIMETypes.DEFAULT)
|
||||
headers.setdefault("Content-Length", content_length)
|
||||
headers.setdefault("Connection", "close")
|
||||
|
||||
|
|
@ -117,14 +110,10 @@ class Response: # pylint: disable=too-few-public-methods
|
|||
response_message_header += f"{header}: {value}\r\n"
|
||||
response_message_header += "\r\n"
|
||||
|
||||
self._send_bytes(
|
||||
self._request.connection, response_message_header.encode("utf-8")
|
||||
)
|
||||
self._send_bytes(self._request.connection, response_message_header.encode("utf-8"))
|
||||
|
||||
def _send(self) -> None:
|
||||
encoded_body = (
|
||||
self._body.encode("utf-8") if isinstance(self._body, str) else self._body
|
||||
)
|
||||
encoded_body = self._body.encode("utf-8") if isinstance(self._body, str) else self._body
|
||||
|
||||
self._send_headers(len(encoded_body), self._content_type)
|
||||
self._send_bytes(self._request.connection, encoded_body)
|
||||
|
|
@ -132,7 +121,7 @@ class Response: # pylint: disable=too-few-public-methods
|
|||
|
||||
def _send_bytes(
|
||||
self,
|
||||
conn: Union["SocketPool.Socket", "socket.socket"],
|
||||
conn: _ISocket,
|
||||
buffer: Union[bytes, bytearray, memoryview],
|
||||
):
|
||||
bytes_sent: int = 0
|
||||
|
|
@ -156,7 +145,7 @@ class Response: # pylint: disable=too-few-public-methods
|
|||
pass
|
||||
|
||||
|
||||
class FileResponse(Response): # pylint: disable=too-few-public-methods
|
||||
class FileResponse(Response):
|
||||
"""
|
||||
Specialized version of `Response` class for sending files.
|
||||
|
||||
|
|
@ -174,7 +163,7 @@ class FileResponse(Response): # pylint: disable=too-few-public-methods
|
|||
return FileResponse(request, filename='index.html', root_path='/www')
|
||||
"""
|
||||
|
||||
def __init__( # pylint: disable=too-many-arguments
|
||||
def __init__(
|
||||
self,
|
||||
request: Request,
|
||||
filename: str = "index.html",
|
||||
|
|
@ -217,6 +206,10 @@ class FileResponse(Response): # pylint: disable=too-few-public-methods
|
|||
)
|
||||
self._filename = filename + "index.html" if filename.endswith("/") else filename
|
||||
self._root_path = root_path or self._request.server.root_path
|
||||
|
||||
if self._root_path is None:
|
||||
raise ValueError("root_path must be provided in Server or in FileResponse")
|
||||
|
||||
self._full_file_path = self._combine_path(self._root_path, self._filename)
|
||||
self._content_type = content_type or MIMETypes.get_for_filename(self._filename)
|
||||
self._file_length = self._get_file_length(self._full_file_path)
|
||||
|
|
@ -240,7 +233,7 @@ class FileResponse(Response): # pylint: disable=too-few-public-methods
|
|||
"""
|
||||
|
||||
# Check for backslashes
|
||||
if "\\" in file_path: # pylint: disable=anomalous-backslash-in-string
|
||||
if "\\" in file_path:
|
||||
raise BackslashInPathError(file_path)
|
||||
|
||||
# Check each component of the path for parent directory references
|
||||
|
|
@ -273,7 +266,7 @@ class FileResponse(Response): # pylint: disable=too-few-public-methods
|
|||
assert (st_mode & 0o170000) == 0o100000 # Check if it is a regular file
|
||||
return st_size
|
||||
except (OSError, AssertionError):
|
||||
raise FileNotExistsError(file_path) # pylint: disable=raise-missing-from
|
||||
raise FileNotExistsError(file_path)
|
||||
|
||||
def _send(self) -> None:
|
||||
self._send_headers(self._file_length, self._content_type)
|
||||
|
|
@ -285,7 +278,7 @@ class FileResponse(Response): # pylint: disable=too-few-public-methods
|
|||
self._close_connection()
|
||||
|
||||
|
||||
class ChunkedResponse(Response): # pylint: disable=too-few-public-methods
|
||||
class ChunkedResponse(Response):
|
||||
"""
|
||||
Specialized version of `Response` class for sending data using chunked transfer encoding.
|
||||
|
||||
|
|
@ -305,7 +298,7 @@ class ChunkedResponse(Response): # pylint: disable=too-few-public-methods
|
|||
return ChunkedResponse(request, body, content_type="text/plain")
|
||||
"""
|
||||
|
||||
def __init__( # pylint: disable=too-many-arguments
|
||||
def __init__(
|
||||
self,
|
||||
request: Request,
|
||||
body: Generator[Union[str, bytes], Any, Any],
|
||||
|
|
@ -353,7 +346,7 @@ class ChunkedResponse(Response): # pylint: disable=too-few-public-methods
|
|||
self._close_connection()
|
||||
|
||||
|
||||
class JSONResponse(Response): # pylint: disable=too-few-public-methods
|
||||
class JSONResponse(Response):
|
||||
"""
|
||||
Specialized version of `Response` class for sending JSON data.
|
||||
|
||||
|
|
@ -400,7 +393,7 @@ class JSONResponse(Response): # pylint: disable=too-few-public-methods
|
|||
self._close_connection()
|
||||
|
||||
|
||||
class Redirect(Response): # pylint: disable=too-few-public-methods
|
||||
class Redirect(Response):
|
||||
"""
|
||||
Specialized version of `Response` class for redirecting to another URL.
|
||||
|
||||
|
|
@ -445,9 +438,7 @@ class Redirect(Response): # pylint: disable=too-few-public-methods
|
|||
"""
|
||||
|
||||
if status is not None and (permanent or preserve_method):
|
||||
raise ValueError(
|
||||
"Cannot specify both status and permanent/preserve_method argument"
|
||||
)
|
||||
raise ValueError("Cannot specify both status and permanent/preserve_method argument")
|
||||
|
||||
if status is None:
|
||||
if preserve_method:
|
||||
|
|
@ -463,7 +454,7 @@ class Redirect(Response): # pylint: disable=too-few-public-methods
|
|||
self._close_connection()
|
||||
|
||||
|
||||
class SSEResponse(Response): # pylint: disable=too-few-public-methods
|
||||
class SSEResponse(Response):
|
||||
"""
|
||||
Specialized version of `Response` class for sending Server-Sent Events.
|
||||
|
||||
|
|
@ -497,7 +488,7 @@ class SSEResponse(Response): # pylint: disable=too-few-public-methods
|
|||
sse.close()
|
||||
"""
|
||||
|
||||
def __init__( # pylint: disable=too-many-arguments
|
||||
def __init__(
|
||||
self,
|
||||
request: Request,
|
||||
headers: Union[Headers, Dict[str, str]] = None,
|
||||
|
|
@ -520,11 +511,11 @@ class SSEResponse(Response): # pylint: disable=too-few-public-methods
|
|||
def _send(self) -> None:
|
||||
self._send_headers()
|
||||
|
||||
def send_event( # pylint: disable=too-many-arguments
|
||||
def send_event(
|
||||
self,
|
||||
data: str,
|
||||
event: str = None,
|
||||
id: int = None, # pylint: disable=redefined-builtin,invalid-name
|
||||
id: int = None,
|
||||
retry: int = None,
|
||||
custom_fields: Dict[str, str] = None,
|
||||
) -> None:
|
||||
|
|
@ -561,7 +552,7 @@ class SSEResponse(Response): # pylint: disable=too-few-public-methods
|
|||
self._close_connection()
|
||||
|
||||
|
||||
class Websocket(Response): # pylint: disable=too-few-public-methods
|
||||
class Websocket(Response):
|
||||
"""
|
||||
Specialized version of `Response` class for creating a websocket connection.
|
||||
|
||||
|
|
@ -632,7 +623,7 @@ class Websocket(Response): # pylint: disable=too-few-public-methods
|
|||
|
||||
return b2a_base64(response_key.digest()).strip().decode()
|
||||
|
||||
def __init__( # pylint: disable=too-many-arguments
|
||||
def __init__(
|
||||
self,
|
||||
request: Request,
|
||||
headers: Union[Headers, Dict[str, str]] = None,
|
||||
|
|
@ -690,7 +681,7 @@ class Websocket(Response): # pylint: disable=too-few-public-methods
|
|||
if fin != Websocket.FIN and opcode == Websocket.CONT:
|
||||
return Websocket.CONT, None
|
||||
|
||||
payload = bytes()
|
||||
payload = b""
|
||||
if fin == Websocket.FIN and opcode == Websocket.CLOSE:
|
||||
return Websocket.CLOSE, payload
|
||||
|
||||
|
|
@ -708,7 +699,7 @@ class Websocket(Response): # pylint: disable=too-few-public-methods
|
|||
length -= min(payload_length, length)
|
||||
|
||||
if has_mask:
|
||||
payload = bytes(x ^ mask[i % 4] for i, x in enumerate(payload))
|
||||
payload = bytes(byte ^ mask[idx % 4] for idx, byte in enumerate(payload))
|
||||
|
||||
return opcode, payload
|
||||
|
||||
|
|
@ -743,9 +734,7 @@ class Websocket(Response): # pylint: disable=too-few-public-methods
|
|||
if self.closed:
|
||||
if fail_silently:
|
||||
return None
|
||||
raise RuntimeError(
|
||||
"Websocket connection is closed, cannot receive messages"
|
||||
)
|
||||
raise RuntimeError("Websocket connection is closed, cannot receive messages")
|
||||
|
||||
try:
|
||||
opcode, payload = self._read_frame()
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
"""
|
||||
|
||||
try:
|
||||
from typing import Callable, Iterable, Union, Tuple, Literal, Dict, TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, Callable, Dict, Iterable, Literal, Tuple, Union
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .response import Response
|
||||
|
|
@ -52,9 +52,7 @@ class Route:
|
|||
self._validate_path(path, append_slash)
|
||||
|
||||
self.path = path
|
||||
self.methods = (
|
||||
set(methods) if isinstance(methods, (set, list, tuple)) else set([methods])
|
||||
)
|
||||
self.methods = set(methods) if isinstance(methods, (set, list, tuple)) else set([methods])
|
||||
self.handler = handler
|
||||
self.parameters_names = [
|
||||
name[1:-1] for name in re.compile(r"/[^<>]*/?").split(path) if name != ""
|
||||
|
|
@ -136,11 +134,11 @@ class Route:
|
|||
return True, dict(zip(self.parameters_names, url_parameters_values))
|
||||
|
||||
def __repr__(self) -> str:
|
||||
path = repr(self.path)
|
||||
methods = repr(self.methods)
|
||||
handler = repr(self.handler)
|
||||
path = self.path
|
||||
methods = self.methods
|
||||
handler = self.handler
|
||||
|
||||
return f"Route({path=}, {methods=}, {handler=})"
|
||||
return f"<Route {path=}, {methods=}, {handler=}>"
|
||||
|
||||
|
||||
def as_route(
|
||||
|
|
|
|||
|
|
@ -8,9 +8,7 @@
|
|||
"""
|
||||
|
||||
try:
|
||||
from typing import Callable, Protocol, Union, List, Tuple, Dict, Iterable
|
||||
from socket import socket
|
||||
from socketpool import SocketPool
|
||||
from typing import Callable, Dict, Iterable, List, Tuple, Union
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
|
@ -19,20 +17,36 @@ from sys import implementation
|
|||
from time import monotonic, sleep
|
||||
from traceback import print_exception
|
||||
|
||||
from .authentication import Basic, Token, Bearer, require_authentication
|
||||
from .authentication import Basic, Bearer, Token, require_authentication
|
||||
from .exceptions import (
|
||||
ServerStoppedError,
|
||||
AuthenticationError,
|
||||
FileNotExistsError,
|
||||
InvalidPathError,
|
||||
ServerStoppedError,
|
||||
ServingFilesDisabledError,
|
||||
)
|
||||
from .headers import Headers
|
||||
from .interfaces import _ISocket, _ISocketPool
|
||||
from .methods import GET, HEAD
|
||||
from .request import Request
|
||||
from .response import Response, FileResponse
|
||||
from .response import FileResponse, Response
|
||||
from .route import Route
|
||||
from .status import BAD_REQUEST_400, UNAUTHORIZED_401, FORBIDDEN_403, NOT_FOUND_404
|
||||
from .status import BAD_REQUEST_400, FORBIDDEN_403, NOT_FOUND_404, UNAUTHORIZED_401
|
||||
|
||||
try:
|
||||
from ssl import SSLContext, create_default_context
|
||||
|
||||
try: # ssl imports for C python
|
||||
from ssl import (
|
||||
CERT_NONE,
|
||||
Purpose,
|
||||
SSLError,
|
||||
)
|
||||
except ImportError:
|
||||
pass
|
||||
SSL_AVAILABLE = True
|
||||
except ImportError:
|
||||
SSL_AVAILABLE = False
|
||||
|
||||
|
||||
NO_REQUEST = "no_request"
|
||||
|
|
@ -40,8 +54,11 @@ CONNECTION_TIMED_OUT = "connection_timed_out"
|
|||
REQUEST_HANDLED_NO_RESPONSE = "request_handled_no_response"
|
||||
REQUEST_HANDLED_RESPONSE_SENT = "request_handled_response_sent"
|
||||
|
||||
# CircuitPython does not have these error codes
|
||||
MBEDTLS_ERR_SSL_FATAL_ALERT_MESSAGE = -30592
|
||||
|
||||
class Server: # pylint: disable=too-many-instance-attributes
|
||||
|
||||
class Server:
|
||||
"""A basic socket-based HTTP server."""
|
||||
|
||||
host: str
|
||||
|
|
@ -53,8 +70,50 @@ class Server: # pylint: disable=too-many-instance-attributes
|
|||
root_path: str
|
||||
"""Root directory to serve files from. ``None`` if serving files is disabled."""
|
||||
|
||||
@staticmethod
|
||||
def _validate_https_cert_provided(
|
||||
certfile: Union[str, None], keyfile: Union[str, None]
|
||||
) -> None:
|
||||
if certfile is None or keyfile is None:
|
||||
raise ValueError("Both certfile and keyfile must be specified for HTTPS")
|
||||
|
||||
@staticmethod
|
||||
def _create_circuitpython_ssl_context(certfile: str, keyfile: str) -> SSLContext:
|
||||
ssl_context = create_default_context()
|
||||
|
||||
ssl_context.load_verify_locations(cadata="")
|
||||
ssl_context.load_cert_chain(certfile, keyfile)
|
||||
|
||||
return ssl_context
|
||||
|
||||
@staticmethod
|
||||
def _create_cpython_ssl_context(certfile: str, keyfile: str) -> SSLContext:
|
||||
ssl_context = create_default_context(purpose=Purpose.CLIENT_AUTH)
|
||||
|
||||
ssl_context.load_cert_chain(certfile, keyfile)
|
||||
|
||||
ssl_context.verify_mode = CERT_NONE
|
||||
ssl_context.check_hostname = False
|
||||
|
||||
return ssl_context
|
||||
|
||||
@classmethod
|
||||
def _create_ssl_context(cls, certfile: str, keyfile: str) -> SSLContext:
|
||||
return (
|
||||
cls._create_circuitpython_ssl_context(certfile, keyfile)
|
||||
if implementation.name == "circuitpython"
|
||||
else cls._create_cpython_ssl_context(certfile, keyfile)
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self, socket_source: Protocol, root_path: str = None, *, debug: bool = False
|
||||
self,
|
||||
socket_source: _ISocketPool,
|
||||
root_path: str = None,
|
||||
*,
|
||||
https: bool = False,
|
||||
certfile: str = None,
|
||||
keyfile: str = None,
|
||||
debug: bool = False,
|
||||
) -> None:
|
||||
"""Create a server, and get it ready to run.
|
||||
|
||||
|
|
@ -62,17 +121,33 @@ class Server: # pylint: disable=too-many-instance-attributes
|
|||
in CircuitPython or the `socket` module in CPython.
|
||||
:param str root_path: Root directory to serve files from
|
||||
:param bool debug: Enables debug messages useful during development
|
||||
:param bool https: If True, the server will use HTTPS
|
||||
:param str certfile: Path to the certificate file, required if ``https`` is True
|
||||
:param str keyfile: Path to the private key file, required if ``https`` is True
|
||||
"""
|
||||
self._auths = []
|
||||
self._buffer = bytearray(1024)
|
||||
self._timeout = 1
|
||||
|
||||
self._auths = []
|
||||
self._routes: "List[Route]" = []
|
||||
self.headers = Headers()
|
||||
|
||||
self._socket_source = socket_source
|
||||
self._sock = None
|
||||
self.headers = Headers()
|
||||
|
||||
self.host, self.port = None, None
|
||||
self.root_path = root_path
|
||||
if root_path in ["", "/"] and debug:
|
||||
self.https = https
|
||||
|
||||
if https:
|
||||
if not SSL_AVAILABLE:
|
||||
raise NotImplementedError("SSL not available on this platform")
|
||||
self._validate_https_cert_provided(certfile, keyfile)
|
||||
self._ssl_context = self._create_ssl_context(certfile, keyfile)
|
||||
else:
|
||||
self._ssl_context = None
|
||||
|
||||
if root_path in {"", "/"} and debug:
|
||||
_debug_warning_exposed_files(root_path)
|
||||
self.stopped = True
|
||||
|
||||
|
|
@ -172,7 +247,7 @@ class Server: # pylint: disable=too-many-instance-attributes
|
|||
raise RuntimeError(f"Cannot start server on {host}:{port}") from error
|
||||
|
||||
def serve_forever(
|
||||
self, host: str, port: int = 80, *, poll_interval: float = 0.1
|
||||
self, host: str = "0.0.0.0", port: int = 5000, *, poll_interval: float = 0.1
|
||||
) -> None:
|
||||
"""
|
||||
Wait for HTTP requests at the given host and port. Does not return.
|
||||
|
|
@ -192,18 +267,32 @@ class Server: # pylint: disable=too-many-instance-attributes
|
|||
except KeyboardInterrupt: # Exit on Ctrl-C e.g. during development
|
||||
self.stop()
|
||||
return
|
||||
except Exception: # pylint: disable=broad-except
|
||||
except Exception:
|
||||
pass # Ignore exceptions in handler function
|
||||
|
||||
def _set_socket_level_to_reuse_address(self) -> None:
|
||||
"""
|
||||
Only for CPython, prevents "Address already in use" error when restarting the server.
|
||||
"""
|
||||
self._sock.setsockopt(
|
||||
self._socket_source.SOL_SOCKET, self._socket_source.SO_REUSEADDR, 1
|
||||
)
|
||||
@staticmethod
|
||||
def _create_server_socket(
|
||||
socket_source: _ISocketPool,
|
||||
ssl_context: "SSLContext | None",
|
||||
host: str,
|
||||
port: int,
|
||||
) -> _ISocket:
|
||||
sock = socket_source.socket(socket_source.AF_INET, socket_source.SOCK_STREAM)
|
||||
|
||||
def start(self, host: str, port: int = 80) -> None:
|
||||
# TODO: Temporary backwards compatibility, remove after CircuitPython 9.0.0 release
|
||||
if implementation.version >= (9,) or implementation.name != "circuitpython":
|
||||
sock.setsockopt(socket_source.SOL_SOCKET, socket_source.SO_REUSEADDR, 1)
|
||||
|
||||
if ssl_context is not None:
|
||||
sock = ssl_context.wrap_socket(sock, server_side=True)
|
||||
|
||||
sock.bind((host, port))
|
||||
sock.listen(10)
|
||||
sock.setblocking(False) # Non-blocking socket
|
||||
|
||||
return sock
|
||||
|
||||
def start(self, host: str = "0.0.0.0", port: int = 5000) -> None:
|
||||
"""
|
||||
Start the HTTP server at the given host and port. Requires calling
|
||||
``.poll()`` in a while loop to handle incoming requests.
|
||||
|
|
@ -216,16 +305,7 @@ class Server: # pylint: disable=too-many-instance-attributes
|
|||
self.host, self.port = host, port
|
||||
|
||||
self.stopped = False
|
||||
self._sock = self._socket_source.socket(
|
||||
self._socket_source.AF_INET, self._socket_source.SOCK_STREAM
|
||||
)
|
||||
|
||||
if implementation.name != "circuitpython":
|
||||
self._set_socket_level_to_reuse_address()
|
||||
|
||||
self._sock.bind((host, port))
|
||||
self._sock.listen(10)
|
||||
self._sock.setblocking(False) # Non-blocking socket
|
||||
self._sock = self._create_server_socket(self._socket_source, self._ssl_context, host, port)
|
||||
|
||||
if self.debug:
|
||||
_debug_started_server(self)
|
||||
|
|
@ -244,11 +324,9 @@ class Server: # pylint: disable=too-many-instance-attributes
|
|||
if self.debug:
|
||||
_debug_stopped_server(self)
|
||||
|
||||
def _receive_header_bytes(
|
||||
self, sock: Union["SocketPool.Socket", "socket.socket"]
|
||||
) -> bytes:
|
||||
def _receive_header_bytes(self, sock: _ISocket) -> bytes:
|
||||
"""Receive bytes until a empty line is received."""
|
||||
received_bytes = bytes()
|
||||
received_bytes = b""
|
||||
while b"\r\n\r\n" not in received_bytes:
|
||||
try:
|
||||
length = sock.recv_into(self._buffer, len(self._buffer))
|
||||
|
|
@ -263,7 +341,7 @@ class Server: # pylint: disable=too-many-instance-attributes
|
|||
|
||||
def _receive_body_bytes(
|
||||
self,
|
||||
sock: Union["SocketPool.Socket", "socket.socket"],
|
||||
sock: _ISocket,
|
||||
received_body_bytes: bytes,
|
||||
content_length: int,
|
||||
) -> bytes:
|
||||
|
|
@ -282,7 +360,7 @@ class Server: # pylint: disable=too-many-instance-attributes
|
|||
|
||||
def _receive_request(
|
||||
self,
|
||||
sock: Union["SocketPool.Socket", "socket.socket"],
|
||||
sock: _ISocket,
|
||||
client_address: Tuple[str, int],
|
||||
) -> Request:
|
||||
"""Receive bytes from socket until the whole request is received."""
|
||||
|
|
@ -300,15 +378,11 @@ class Server: # pylint: disable=too-many-instance-attributes
|
|||
received_body_bytes = request.body
|
||||
|
||||
# Receiving remaining body bytes
|
||||
request.body = self._receive_body_bytes(
|
||||
sock, received_body_bytes, content_length
|
||||
)
|
||||
request.body = self._receive_body_bytes(sock, received_body_bytes, content_length)
|
||||
|
||||
return request
|
||||
|
||||
def _find_handler( # pylint: disable=cell-var-from-loop
|
||||
self, method: str, path: str
|
||||
) -> Union[Callable[..., "Response"], None]:
|
||||
def _find_handler(self, method: str, path: str) -> Union[Callable[..., "Response"], None]:
|
||||
"""
|
||||
Finds a handler for a given route.
|
||||
|
||||
|
|
@ -352,7 +426,7 @@ class Server: # pylint: disable=too-many-instance-attributes
|
|||
raise ServingFilesDisabledError
|
||||
|
||||
# Method is GET or HEAD, try to serve a file from the filesystem.
|
||||
if request.method in (GET, HEAD):
|
||||
if request.method in {GET, HEAD}:
|
||||
return FileResponse(
|
||||
request,
|
||||
filename=request.path,
|
||||
|
|
@ -384,11 +458,11 @@ class Server: # pylint: disable=too-many-instance-attributes
|
|||
|
||||
def _set_default_server_headers(self, response: Response) -> None:
|
||||
for name, value in self.headers.items():
|
||||
response._headers.setdefault( # pylint: disable=protected-access
|
||||
name, value
|
||||
)
|
||||
response._headers.setdefault(name, value)
|
||||
|
||||
def poll(self) -> str:
|
||||
def poll(
|
||||
self,
|
||||
) -> str:
|
||||
"""
|
||||
Call this method inside your main loop to get the server to check for new incoming client
|
||||
requests. When a request comes in, it will be handled by the handler function.
|
||||
|
|
@ -401,11 +475,12 @@ class Server: # pylint: disable=too-many-instance-attributes
|
|||
|
||||
conn = None
|
||||
try:
|
||||
if self.debug:
|
||||
_debug_start_time = monotonic()
|
||||
|
||||
conn, client_address = self._sock.accept()
|
||||
conn.settimeout(self._timeout)
|
||||
|
||||
_debug_start_time = monotonic()
|
||||
|
||||
# Receive the whole request
|
||||
if (request := self._receive_request(conn, client_address)) is None:
|
||||
conn.close()
|
||||
|
|
@ -424,16 +499,15 @@ class Server: # pylint: disable=too-many-instance-attributes
|
|||
self._set_default_server_headers(response)
|
||||
|
||||
# Send the response
|
||||
response._send() # pylint: disable=protected-access
|
||||
|
||||
_debug_end_time = monotonic()
|
||||
response._send()
|
||||
|
||||
if self.debug:
|
||||
_debug_end_time = monotonic()
|
||||
_debug_response_sent(response, _debug_end_time - _debug_start_time)
|
||||
|
||||
return REQUEST_HANDLED_RESPONSE_SENT
|
||||
|
||||
except Exception as error: # pylint: disable=broad-except
|
||||
except Exception as error:
|
||||
if isinstance(error, OSError):
|
||||
# There is no data available right now, try again later.
|
||||
if error.errno == EAGAIN:
|
||||
|
|
@ -441,6 +515,15 @@ class Server: # pylint: disable=too-many-instance-attributes
|
|||
# Connection reset by peer, try again later.
|
||||
if error.errno == ECONNRESET:
|
||||
return NO_REQUEST
|
||||
# Handshake failed, try again later.
|
||||
if error.errno == MBEDTLS_ERR_SSL_FATAL_ALERT_MESSAGE:
|
||||
return NO_REQUEST
|
||||
|
||||
# CPython specific SSL related errors
|
||||
if implementation.name != "circuitpython" and isinstance(error, SSLError):
|
||||
# Ignore unknown SSL certificate errors
|
||||
if getattr(error, "reason", None) == "SSLV3_ALERT_CERTIFICATE_UNKNOWN":
|
||||
return NO_REQUEST
|
||||
|
||||
if self.debug:
|
||||
_debug_exception_in_handler(error)
|
||||
|
|
@ -530,26 +613,33 @@ class Server: # pylint: disable=too-many-instance-attributes
|
|||
else:
|
||||
raise ValueError("Server.socket_timeout must be a positive numeric value.")
|
||||
|
||||
def __repr__(self) -> str:
|
||||
host = self.host
|
||||
port = self.port
|
||||
root_path = self.root_path
|
||||
|
||||
return f"<Server {host=}, {port=}, {root_path=}>"
|
||||
|
||||
|
||||
def _debug_warning_exposed_files(root_path: str):
|
||||
"""Warns about exposing all files on the device."""
|
||||
print(
|
||||
f"WARNING: Setting root_path to '{root_path}' will expose all files on your device through"
|
||||
" the webserver, including potentially sensitive files like settings.toml or secrets.py. "
|
||||
f"WARNING: Setting root_path to '{root_path}' will expose all files on your device "
|
||||
"through the webserver, including potentially sensitive files like settings.toml. "
|
||||
"Consider making a sub-directory on your device and using that for your root_path instead."
|
||||
)
|
||||
|
||||
|
||||
def _debug_started_server(server: "Server"):
|
||||
"""Prints a message when the server starts."""
|
||||
scheme = "https" if server.https else "http"
|
||||
host, port = server.host, server.port
|
||||
|
||||
print(f"Started development server on http://{host}:{port}")
|
||||
print(f"Started development server on {scheme}://{host}:{port}")
|
||||
|
||||
|
||||
def _debug_response_sent(response: "Response", time_elapsed: float):
|
||||
"""Prints a message after a response is sent."""
|
||||
# pylint: disable=protected-access
|
||||
client_ip = response._request.client_address[0]
|
||||
method = response._request.method
|
||||
query_params = response._request.query_params
|
||||
|
|
@ -564,7 +654,7 @@ def _debug_response_sent(response: "Response", time_elapsed: float):
|
|||
)
|
||||
|
||||
|
||||
def _debug_stopped_server(server: "Server"): # pylint: disable=unused-argument
|
||||
def _debug_stopped_server(server: "Server"):
|
||||
"""Prints a message after the server stops."""
|
||||
print("Stopped development server")
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
"""
|
||||
|
||||
|
||||
class Status: # pylint: disable=too-few-public-methods
|
||||
class Status:
|
||||
"""HTTP status code."""
|
||||
|
||||
def __init__(self, code: int, text: str):
|
||||
|
|
@ -21,14 +21,17 @@ class Status: # pylint: disable=too-few-public-methods
|
|||
self.code = code
|
||||
self.text = text
|
||||
|
||||
def __repr__(self):
|
||||
return f'Status({self.code}, "{self.text}")'
|
||||
def __eq__(self, other: "Status"):
|
||||
return self.code == other.code and self.text == other.text
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.code} {self.text}"
|
||||
|
||||
def __eq__(self, other: "Status"):
|
||||
return self.code == other.code and self.text == other.text
|
||||
def __repr__(self):
|
||||
code = self.code
|
||||
text = self.text
|
||||
|
||||
return f'<Status {code}, "{text}">'
|
||||
|
||||
|
||||
SWITCHING_PROTOCOLS_101 = Status(101, "Switching Protocols")
|
||||
|
|
|
|||
|
|
@ -4,6 +4,9 @@
|
|||
.. If your library file(s) are nested in a directory (e.g. /adafruit_foo/foo.py)
|
||||
.. use this format as the module name: "adafruit_foo.foo"
|
||||
|
||||
API Reference
|
||||
#############
|
||||
|
||||
.. automodule:: adafruit_httpserver
|
||||
:members:
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,10 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
# SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
import datetime
|
||||
import os
|
||||
import sys
|
||||
import datetime
|
||||
|
||||
sys.path.insert(0, os.path.abspath(".."))
|
||||
|
||||
|
|
@ -54,9 +52,7 @@ project = "Adafruit CircuitPython HTTPServer Library"
|
|||
creation_year = "2022"
|
||||
current_year = str(datetime.datetime.now().year)
|
||||
year_duration = (
|
||||
current_year
|
||||
if current_year == creation_year
|
||||
else creation_year + " - " + current_year
|
||||
current_year if current_year == creation_year else creation_year + " - " + current_year
|
||||
)
|
||||
copyright = year_duration + " Dan Halbert"
|
||||
author = "Dan Halbert"
|
||||
|
|
@ -116,7 +112,6 @@ napoleon_numpy_docstring = False
|
|||
import sphinx_rtd_theme
|
||||
|
||||
html_theme = "sphinx_rtd_theme"
|
||||
html_theme_path = [sphinx_rtd_theme.get_html_theme_path(), "."]
|
||||
|
||||
# Add any paths that contain custom static files (such as style sheets) here,
|
||||
# relative to this directory. They are copied after the builtin static files,
|
||||
|
|
|
|||
|
|
@ -1,47 +1,21 @@
|
|||
Simple Test
|
||||
-----------
|
||||
.. note::
|
||||
All examples in this document are using ``Server`` in ``debug`` mode.
|
||||
This mode is useful for development, but it is not recommended to use it in production.
|
||||
More about Debug mode at the end of Examples section.
|
||||
|
||||
**All examples in this document are using** ``Server`` **in** ``debug`` **mode.**
|
||||
**This mode is useful for development, but it is not recommended to use it in production.**
|
||||
**More about Debug mode at the end of Examples section.**
|
||||
Different ways of starting the server
|
||||
-------------------------------------
|
||||
|
||||
This is the minimal example of using the library with CircuitPython.
|
||||
This example is serving a simple static text message.
|
||||
There are several ways to start the server on CircuitPython, mostly depending on the device you are using and
|
||||
whether you have access to external network.
|
||||
|
||||
It also manually connects to the WiFi network.
|
||||
Functionally, all of them are the same, not features of the server are limited or disabled in any way.
|
||||
|
||||
.. literalinclude:: ../examples/httpserver_simpletest_manual.py
|
||||
:caption: examples/httpserver_simpletest_manual.py
|
||||
:emphasize-lines: 12-17
|
||||
:linenos:
|
||||
Below you can find examples of different ways to start the server:
|
||||
|
||||
It is also possible to use Ethernet instead of WiFi.
|
||||
The only difference in usage is related to configuring the ``socket_source`` differently.
|
||||
.. toctree::
|
||||
|
||||
.. literalinclude:: ../examples/httpserver_ethernet_simpletest.py
|
||||
:caption: examples/httpserver_ethernet_simpletest.py
|
||||
:emphasize-lines: 13-23
|
||||
:linenos:
|
||||
|
||||
Although there is nothing wrong with this approach, from the version 8.0.0 of CircuitPython,
|
||||
`it is possible to use the environment variables <https://docs.circuitpython.org/en/latest/docs/environment.html#circuitpython-behavior>`_
|
||||
defined in ``settings.toml`` file to store secrets and configure the WiFi network.
|
||||
|
||||
This is the same example as above, but it uses the ``settings.toml`` file to configure the WiFi network.
|
||||
|
||||
**From now on, all the examples will use the** ``settings.toml`` **file to configure the WiFi network.**
|
||||
|
||||
.. literalinclude:: ../examples/settings.toml
|
||||
:caption: settings.toml
|
||||
:lines: 5-
|
||||
:linenos:
|
||||
|
||||
Note that we still need to import ``socketpool`` and ``wifi`` modules.
|
||||
|
||||
.. literalinclude:: ../examples/httpserver_simpletest_auto.py
|
||||
:caption: examples/httpserver_simpletest_auto.py
|
||||
:emphasize-lines: 11
|
||||
:linenos:
|
||||
starting_methods
|
||||
|
||||
CPython usage
|
||||
--------------------
|
||||
|
|
@ -65,7 +39,7 @@ In order to save memory, we are unregistering unused MIME types and registering
|
|||
|
||||
.. literalinclude:: ../examples/httpserver_static_files_serving.py
|
||||
:caption: examples/httpserver_static_files_serving.py
|
||||
:emphasize-lines: 12-18,23-26
|
||||
:emphasize-lines: 11-17,22-25
|
||||
:linenos:
|
||||
|
||||
You can also serve a specific file from the handler.
|
||||
|
|
@ -77,7 +51,7 @@ By doing that, you can serve files from multiple directories, and decide exactly
|
|||
|
||||
.. literalinclude:: ../examples/httpserver_handler_serves_file.py
|
||||
:caption: examples/httpserver_handler_serves_file.py
|
||||
:emphasize-lines: 13,22
|
||||
:emphasize-lines: 12,21
|
||||
:linenos:
|
||||
|
||||
.. literalinclude:: ../examples/home.html
|
||||
|
|
@ -100,7 +74,7 @@ a running total of the last 10 samples.
|
|||
|
||||
.. literalinclude:: ../examples/httpserver_start_and_poll.py
|
||||
:caption: examples/httpserver_start_and_poll.py
|
||||
:emphasize-lines: 29,38
|
||||
:emphasize-lines: 28,37
|
||||
:linenos:
|
||||
|
||||
|
||||
|
|
@ -112,7 +86,7 @@ without needing to manually manage the timing of each task.
|
|||
|
||||
.. literalinclude:: ../examples/httpserver_start_and_poll_asyncio.py
|
||||
:caption: examples/httpserver_start_and_poll_asyncio.py
|
||||
:emphasize-lines: 5,33,42,45,50,55-62
|
||||
:emphasize-lines: 5,6,34,43,46,51,56-63
|
||||
:linenos:
|
||||
|
||||
Server with MDNS
|
||||
|
|
@ -122,12 +96,12 @@ It is possible to use the MDNS protocol to make the server accessible via a host
|
|||
to an IP address. It is worth noting that it takes a bit longer to get the response from the server
|
||||
when accessing it via the hostname.
|
||||
|
||||
In this example, the server is accessible via the IP and ``http://custom-mdns-hostname.local/``.
|
||||
On some routers it is also possible to use ``http://custom-mdns-hostname/``, but **this is not guaranteed to work**.
|
||||
In this example, the server is accessible via the IP and ``http://custom-mdns-hostname.local:5000/``.
|
||||
On some routers it is also possible to use ``http://custom-mdns-hostname:5000/``, but **this is not guaranteed to work**.
|
||||
|
||||
.. literalinclude:: ../examples/httpserver_mdns.py
|
||||
:caption: examples/httpserver_mdns.py
|
||||
:emphasize-lines: 12-14
|
||||
:emphasize-lines: 11-13
|
||||
:linenos:
|
||||
|
||||
Get CPU information
|
||||
|
|
@ -141,7 +115,7 @@ More info: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS
|
|||
|
||||
.. literalinclude:: ../examples/httpserver_cpu_information.py
|
||||
:caption: examples/httpserver_cpu_information.py
|
||||
:emphasize-lines: 9,15-18,33
|
||||
:emphasize-lines: 9,14-17,32
|
||||
:linenos:
|
||||
|
||||
Handling different methods
|
||||
|
|
@ -160,7 +134,7 @@ In example below, handler for ``/api`` and ``/api/`` route will be called when a
|
|||
|
||||
.. literalinclude:: ../examples/httpserver_methods.py
|
||||
:caption: examples/httpserver_methods.py
|
||||
:emphasize-lines: 8,19,26,30,49
|
||||
:emphasize-lines: 8,18,25,29,46
|
||||
:linenos:
|
||||
|
||||
Change NeoPixel color
|
||||
|
|
@ -182,7 +156,7 @@ Tested on ESP32-S2 Feather.
|
|||
|
||||
.. literalinclude:: ../examples/httpserver_neopixel.py
|
||||
:caption: examples/httpserver_neopixel.py
|
||||
:emphasize-lines: 26-28,41,52,68,74
|
||||
:emphasize-lines: 25-27,40,51,67,73
|
||||
:linenos:
|
||||
|
||||
Templates
|
||||
|
|
@ -207,7 +181,7 @@ You can find more information about the template syntax in the
|
|||
|
||||
.. literalinclude:: ../examples/httpserver_templates.py
|
||||
:caption: examples/httpserver_templates.py
|
||||
:emphasize-lines: 12-15,49-55
|
||||
:emphasize-lines: 12-15,51-59
|
||||
:linenos:
|
||||
|
||||
Form data parsing
|
||||
|
|
@ -222,7 +196,7 @@ It is important to use correct ``enctype``, depending on the type of data you wa
|
|||
- ``application/x-www-form-urlencoded`` - For sending simple text data without any special characters including spaces.
|
||||
If you use it, values will be automatically parsed as strings, but special characters will be URL encoded
|
||||
e.g. ``"Hello World! ^-$%"`` will be saved as ``"Hello+World%21+%5E-%24%25"``
|
||||
- ``multipart/form-data`` - For sending textwith special characters and files
|
||||
- ``multipart/form-data`` - For sending text with special characters and files
|
||||
When used, non-file values will be automatically parsed as strings and non plain text files will be saved as ``bytes``.
|
||||
e.g. ``"Hello World! ^-$%"`` will be saved as ``'Hello World! ^-$%'``, and e.g. a PNG file will be saved as ``b'\x89PNG\r\n\x1a\n\x00\...``.
|
||||
- ``text/plain`` - For sending text data with special characters.
|
||||
|
|
@ -235,7 +209,7 @@ return only the first one.
|
|||
|
||||
.. literalinclude:: ../examples/httpserver_form_data.py
|
||||
:caption: examples/httpserver_form_data.py
|
||||
:emphasize-lines: 32,47,50
|
||||
:emphasize-lines: 31,46,49
|
||||
:linenos:
|
||||
|
||||
Cookies
|
||||
|
|
@ -249,7 +223,7 @@ In order to set cookies, pass ``cookies`` dictionary to ``Response`` constructo
|
|||
|
||||
.. literalinclude:: ../examples/httpserver_cookies.py
|
||||
:caption: examples/httpserver_cookies.py
|
||||
:emphasize-lines: 70,74-75,82
|
||||
:emphasize-lines: 69,73-74,81
|
||||
:linenos:
|
||||
|
||||
Chunked response
|
||||
|
|
@ -261,7 +235,7 @@ constructor.
|
|||
|
||||
.. literalinclude:: ../examples/httpserver_chunked.py
|
||||
:caption: examples/httpserver_chunked.py
|
||||
:emphasize-lines: 8,21-26,28
|
||||
:emphasize-lines: 8,20-25,27
|
||||
:linenos:
|
||||
|
||||
URL parameters and wildcards
|
||||
|
|
@ -297,7 +271,7 @@ In both cases, wildcards will not match empty path segment, so ``/api/.../users`
|
|||
|
||||
.. literalinclude:: ../examples/httpserver_url_parameters.py
|
||||
:caption: examples/httpserver_url_parameters.py
|
||||
:emphasize-lines: 30-34,53-54,65-66
|
||||
:emphasize-lines: 29-31,48-49,60-61
|
||||
:linenos:
|
||||
|
||||
Authentication
|
||||
|
|
@ -310,7 +284,7 @@ If you want to apply authentication to the whole server, you need to call ``.req
|
|||
|
||||
.. literalinclude:: ../examples/httpserver_authentication_server.py
|
||||
:caption: examples/httpserver_authentication_server.py
|
||||
:emphasize-lines: 8,11-16,20
|
||||
:emphasize-lines: 8,10-15,19
|
||||
:linenos:
|
||||
|
||||
On the other hand, if you want to apply authentication to a set of routes, you need to call ``require_authentication`` function.
|
||||
|
|
@ -318,7 +292,7 @@ In both cases you can check if ``request`` is authenticated by calling ``check_a
|
|||
|
||||
.. literalinclude:: ../examples/httpserver_authentication_handlers.py
|
||||
:caption: examples/httpserver_authentication_handlers.py
|
||||
:emphasize-lines: 9-16,22-27,35,49,61
|
||||
:emphasize-lines: 9-16,21-26,34,48,60
|
||||
:linenos:
|
||||
|
||||
Redirects
|
||||
|
|
@ -335,7 +309,7 @@ Alternatively, you can pass a ``status`` object directly to ``Redirect`` constru
|
|||
|
||||
.. literalinclude:: ../examples/httpserver_redirects.py
|
||||
:caption: examples/httpserver_redirects.py
|
||||
:emphasize-lines: 22-26,32,38,50,62
|
||||
:emphasize-lines: 21-25,31,37,49,61
|
||||
:linenos:
|
||||
|
||||
Server-Sent Events
|
||||
|
|
@ -348,12 +322,14 @@ This can be overcomed by periodically polling the server, but it is not an elega
|
|||
Response is initialized on ``return``, events can be sent using ``.send_event()`` method. Due to the nature of SSE, it is necessary to store the
|
||||
response object somewhere, so that it can be accessed later.
|
||||
|
||||
**Because of the limited number of concurrently open sockets, it is not possible to process more than one SSE response at the same time.
|
||||
This might change in the future, but for now, it is recommended to use SSE only with one client at a time.**
|
||||
|
||||
.. warning::
|
||||
Because of the limited number of concurrently open sockets, it is **not possible to process more than one SSE response at the same time**.
|
||||
This might change in the future, but for now, it is recommended to use SSE **only with one client at a time**.
|
||||
|
||||
.. literalinclude:: ../examples/httpserver_sse.py
|
||||
:caption: examples/httpserver_sse.py
|
||||
:emphasize-lines: 10,17,46-53,63
|
||||
:emphasize-lines: 11,17,46-53,63
|
||||
:linenos:
|
||||
|
||||
Websockets
|
||||
|
|
@ -370,14 +346,61 @@ This is anologous to calling ``.poll()`` on the ``Server`` object.
|
|||
The following example uses ``asyncio``, which has to be installed separately. It is not necessary to use ``asyncio`` to use Websockets,
|
||||
but it is recommended as it makes it easier to handle multiple tasks. It can be used in any of the examples, but here it is particularly useful.
|
||||
|
||||
**Because of the limited number of concurrently open sockets, it is not possible to process more than one Websocket response at the same time.
|
||||
This might change in the future, but for now, it is recommended to use Websocket only with one client at a time.**
|
||||
.. warning::
|
||||
Because of the limited number of concurrently open sockets, it is **not possible to process more than one Websocket response at the same time**.
|
||||
This might change in the future, but for now, it is recommended to use Websocket **only with one client at a time**.
|
||||
|
||||
.. literalinclude:: ../examples/httpserver_websocket.py
|
||||
:caption: examples/httpserver_websocket.py
|
||||
:emphasize-lines: 12,20,65-72,88,99
|
||||
:emphasize-lines: 14,21,66-73,89,100
|
||||
:linenos:
|
||||
|
||||
Custom response types e.g. video streaming
|
||||
------------------------------------------
|
||||
|
||||
The built-in response types may not always meet your specific requirements. In such cases, you can define custom response types and implement
|
||||
the necessary logic.
|
||||
|
||||
The example below demonstrates a ``XMixedReplaceResponse`` class, which uses the ``multipart/x-mixed-replace`` content type to stream video frames
|
||||
from a camera, similar to a CCTV system.
|
||||
|
||||
To ensure the server remains responsive, a global list of open connections is maintained. By running tasks asynchronously, the server can stream
|
||||
video to multiple clients while simultaneously handling other requests.
|
||||
|
||||
.. literalinclude:: ../examples/httpserver_video_stream.py
|
||||
:caption: examples/httpserver_video_stream.py
|
||||
:emphasize-lines: 30-72,87
|
||||
:linenos:
|
||||
|
||||
HTTPS
|
||||
-----
|
||||
|
||||
.. warning::
|
||||
HTTPS on CircuitPython **works only on boards with enough memory e.g. ESP32-S3**.
|
||||
|
||||
When you want to expose your server to the internet or an untrusted network, it is recommended to use HTTPS.
|
||||
Together with authentication, it provides a relatively secure way to communicate with the server.
|
||||
|
||||
.. note::
|
||||
Using HTTPS slows down the server, because of additional work with encryption and decryption.
|
||||
|
||||
Enabling HTTPS is straightforward and comes down to passing the path to the certificate and key files to the ``Server`` constructor
|
||||
and setting ``https=True``.
|
||||
|
||||
.. literalinclude:: ../examples/httpserver_https.py
|
||||
:caption: examples/httpserver_https.py
|
||||
:emphasize-lines: 14-16
|
||||
:linenos:
|
||||
|
||||
|
||||
To create your own certificate, you can use the following command:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
sudo openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout key.pem -out cert.pem
|
||||
|
||||
You might have to change permissions of the files, so that the server can read them.
|
||||
|
||||
Multiple servers
|
||||
----------------
|
||||
|
||||
|
|
@ -387,7 +410,7 @@ Using ``.serve_forever()`` for this is not possible because of it's blocking beh
|
|||
|
||||
Each server **must have a different port number**.
|
||||
|
||||
In order to distinguish between responses from different servers a 'X-Server' header is added to each response.
|
||||
To distinguish between responses from different servers a 'X-Server' header is added to each response.
|
||||
**This is an optional step**, both servers will work without it.
|
||||
|
||||
In combination with separate authentication and diffrent ``root_path`` this allows creating moderately complex setups.
|
||||
|
|
@ -395,7 +418,7 @@ You can share same handler functions between servers or use different ones for e
|
|||
|
||||
.. literalinclude:: ../examples/httpserver_multiple_servers.py
|
||||
:caption: examples/httpserver_multiple_servers.py
|
||||
:emphasize-lines: 13-14,16-17,20,28,36-37,48-49,54-55
|
||||
:emphasize-lines: 12-13,15-16,19,27,35-36,47-48,53-54
|
||||
:linenos:
|
||||
|
||||
Debug mode
|
||||
|
|
@ -412,7 +435,7 @@ occurs during handling of the request in ``.serve_forever()``.
|
|||
|
||||
This is how the logs might look like when debug mode is enabled::
|
||||
|
||||
Started development server on http://192.168.0.100:80
|
||||
Started development server on http://192.168.0.100:5000
|
||||
192.168.0.101 -- "GET /" 194 -- "200 OK" 154 -- 96ms
|
||||
192.168.0.101 -- "GET /example" 134 -- "404 Not Found" 172 -- 123ms
|
||||
192.168.0.102 -- "POST /api" 1241 -- "401 Unauthorized" 95 -- 64ms
|
||||
|
|
@ -430,5 +453,5 @@ This is the default format of the logs::
|
|||
If you need more information about the server or request, or you want it in a different format you can modify
|
||||
functions at the bottom of ``adafruit_httpserver/server.py`` that start with ``_debug_...``.
|
||||
|
||||
NOTE:
|
||||
*This is an advanced usage that might change in the future. It is not recommended to modify other parts of the code.*
|
||||
.. note::
|
||||
This is an advanced usage that might change in the future. It is not recommended to modify other parts of the code.
|
||||
|
|
|
|||
80
docs/starting_methods.rst
Normal file
80
docs/starting_methods.rst
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
|
||||
Manual WiFi
|
||||
-----------
|
||||
|
||||
This is the minimal example of using the library with CircuitPython.
|
||||
This example is serving a simple static text message.
|
||||
|
||||
It also manually connects to the WiFi network. SSID and password are stored in the code, but they
|
||||
can as well be stored in the ``settings.toml`` file, and then read from there using ``os.getenv()``.
|
||||
|
||||
.. literalinclude:: ../examples/httpserver_simpletest_manual_wifi.py
|
||||
:caption: examples/httpserver_simpletest_manual_wifi.py
|
||||
:emphasize-lines: 10-17
|
||||
:linenos:
|
||||
|
||||
Manual AP (access point)
|
||||
------------------------
|
||||
|
||||
If there is no external network available, it is possible to create an access point (AP) and run a server on it.
|
||||
It is important to note that only devices connected to the AP will be able to access the server and depending on the device,
|
||||
it may not be able to access the internet.
|
||||
|
||||
.. literalinclude:: ../examples/httpserver_simpletest_manual_ap.py
|
||||
:caption: examples/httpserver_simpletest_manual_ap.py
|
||||
:emphasize-lines: 10-15,29
|
||||
:linenos:
|
||||
|
||||
Manual Ethernet
|
||||
---------------
|
||||
|
||||
Ethernet can also be used to connect to the location network.
|
||||
|
||||
.. literalinclude:: ../examples/httpserver_simpletest_manual_ethernet.py
|
||||
:caption: examples/httpserver_simpletest_manual_ethernet.py
|
||||
:emphasize-lines: 11-20
|
||||
:linenos:
|
||||
|
||||
Automatic WiFi using ``settings.toml``
|
||||
--------------------------------------
|
||||
|
||||
From the version 8.0.0 of CircuitPython,
|
||||
`it is possible to use the environment variables <https://docs.circuitpython.org/en/latest/docs/environment.html#circuitpython-behavior>`_
|
||||
defined in ``settings.toml`` file to store secrets and configure the WiFi network
|
||||
using the ``CIRCUITPY_WIFI_SSID`` and ``CIRCUITPY_WIFI_PASSWORD`` variables.
|
||||
|
||||
By default the library uses ``0.0.0.0`` and port ``5000`` for the server, as port ``80`` is reserved for the CircuitPython Web Workflow.
|
||||
If you want to use port ``80`` , you need to set ``CIRCUITPY_WEB_API_PORT`` to any other port, and then set ``port`` parameter in ``Server`` constructor to ``80`` .
|
||||
|
||||
This is the same example as above, but it uses the ``settings.toml`` file to configure the WiFi network.
|
||||
|
||||
.. note::
|
||||
From now on, all the examples will use the ``settings.toml`` file to configure the WiFi network.
|
||||
|
||||
.. literalinclude:: ../examples/settings.toml
|
||||
:caption: settings.toml
|
||||
:lines: 5-
|
||||
:linenos:
|
||||
|
||||
Note that we still need to import ``socketpool`` and ``wifi`` modules.
|
||||
|
||||
.. literalinclude:: ../examples/httpserver_simpletest_auto_settings_toml.py
|
||||
:caption: examples/httpserver_simpletest_auto_settings_toml.py
|
||||
:emphasize-lines: 10
|
||||
:linenos:
|
||||
|
||||
|
||||
Helper for socket pool using ``adafruit_connection_manager``
|
||||
------------------------------------------------------------
|
||||
|
||||
If you do not want to configure the socket pool manually, you can use the ``adafruit_connection_manager`` library,
|
||||
which provides helpers for getting socket pool and SSL context for common boards.
|
||||
|
||||
Note that it is not installed by default.
|
||||
You can read `more about it here <https://docs.circuitpython.org/projects/connectionmanager/en/latest/index.html>`_.
|
||||
|
||||
|
||||
.. literalinclude:: ../examples/httpserver_simpletest_auto_connection_manager.py
|
||||
:caption: examples/httpserver_simpletest_auto_connection_manager.py
|
||||
:emphasize-lines: 6,10
|
||||
:linenos:
|
||||
3
docs/starting_methods.rst.license
Normal file
3
docs/starting_methods.rst.license
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
SPDX-FileCopyrightText: 2024 Michał Pokusa
|
||||
|
||||
SPDX-License-Identifier: MIT
|
||||
|
|
@ -5,17 +5,16 @@
|
|||
import socketpool
|
||||
import wifi
|
||||
|
||||
from adafruit_httpserver import Server, Request, Response, UNAUTHORIZED_401
|
||||
from adafruit_httpserver import UNAUTHORIZED_401, Request, Response, Server
|
||||
from adafruit_httpserver.authentication import (
|
||||
AuthenticationError,
|
||||
Basic,
|
||||
Token,
|
||||
Bearer,
|
||||
Token,
|
||||
check_authentication,
|
||||
require_authentication,
|
||||
)
|
||||
|
||||
|
||||
pool = socketpool.SocketPool(wifi.radio)
|
||||
server = Server(pool, debug=True)
|
||||
|
||||
|
|
|
|||
|
|
@ -5,8 +5,7 @@
|
|||
import socketpool
|
||||
import wifi
|
||||
|
||||
from adafruit_httpserver import Server, Request, Response, Basic, Token, Bearer
|
||||
|
||||
from adafruit_httpserver import Basic, Bearer, Request, Response, Server, Token
|
||||
|
||||
# Create a list of available authentication methods.
|
||||
auths = [
|
||||
|
|
|
|||
|
|
@ -5,8 +5,7 @@
|
|||
import socketpool
|
||||
import wifi
|
||||
|
||||
from adafruit_httpserver import Server, Request, ChunkedResponse
|
||||
|
||||
from adafruit_httpserver import ChunkedResponse, Request, Server
|
||||
|
||||
pool = socketpool.SocketPool(wifi.radio)
|
||||
server = Server(pool, debug=True)
|
||||
|
|
|
|||
|
|
@ -5,8 +5,7 @@
|
|||
import socketpool
|
||||
import wifi
|
||||
|
||||
from adafruit_httpserver import Server, Request, Response, GET, Headers
|
||||
|
||||
from adafruit_httpserver import GET, Headers, Request, Response, Server
|
||||
|
||||
pool = socketpool.SocketPool(wifi.radio)
|
||||
server = Server(pool, debug=True)
|
||||
|
|
|
|||
|
|
@ -6,8 +6,7 @@ import microcontroller
|
|||
import socketpool
|
||||
import wifi
|
||||
|
||||
from adafruit_httpserver import Server, Request, JSONResponse
|
||||
|
||||
from adafruit_httpserver import JSONResponse, Request, Server
|
||||
|
||||
pool = socketpool.SocketPool(wifi.radio)
|
||||
server = Server(pool, debug=True)
|
||||
|
|
|
|||
|
|
@ -4,8 +4,7 @@
|
|||
|
||||
import socket
|
||||
|
||||
from adafruit_httpserver import Server, Request, Response
|
||||
|
||||
from adafruit_httpserver import Request, Response, Server
|
||||
|
||||
pool = socket
|
||||
server = Server(pool, "/static", debug=True)
|
||||
|
|
|
|||
|
|
@ -5,8 +5,7 @@
|
|||
import socketpool
|
||||
import wifi
|
||||
|
||||
from adafruit_httpserver import Server, Request, Response, GET, POST
|
||||
|
||||
from adafruit_httpserver import GET, POST, Request, Response, Server
|
||||
|
||||
pool = socketpool.SocketPool(wifi.radio)
|
||||
server = Server(pool, debug=True)
|
||||
|
|
|
|||
|
|
@ -6,8 +6,7 @@
|
|||
import socketpool
|
||||
import wifi
|
||||
|
||||
from adafruit_httpserver import Server, Request, FileResponse
|
||||
|
||||
from adafruit_httpserver import FileResponse, Request, Server
|
||||
|
||||
pool = socketpool.SocketPool(wifi.radio)
|
||||
server = Server(pool, "/default-static-folder", debug=True)
|
||||
|
|
|
|||
29
examples/httpserver_https.py
Normal file
29
examples/httpserver_https.py
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
# SPDX-FileCopyrightText: 2024 Michał Pokusa
|
||||
#
|
||||
# SPDX-License-Identifier: Unlicense
|
||||
|
||||
import socketpool
|
||||
import wifi
|
||||
|
||||
from adafruit_httpserver import Request, Response, Server
|
||||
|
||||
pool = socketpool.SocketPool(wifi.radio)
|
||||
server = Server(
|
||||
pool,
|
||||
root_path="/static",
|
||||
https=True,
|
||||
certfile="cert.pem",
|
||||
keyfile="key.pem",
|
||||
debug=True,
|
||||
)
|
||||
|
||||
|
||||
@server.route("/")
|
||||
def base(request: Request):
|
||||
"""
|
||||
Serve a default static plain text message.
|
||||
"""
|
||||
return Response(request, "Hello from the CircuitPython HTTPS Server!")
|
||||
|
||||
|
||||
server.serve_forever(str(wifi.radio.ipv4_address), 443)
|
||||
|
|
@ -6,12 +6,11 @@ import mdns
|
|||
import socketpool
|
||||
import wifi
|
||||
|
||||
from adafruit_httpserver import Server, Request, FileResponse
|
||||
|
||||
from adafruit_httpserver import FileResponse, Request, Server
|
||||
|
||||
mdns_server = mdns.Server(wifi.radio)
|
||||
mdns_server.hostname = "custom-mdns-hostname"
|
||||
mdns_server.advertise_service(service_type="_http", protocol="_tcp", port=80)
|
||||
mdns_server.advertise_service(service_type="_http", protocol="_tcp", port=5000)
|
||||
|
||||
pool = socketpool.SocketPool(wifi.radio)
|
||||
server = Server(pool, "/static", debug=True)
|
||||
|
|
|
|||
|
|
@ -5,8 +5,7 @@
|
|||
import socketpool
|
||||
import wifi
|
||||
|
||||
from adafruit_httpserver import Server, Request, JSONResponse, GET, POST, PUT, DELETE
|
||||
|
||||
from adafruit_httpserver import DELETE, GET, POST, PUT, JSONResponse, Request, Server
|
||||
|
||||
pool = socketpool.SocketPool(wifi.radio)
|
||||
server = Server(pool, debug=True)
|
||||
|
|
@ -27,7 +26,7 @@ def api(request: Request):
|
|||
return JSONResponse(request, objects)
|
||||
|
||||
# Upload or update objects
|
||||
if request.method in [POST, PUT]:
|
||||
if request.method in {POST, PUT}:
|
||||
uploaded_object = request.json()
|
||||
|
||||
# Find object with same ID
|
||||
|
|
@ -41,9 +40,7 @@ def api(request: Request):
|
|||
|
||||
# If not found, add it
|
||||
objects.append(uploaded_object)
|
||||
return JSONResponse(
|
||||
request, {"message": "Object added", "object": uploaded_object}
|
||||
)
|
||||
return JSONResponse(request, {"message": "Object added", "object": uploaded_object})
|
||||
|
||||
# Delete objects
|
||||
if request.method == DELETE:
|
||||
|
|
@ -59,9 +56,7 @@ def api(request: Request):
|
|||
)
|
||||
|
||||
# If not found, return error
|
||||
return JSONResponse(
|
||||
request, {"message": "Object not found", "object": deleted_object}
|
||||
)
|
||||
return JSONResponse(request, {"message": "Object not found", "object": deleted_object})
|
||||
|
||||
# If we get here, something went wrong
|
||||
return JSONResponse(request, {"message": "Something went wrong"})
|
||||
|
|
|
|||
|
|
@ -5,8 +5,7 @@
|
|||
import socketpool
|
||||
import wifi
|
||||
|
||||
from adafruit_httpserver import Server, Request, Response
|
||||
|
||||
from adafruit_httpserver import Request, Response, Server
|
||||
|
||||
pool = socketpool.SocketPool(wifi.radio)
|
||||
|
||||
|
|
@ -42,11 +41,11 @@ def home(request: Request):
|
|||
return Response(request, "Hello from home!")
|
||||
|
||||
|
||||
id_address = str(wifi.radio.ipv4_address)
|
||||
ip_address = str(wifi.radio.ipv4_address)
|
||||
|
||||
# Start the servers.
|
||||
bedroom_server.start(id_address, 5000)
|
||||
office_server.start(id_address, 8000)
|
||||
bedroom_server.start(ip_address, 5000)
|
||||
office_server.start(ip_address, 8000)
|
||||
|
||||
while True:
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -7,8 +7,7 @@ import neopixel
|
|||
import socketpool
|
||||
import wifi
|
||||
|
||||
from adafruit_httpserver import Server, Route, as_route, Request, Response, GET, POST
|
||||
|
||||
from adafruit_httpserver import GET, POST, Request, Response, Route, Server, as_route
|
||||
|
||||
pool = socketpool.SocketPool(wifi.radio)
|
||||
server = Server(pool, "/static", debug=True)
|
||||
|
|
|
|||
|
|
@ -6,16 +6,15 @@ import socketpool
|
|||
import wifi
|
||||
|
||||
from adafruit_httpserver import (
|
||||
Server,
|
||||
MOVED_PERMANENTLY_301,
|
||||
NOT_FOUND_404,
|
||||
POST,
|
||||
Redirect,
|
||||
Request,
|
||||
Response,
|
||||
Redirect,
|
||||
POST,
|
||||
NOT_FOUND_404,
|
||||
MOVED_PERMANENTLY_301,
|
||||
Server,
|
||||
)
|
||||
|
||||
|
||||
pool = socketpool.SocketPool(wifi.radio)
|
||||
server = Server(pool, debug=True)
|
||||
|
||||
|
|
|
|||
22
examples/httpserver_simpletest_auto_connection_manager.py
Normal file
22
examples/httpserver_simpletest_auto_connection_manager.py
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
# SPDX-FileCopyrightText: 2024 DJDevon3
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
import wifi
|
||||
from adafruit_connection_manager import get_radio_socketpool
|
||||
|
||||
from adafruit_httpserver import Request, Response, Server
|
||||
|
||||
pool = get_radio_socketpool(wifi.radio)
|
||||
server = Server(pool, "/static", debug=True)
|
||||
|
||||
|
||||
@server.route("/")
|
||||
def base(request: Request):
|
||||
"""
|
||||
Serve a default static plain text message.
|
||||
"""
|
||||
return Response(request, "Hello from the CircuitPython HTTP Server!")
|
||||
|
||||
|
||||
server.serve_forever(str(wifi.radio.ipv4_address))
|
||||
|
|
@ -5,8 +5,7 @@
|
|||
import socketpool
|
||||
import wifi
|
||||
|
||||
from adafruit_httpserver import Server, Request, Response
|
||||
|
||||
from adafruit_httpserver import Request, Response, Server
|
||||
|
||||
pool = socketpool.SocketPool(wifi.radio)
|
||||
server = Server(pool, "/static", debug=True)
|
||||
29
examples/httpserver_simpletest_manual_ap.py
Normal file
29
examples/httpserver_simpletest_manual_ap.py
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
# SPDX-FileCopyrightText: 2024 Michał Pokusa
|
||||
#
|
||||
# SPDX-License-Identifier: Unlicense
|
||||
|
||||
import socketpool
|
||||
import wifi
|
||||
|
||||
from adafruit_httpserver import Request, Response, Server
|
||||
|
||||
AP_SSID = "..."
|
||||
AP_PASSWORD = "..."
|
||||
|
||||
print("Creating access point...")
|
||||
wifi.radio.start_ap(ssid=AP_SSID, password=AP_PASSWORD)
|
||||
print(f"Created access point {AP_SSID}")
|
||||
|
||||
pool = socketpool.SocketPool(wifi.radio)
|
||||
server = Server(pool, "/static", debug=True)
|
||||
|
||||
|
||||
@server.route("/")
|
||||
def base(request: Request):
|
||||
"""
|
||||
Serve a default static plain text message.
|
||||
"""
|
||||
return Response(request, "Hello from the CircuitPython HTTP Server!")
|
||||
|
||||
|
||||
server.serve_forever(str(wifi.radio.ipv4_address_ap))
|
||||
|
|
@ -1,29 +1,28 @@
|
|||
# SPDX-FileCopyrightText: 2023 Tim C for Adafruit Industries
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
import adafruit_connection_manager
|
||||
import board
|
||||
import digitalio
|
||||
|
||||
from adafruit_wiznet5k.adafruit_wiznet5k import WIZNET5K
|
||||
import adafruit_wiznet5k.adafruit_wiznet5k_socket as socket
|
||||
from adafruit_httpserver import Server, Request, Response
|
||||
|
||||
print("Wiznet5k HTTPServer Test")
|
||||
from adafruit_httpserver import Request, Response, Server
|
||||
|
||||
# For Adafruit Ethernet FeatherWing
|
||||
cs = digitalio.DigitalInOut(board.D10)
|
||||
|
||||
# For Particle Ethernet FeatherWing
|
||||
# cs = digitalio.DigitalInOut(board.D5)
|
||||
|
||||
spi_bus = board.SPI()
|
||||
|
||||
# Initialize ethernet interface with DHCP
|
||||
eth = WIZNET5K(spi_bus, cs)
|
||||
|
||||
# Set the interface on the socket source
|
||||
socket.set_interface(eth)
|
||||
pool = adafruit_connection_manager.get_radio_socketpool(eth)
|
||||
|
||||
# Initialize the server
|
||||
server = Server(socket, "/static", debug=True)
|
||||
server = Server(pool, "/static", debug=True)
|
||||
|
||||
|
||||
@server.route("/")
|
||||
|
|
@ -2,21 +2,20 @@
|
|||
#
|
||||
# SPDX-License-Identifier: Unlicense
|
||||
|
||||
import os
|
||||
|
||||
import socketpool
|
||||
import wifi
|
||||
|
||||
from adafruit_httpserver import Server, Request, Response
|
||||
from adafruit_httpserver import Request, Response, Server
|
||||
|
||||
ssid = os.getenv("WIFI_SSID")
|
||||
password = os.getenv("WIFI_PASSWORD")
|
||||
WIFI_SSID = "..."
|
||||
WIFI_PASSWORD = "..."
|
||||
|
||||
print("Connecting to", ssid)
|
||||
wifi.radio.connect(ssid, password)
|
||||
print("Connected to", ssid)
|
||||
print(f"Connecting to {WIFI_SSID}...")
|
||||
wifi.radio.connect(WIFI_SSID, WIFI_PASSWORD)
|
||||
print(f"Connected to {WIFI_SSID}")
|
||||
|
||||
pool = socketpool.SocketPool(wifi.radio)
|
||||
|
||||
server = Server(pool, "/static", debug=True)
|
||||
|
||||
|
||||
|
|
@ -3,12 +3,12 @@
|
|||
# SPDX-License-Identifier: Unlicense
|
||||
|
||||
from time import monotonic
|
||||
|
||||
import microcontroller
|
||||
import socketpool
|
||||
import wifi
|
||||
|
||||
from adafruit_httpserver import Server, Request, Response, SSEResponse, GET
|
||||
|
||||
from adafruit_httpserver import GET, Request, Response, Server, SSEResponse
|
||||
|
||||
pool = socketpool.SocketPool(wifi.radio)
|
||||
server = Server(pool, debug=True)
|
||||
|
|
@ -43,7 +43,7 @@ def client(request: Request):
|
|||
|
||||
@server.route("/connect-client", GET)
|
||||
def connect_client(request: Request):
|
||||
global sse_response # pylint: disable=global-statement
|
||||
global sse_response
|
||||
|
||||
if sse_response is not None:
|
||||
sse_response.close() # Close any existing connection
|
||||
|
|
|
|||
|
|
@ -6,13 +6,12 @@ import socketpool
|
|||
import wifi
|
||||
|
||||
from adafruit_httpserver import (
|
||||
Server,
|
||||
REQUEST_HANDLED_RESPONSE_SENT,
|
||||
Request,
|
||||
FileResponse,
|
||||
Request,
|
||||
Server,
|
||||
)
|
||||
|
||||
|
||||
pool = socketpool.SocketPool(wifi.radio)
|
||||
server = Server(pool, "/static", debug=True)
|
||||
|
||||
|
|
|
|||
|
|
@ -2,18 +2,19 @@
|
|||
#
|
||||
# SPDX-License-Identifier: Unlicense
|
||||
|
||||
from asyncio import create_task, gather, run, sleep as async_sleep
|
||||
from asyncio import create_task, gather, run
|
||||
from asyncio import sleep as async_sleep
|
||||
|
||||
import socketpool
|
||||
import wifi
|
||||
|
||||
from adafruit_httpserver import (
|
||||
Server,
|
||||
REQUEST_HANDLED_RESPONSE_SENT,
|
||||
Request,
|
||||
FileResponse,
|
||||
Request,
|
||||
Server,
|
||||
)
|
||||
|
||||
|
||||
pool = socketpool.SocketPool(wifi.radio)
|
||||
server = Server(pool, "/static", debug=True)
|
||||
|
||||
|
|
|
|||
|
|
@ -6,8 +6,7 @@
|
|||
import socketpool
|
||||
import wifi
|
||||
|
||||
from adafruit_httpserver import Server, MIMETypes
|
||||
|
||||
from adafruit_httpserver import MIMETypes, Server
|
||||
|
||||
MIMETypes.configure(
|
||||
default_to="text/plain",
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import re
|
|||
import socketpool
|
||||
import wifi
|
||||
|
||||
from adafruit_httpserver import Server, Request, Response, FileResponse
|
||||
from adafruit_httpserver import FileResponse, Request, Response, Server
|
||||
|
||||
try:
|
||||
from adafruit_templateengine import render_template
|
||||
|
|
|
|||
|
|
@ -5,22 +5,21 @@
|
|||
import socketpool
|
||||
import wifi
|
||||
|
||||
from adafruit_httpserver import Server, Request, Response
|
||||
|
||||
from adafruit_httpserver import Request, Response, Server
|
||||
|
||||
pool = socketpool.SocketPool(wifi.radio)
|
||||
server = Server(pool, debug=True)
|
||||
|
||||
|
||||
class Device:
|
||||
def turn_on(self): # pylint: disable=no-self-use
|
||||
def turn_on(self):
|
||||
print("Turning on device.")
|
||||
|
||||
def turn_off(self): # pylint: disable=no-self-use
|
||||
def turn_off(self):
|
||||
print("Turning off device.")
|
||||
|
||||
|
||||
def get_device(device_id: str) -> Device: # pylint: disable=unused-argument
|
||||
def get_device(device_id: str) -> Device:
|
||||
"""
|
||||
This is a **made up** function that returns a `Device` object.
|
||||
"""
|
||||
|
|
@ -29,25 +28,21 @@ def get_device(device_id: str) -> Device: # pylint: disable=unused-argument
|
|||
|
||||
@server.route("/device/<device_id>/action/<action>")
|
||||
@server.route("/device/emergency-power-off/<device_id>")
|
||||
def perform_action(
|
||||
request: Request, device_id: str, action: str = "emergency_power_off"
|
||||
):
|
||||
def perform_action(request: Request, device_id: str, action: str = "emergency_power_off"):
|
||||
"""
|
||||
Performs an "action" on a specified device.
|
||||
"""
|
||||
|
||||
device = get_device(device_id)
|
||||
|
||||
if action in ["turn_on"]:
|
||||
if action in {"turn_on"}:
|
||||
device.turn_on()
|
||||
elif action in ["turn_off", "emergency_power_off"]:
|
||||
elif action in {"turn_off", "emergency_power_off"}:
|
||||
device.turn_off()
|
||||
else:
|
||||
return Response(request, f"Unknown action ({action})")
|
||||
|
||||
return Response(
|
||||
request, f"Action ({action}) performed on device with ID: {device_id}"
|
||||
)
|
||||
return Response(request, f"Action ({action}) performed on device with ID: {device_id}")
|
||||
|
||||
|
||||
@server.route("/device/<device_id>/status/<date>")
|
||||
|
|
|
|||
123
examples/httpserver_video_stream.py
Normal file
123
examples/httpserver_video_stream.py
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
# SPDX-FileCopyrightText: 2024 Michał Pokusa
|
||||
#
|
||||
# SPDX-License-Identifier: Unlicense
|
||||
|
||||
try:
|
||||
from typing import Dict, List, Tuple, Union
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
from asyncio import create_task, gather, run, sleep
|
||||
from random import choice
|
||||
|
||||
import socketpool
|
||||
import wifi
|
||||
from adafruit_pycamera import PyCamera
|
||||
|
||||
from adafruit_httpserver import OK_200, Headers, Request, Response, Server, Status
|
||||
|
||||
pool = socketpool.SocketPool(wifi.radio)
|
||||
server = Server(pool, debug=True)
|
||||
|
||||
|
||||
camera = PyCamera()
|
||||
camera.display.brightness = 0
|
||||
camera.mode = 0 # JPEG, required for `capture_into_jpeg()`
|
||||
camera.resolution = "1280x720"
|
||||
camera.effect = 0 # No effect
|
||||
|
||||
|
||||
class XMixedReplaceResponse(Response):
|
||||
def __init__(
|
||||
self,
|
||||
request: Request,
|
||||
frame_content_type: str,
|
||||
*,
|
||||
status: Union[Status, Tuple[int, str]] = OK_200,
|
||||
headers: Union[Headers, Dict[str, str]] = None,
|
||||
cookies: Dict[str, str] = None,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
request=request,
|
||||
headers=headers,
|
||||
cookies=cookies,
|
||||
status=status,
|
||||
)
|
||||
self._boundary = self._get_random_boundary()
|
||||
self._headers.setdefault(
|
||||
"Content-Type", f"multipart/x-mixed-replace; boundary={self._boundary}"
|
||||
)
|
||||
self._frame_content_type = frame_content_type
|
||||
|
||||
@staticmethod
|
||||
def _get_random_boundary() -> str:
|
||||
symbols = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
return "--" + "".join([choice(symbols) for _ in range(16)])
|
||||
|
||||
def send_frame(self, frame: Union[str, bytes] = "") -> None:
|
||||
encoded_frame = bytes(frame.encode("utf-8") if isinstance(frame, str) else frame)
|
||||
|
||||
self._send_bytes(self._request.connection, bytes(f"{self._boundary}\r\n", "utf-8"))
|
||||
self._send_bytes(
|
||||
self._request.connection,
|
||||
bytes(f"Content-Type: {self._frame_content_type}\r\n\r\n", "utf-8"),
|
||||
)
|
||||
self._send_bytes(self._request.connection, encoded_frame)
|
||||
self._send_bytes(self._request.connection, bytes("\r\n", "utf-8"))
|
||||
|
||||
def _send(self) -> None:
|
||||
self._send_headers()
|
||||
|
||||
def close(self) -> None:
|
||||
self._close_connection()
|
||||
|
||||
|
||||
stream_connections: List[XMixedReplaceResponse] = []
|
||||
|
||||
|
||||
@server.route("/frame")
|
||||
def frame_handler(request: Request):
|
||||
frame = camera.capture_into_jpeg()
|
||||
|
||||
return Response(request, body=frame, content_type="image/jpeg")
|
||||
|
||||
|
||||
@server.route("/stream")
|
||||
def stream_handler(request: Request):
|
||||
response = XMixedReplaceResponse(request, frame_content_type="image/jpeg")
|
||||
stream_connections.append(response)
|
||||
|
||||
return response
|
||||
|
||||
|
||||
async def send_stream_frames():
|
||||
while True:
|
||||
await sleep(0.1)
|
||||
|
||||
frame = camera.capture_into_jpeg()
|
||||
|
||||
for connection in iter(stream_connections):
|
||||
try:
|
||||
connection.send_frame(frame)
|
||||
except BrokenPipeError:
|
||||
connection.close()
|
||||
stream_connections.remove(connection)
|
||||
|
||||
|
||||
async def handle_http_requests():
|
||||
server.start(str(wifi.radio.ipv4_address))
|
||||
|
||||
while True:
|
||||
await sleep(0)
|
||||
|
||||
server.poll()
|
||||
|
||||
|
||||
async def main():
|
||||
await gather(
|
||||
create_task(send_stream_frames()),
|
||||
create_task(handle_http_requests()),
|
||||
)
|
||||
|
||||
|
||||
run(main())
|
||||
|
|
@ -2,15 +2,16 @@
|
|||
#
|
||||
# SPDX-License-Identifier: Unlicense
|
||||
|
||||
from asyncio import create_task, gather, run, sleep as async_sleep
|
||||
from asyncio import create_task, gather, run
|
||||
from asyncio import sleep as async_sleep
|
||||
|
||||
import board
|
||||
import microcontroller
|
||||
import neopixel
|
||||
import socketpool
|
||||
import wifi
|
||||
|
||||
from adafruit_httpserver import Server, Request, Response, Websocket, GET
|
||||
|
||||
from adafruit_httpserver import GET, Request, Response, Server, Websocket
|
||||
|
||||
pool = socketpool.SocketPool(wifi.radio)
|
||||
server = Server(pool, debug=True)
|
||||
|
|
@ -62,7 +63,7 @@ def client(request: Request):
|
|||
|
||||
@server.route("/connect-websocket", GET)
|
||||
def connect_client(request: Request):
|
||||
global websocket # pylint: disable=global-statement
|
||||
global websocket
|
||||
|
||||
if websocket is not None:
|
||||
websocket.close() # Close any existing connection
|
||||
|
|
|
|||
112
ruff.toml
Normal file
112
ruff.toml
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
# SPDX-FileCopyrightText: 2024 Tim Cocks for Adafruit Industries
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
target-version = "py38"
|
||||
line-length = 100
|
||||
|
||||
[lint]
|
||||
preview = true
|
||||
select = ["I", "PL", "UP"]
|
||||
|
||||
extend-select = [
|
||||
"D419", # empty-docstring
|
||||
"E501", # line-too-long
|
||||
"W291", # trailing-whitespace
|
||||
"PLC0414", # useless-import-alias
|
||||
"PLC2401", # non-ascii-name
|
||||
"PLC2801", # unnecessary-dunder-call
|
||||
"PLC3002", # unnecessary-direct-lambda-call
|
||||
"E999", # syntax-error
|
||||
"PLE0101", # return-in-init
|
||||
"F706", # return-outside-function
|
||||
"F704", # yield-outside-function
|
||||
"PLE0116", # continue-in-finally
|
||||
"PLE0117", # nonlocal-without-binding
|
||||
"PLE0241", # duplicate-bases
|
||||
"PLE0302", # unexpected-special-method-signature
|
||||
"PLE0604", # invalid-all-object
|
||||
"PLE0605", # invalid-all-format
|
||||
"PLE0643", # potential-index-error
|
||||
"PLE0704", # misplaced-bare-raise
|
||||
"PLE1141", # dict-iter-missing-items
|
||||
"PLE1142", # await-outside-async
|
||||
"PLE1205", # logging-too-many-args
|
||||
"PLE1206", # logging-too-few-args
|
||||
"PLE1307", # bad-string-format-type
|
||||
"PLE1310", # bad-str-strip-call
|
||||
"PLE1507", # invalid-envvar-value
|
||||
"PLE2502", # bidirectional-unicode
|
||||
"PLE2510", # invalid-character-backspace
|
||||
"PLE2512", # invalid-character-sub
|
||||
"PLE2513", # invalid-character-esc
|
||||
"PLE2514", # invalid-character-nul
|
||||
"PLE2515", # invalid-character-zero-width-space
|
||||
"PLR0124", # comparison-with-itself
|
||||
"PLR0202", # no-classmethod-decorator
|
||||
"PLR0203", # no-staticmethod-decorator
|
||||
"UP004", # useless-object-inheritance
|
||||
"PLR0206", # property-with-parameters
|
||||
"PLR0904", # too-many-public-methods
|
||||
"PLR0911", # too-many-return-statements
|
||||
"PLR0912", # too-many-branches
|
||||
"PLR0913", # too-many-arguments
|
||||
"PLR0914", # too-many-locals
|
||||
"PLR0915", # too-many-statements
|
||||
"PLR0916", # too-many-boolean-expressions
|
||||
"PLR1702", # too-many-nested-blocks
|
||||
"PLR1704", # redefined-argument-from-local
|
||||
"PLR1711", # useless-return
|
||||
"C416", # unnecessary-comprehension
|
||||
"PLR1733", # unnecessary-dict-index-lookup
|
||||
"PLR1736", # unnecessary-list-index-lookup
|
||||
|
||||
# ruff reports this rule is unstable
|
||||
#"PLR6301", # no-self-use
|
||||
|
||||
"PLW0108", # unnecessary-lambda
|
||||
"PLW0120", # useless-else-on-loop
|
||||
"PLW0127", # self-assigning-variable
|
||||
"PLW0129", # assert-on-string-literal
|
||||
"B033", # duplicate-value
|
||||
"PLW0131", # named-expr-without-context
|
||||
"PLW0245", # super-without-brackets
|
||||
"PLW0406", # import-self
|
||||
"PLW0602", # global-variable-not-assigned
|
||||
"PLW0603", # global-statement
|
||||
"PLW0604", # global-at-module-level
|
||||
|
||||
# fails on the try: import typing used by libraries
|
||||
#"F401", # unused-import
|
||||
|
||||
"F841", # unused-variable
|
||||
"E722", # bare-except
|
||||
"PLW0711", # binary-op-exception
|
||||
"PLW1501", # bad-open-mode
|
||||
"PLW1508", # invalid-envvar-default
|
||||
"PLW1509", # subprocess-popen-preexec-fn
|
||||
"PLW2101", # useless-with-lock
|
||||
"PLW3301", # nested-min-max
|
||||
]
|
||||
|
||||
ignore = [
|
||||
"PLR2004", # magic-value-comparison
|
||||
"UP030", # format literals
|
||||
"PLW1514", # unspecified-encoding
|
||||
"PLR0913", # too-many-arguments
|
||||
"PLR0915", # too-many-statements
|
||||
"PLR0917", # too-many-positional-arguments
|
||||
"PLR0904", # too-many-public-methods
|
||||
"PLR0912", # too-many-branches
|
||||
"PLR0916", # too-many-boolean-expressions
|
||||
"PLR6301", # could-be-static no-self-use
|
||||
"PLC0415", # import outside toplevel
|
||||
"PLC2701", # private import
|
||||
"PLR0911", # too many return
|
||||
"PLW1641", # object not implement hash
|
||||
"PLW0603", # global statement
|
||||
"PLC1901", # string falsey simplified
|
||||
]
|
||||
|
||||
[format]
|
||||
line-ending = "lf"
|
||||
Loading…
Reference in a new issue