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
|
# SPDX-License-Identifier: Unlicense
|
||||||
|
|
||||||
repos:
|
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
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
rev: v4.4.0
|
rev: v4.5.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: check-yaml
|
- id: check-yaml
|
||||||
- id: end-of-file-fixer
|
- id: end-of-file-fixer
|
||||||
- id: trailing-whitespace
|
- id: trailing-whitespace
|
||||||
- repo: https://github.com/pycqa/pylint
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
rev: v2.17.4
|
rev: v0.3.4
|
||||||
hooks:
|
hooks:
|
||||||
- id: pylint
|
- id: ruff-format
|
||||||
name: pylint (library code)
|
- id: ruff
|
||||||
types: [python]
|
args: ["--fix"]
|
||||||
args:
|
- repo: https://github.com/fsfe/reuse-tool
|
||||||
- --disable=consider-using-f-string
|
rev: v3.0.1
|
||||||
exclude: "^(docs/|examples/|tests/|setup.py$)"
|
hooks:
|
||||||
- id: pylint
|
- id: reuse
|
||||||
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
|
|
||||||
|
|
|
||||||
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
|
# Required
|
||||||
version: 2
|
version: 2
|
||||||
|
|
||||||
|
sphinx:
|
||||||
|
configuration: docs/conf.py
|
||||||
|
|
||||||
build:
|
build:
|
||||||
os: ubuntu-20.04
|
os: ubuntu-lts-latest
|
||||||
tools:
|
tools:
|
||||||
python: "3"
|
python: "3"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,9 +17,9 @@ Introduction
|
||||||
:alt: Build Status
|
:alt: Build Status
|
||||||
|
|
||||||
|
|
||||||
.. image:: https://img.shields.io/badge/code%20style-black-000000.svg
|
.. image:: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json
|
||||||
:target: https://github.com/psf/black
|
:target: https://github.com/astral-sh/ruff
|
||||||
:alt: Code Style: Black
|
:alt: Code Style: Ruff
|
||||||
|
|
||||||
HTTP Server for CircuitPython.
|
HTTP Server for CircuitPython.
|
||||||
|
|
||||||
|
|
@ -32,6 +32,7 @@ HTTP Server for CircuitPython.
|
||||||
- Supports URL parameters and wildcard URLs.
|
- Supports URL parameters and wildcard URLs.
|
||||||
- Supports HTTP Basic and Bearer Authentication on both server and route per level.
|
- Supports HTTP Basic and Bearer Authentication on both server and route per level.
|
||||||
- Supports Websockets and Server-Sent Events.
|
- Supports Websockets and Server-Sent Events.
|
||||||
|
- Limited support for HTTPS (only on selected microcontrollers with enough memory e.g. ESP32-S3).
|
||||||
|
|
||||||
|
|
||||||
Dependencies
|
Dependencies
|
||||||
|
|
|
||||||
|
|
@ -25,70 +25,70 @@ __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_HTTPServer.git"
|
||||||
|
|
||||||
from .authentication import (
|
from .authentication import (
|
||||||
Basic,
|
Basic,
|
||||||
Token,
|
|
||||||
Bearer,
|
Bearer,
|
||||||
|
Token,
|
||||||
check_authentication,
|
check_authentication,
|
||||||
require_authentication,
|
require_authentication,
|
||||||
)
|
)
|
||||||
from .exceptions import (
|
from .exceptions import (
|
||||||
ServerStoppedError,
|
|
||||||
AuthenticationError,
|
AuthenticationError,
|
||||||
|
BackslashInPathError,
|
||||||
|
FileNotExistsError,
|
||||||
InvalidPathError,
|
InvalidPathError,
|
||||||
ParentDirectoryReferenceError,
|
ParentDirectoryReferenceError,
|
||||||
BackslashInPathError,
|
ServerStoppedError,
|
||||||
ServingFilesDisabledError,
|
ServingFilesDisabledError,
|
||||||
FileNotExistsError,
|
|
||||||
)
|
)
|
||||||
from .headers import Headers
|
from .headers import Headers
|
||||||
from .methods import (
|
from .methods import (
|
||||||
GET,
|
CONNECT,
|
||||||
POST,
|
|
||||||
PUT,
|
|
||||||
DELETE,
|
DELETE,
|
||||||
PATCH,
|
GET,
|
||||||
HEAD,
|
HEAD,
|
||||||
OPTIONS,
|
OPTIONS,
|
||||||
|
PATCH,
|
||||||
|
POST,
|
||||||
|
PUT,
|
||||||
TRACE,
|
TRACE,
|
||||||
CONNECT,
|
|
||||||
)
|
)
|
||||||
from .mime_types import MIMETypes
|
from .mime_types import MIMETypes
|
||||||
from .request import QueryParams, FormData, Request
|
from .request import FormData, QueryParams, Request
|
||||||
from .response import (
|
from .response import (
|
||||||
Response,
|
|
||||||
FileResponse,
|
|
||||||
ChunkedResponse,
|
ChunkedResponse,
|
||||||
|
FileResponse,
|
||||||
JSONResponse,
|
JSONResponse,
|
||||||
Redirect,
|
Redirect,
|
||||||
|
Response,
|
||||||
SSEResponse,
|
SSEResponse,
|
||||||
Websocket,
|
Websocket,
|
||||||
)
|
)
|
||||||
from .route import Route, as_route
|
from .route import Route, as_route
|
||||||
from .server import (
|
from .server import (
|
||||||
Server,
|
|
||||||
NO_REQUEST,
|
|
||||||
CONNECTION_TIMED_OUT,
|
CONNECTION_TIMED_OUT,
|
||||||
|
NO_REQUEST,
|
||||||
REQUEST_HANDLED_NO_RESPONSE,
|
REQUEST_HANDLED_NO_RESPONSE,
|
||||||
REQUEST_HANDLED_RESPONSE_SENT,
|
REQUEST_HANDLED_RESPONSE_SENT,
|
||||||
|
Server,
|
||||||
)
|
)
|
||||||
from .status import (
|
from .status import (
|
||||||
Status,
|
|
||||||
SWITCHING_PROTOCOLS_101,
|
|
||||||
OK_200,
|
|
||||||
CREATED_201,
|
|
||||||
ACCEPTED_202,
|
ACCEPTED_202,
|
||||||
NO_CONTENT_204,
|
|
||||||
PARTIAL_CONTENT_206,
|
|
||||||
MOVED_PERMANENTLY_301,
|
|
||||||
FOUND_302,
|
|
||||||
TEMPORARY_REDIRECT_307,
|
|
||||||
PERMANENT_REDIRECT_308,
|
|
||||||
BAD_REQUEST_400,
|
BAD_REQUEST_400,
|
||||||
UNAUTHORIZED_401,
|
CREATED_201,
|
||||||
FORBIDDEN_403,
|
FORBIDDEN_403,
|
||||||
NOT_FOUND_404,
|
FOUND_302,
|
||||||
METHOD_NOT_ALLOWED_405,
|
|
||||||
TOO_MANY_REQUESTS_429,
|
|
||||||
INTERNAL_SERVER_ERROR_500,
|
INTERNAL_SERVER_ERROR_500,
|
||||||
|
METHOD_NOT_ALLOWED_405,
|
||||||
|
MOVED_PERMANENTLY_301,
|
||||||
|
NO_CONTENT_204,
|
||||||
|
NOT_FOUND_404,
|
||||||
NOT_IMPLEMENTED_501,
|
NOT_IMPLEMENTED_501,
|
||||||
|
OK_200,
|
||||||
|
PARTIAL_CONTENT_206,
|
||||||
|
PERMANENT_REDIRECT_308,
|
||||||
SERVICE_UNAVAILABLE_503,
|
SERVICE_UNAVAILABLE_503,
|
||||||
|
SWITCHING_PROTOCOLS_101,
|
||||||
|
TEMPORARY_REDIRECT_307,
|
||||||
|
TOO_MANY_REQUESTS_429,
|
||||||
|
UNAUTHORIZED_401,
|
||||||
|
Status,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
"""
|
"""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from typing import Union, List
|
from typing import List, Union
|
||||||
except ImportError:
|
except ImportError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
@ -40,15 +40,13 @@ class Token:
|
||||||
return f"{self.prefix} {self._value}"
|
return f"{self.prefix} {self._value}"
|
||||||
|
|
||||||
|
|
||||||
class Bearer(Token): # pylint: disable=too-few-public-methods
|
class Bearer(Token):
|
||||||
"""Represents HTTP Bearer Token Authentication."""
|
"""Represents HTTP Bearer Token Authentication."""
|
||||||
|
|
||||||
prefix = "Bearer"
|
prefix = "Bearer"
|
||||||
|
|
||||||
|
|
||||||
def check_authentication(
|
def check_authentication(request: Request, auths: List[Union[Basic, Token, Bearer]]) -> bool:
|
||||||
request: Request, auths: List[Union[Basic, Token, Bearer]]
|
|
||||||
) -> bool:
|
|
||||||
"""
|
"""
|
||||||
Returns ``True`` if request is authorized by any of the authentications, ``False`` otherwise.
|
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)
|
return any(auth_header == str(auth) for auth in auths)
|
||||||
|
|
||||||
|
|
||||||
def require_authentication(
|
def require_authentication(request: Request, auths: List[Union[Basic, Token, Bearer]]) -> None:
|
||||||
request: Request, auths: List[Union[Basic, Token, Bearer]]
|
|
||||||
) -> None:
|
|
||||||
"""
|
"""
|
||||||
Checks if the request is authorized and raises ``AuthenticationError`` if not.
|
Checks if the request is authorized and raises ``AuthenticationError`` if not.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -93,9 +93,7 @@ class Headers(_IFieldStorage):
|
||||||
return default
|
return default
|
||||||
return header_value.split(";")[0].strip('" ')
|
return header_value.split(";")[0].strip('" ')
|
||||||
|
|
||||||
def get_parameter(
|
def get_parameter(self, name: str, parameter: str, default: str = None) -> Union[str, None]:
|
||||||
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.
|
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]):
|
def update(self, headers: Dict[str, str]):
|
||||||
"""Updates the headers with the given dict."""
|
"""Updates the headers with the given dict."""
|
||||||
return self._storage.update(
|
return self._storage.update({key.lower(): [value] for key, value in headers.items()})
|
||||||
{key.lower(): [value] for key, value in headers.items()}
|
|
||||||
)
|
|
||||||
|
|
||||||
def copy(self):
|
def copy(self):
|
||||||
"""Returns a copy of the headers."""
|
"""Returns a copy of the headers."""
|
||||||
return Headers(
|
return Headers(
|
||||||
"\r\n".join(
|
"\r\n".join(f"{key}: {value}" for key in self.fields for value in self.get_list(key))
|
||||||
f"{key}: {value}" for key in self.fields for value in self.get_list(key)
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def __getitem__(self, name: str):
|
def __getitem__(self, name: str):
|
||||||
|
|
|
||||||
|
|
@ -8,11 +8,59 @@
|
||||||
"""
|
"""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from typing import List, Dict, Union, Any
|
from typing import Any, Dict, List, Tuple, Union
|
||||||
except ImportError:
|
except ImportError:
|
||||||
pass
|
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:
|
class _IFieldStorage:
|
||||||
"""Interface with shared methods for QueryParams, FormData and Headers."""
|
"""Interface with shared methods for QueryParams, FormData and Headers."""
|
||||||
|
|
||||||
|
|
@ -62,7 +110,7 @@ class _IFieldStorage:
|
||||||
return key in self._storage
|
return key in self._storage
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
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]:
|
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):
|
class _IXSSSafeFieldStorage(_IFieldStorage):
|
||||||
def get(
|
def get(self, field_name: str, default: Any = None, *, safe=True) -> Union[Any, None]:
|
||||||
self, field_name: str, default: Any = None, *, safe=True
|
|
||||||
) -> Union[Any, None]:
|
|
||||||
if safe:
|
if safe:
|
||||||
return _encode_html_entities(super().get(field_name, default))
|
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]:
|
def get_list(self, field_name: str, *, safe=True) -> List[Any]:
|
||||||
if safe:
|
if safe:
|
||||||
return [
|
return [_encode_html_entities(value) for value in super().get_list(field_name)]
|
||||||
_encode_html_entities(value) for value in super().get_list(field_name)
|
|
||||||
]
|
|
||||||
|
|
||||||
_debug_warning_nonencoded_output()
|
_debug_warning_nonencoded_output()
|
||||||
return super().get_list(field_name)
|
return super().get_list(field_name)
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@
|
||||||
* Author(s): Michał Pokusa
|
* Author(s): Michał Pokusa
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
GET = "GET"
|
GET = "GET"
|
||||||
|
|
||||||
POST = "POST"
|
POST = "POST"
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
"""
|
"""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from typing import List, Dict
|
from typing import Dict, List
|
||||||
except ImportError:
|
except ImportError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,7 @@
|
||||||
"""
|
"""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from typing import List, Dict, Tuple, Union, Any, TYPE_CHECKING
|
from typing import TYPE_CHECKING, Any, Dict, List, Tuple, Union
|
||||||
from socket import socket
|
|
||||||
from socketpool import SocketPool
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .server import Server
|
from .server import Server
|
||||||
|
|
@ -20,8 +18,8 @@ except ImportError:
|
||||||
import json
|
import json
|
||||||
|
|
||||||
from .headers import Headers
|
from .headers import Headers
|
||||||
from .interfaces import _IFieldStorage, _IXSSSafeFieldStorage
|
from .interfaces import _IFieldStorage, _ISocket, _IXSSSafeFieldStorage
|
||||||
from .methods import POST, PUT, PATCH, DELETE
|
from .methods import DELETE, PATCH, POST, PUT
|
||||||
|
|
||||||
|
|
||||||
class QueryParams(_IXSSSafeFieldStorage):
|
class QueryParams(_IXSSSafeFieldStorage):
|
||||||
|
|
@ -56,9 +54,7 @@ class QueryParams(_IXSSSafeFieldStorage):
|
||||||
def _add_field_value(self, field_name: str, value: str) -> None:
|
def _add_field_value(self, field_name: str, value: str) -> None:
|
||||||
super()._add_field_value(field_name, value)
|
super()._add_field_value(field_name, value)
|
||||||
|
|
||||||
def get(
|
def get(self, field_name: str, default: str = None, *, safe=True) -> Union[str, None]:
|
||||||
self, field_name: str, default: str = None, *, safe=True
|
|
||||||
) -> Union[str, None]:
|
|
||||||
return super().get(field_name, default, safe=safe)
|
return super().get(field_name, default, safe=safe)
|
||||||
|
|
||||||
def get_list(self, field_name: str, *, safe=True) -> List[str]:
|
def get_list(self, field_name: str, *, safe=True) -> List[str]:
|
||||||
|
|
@ -94,9 +90,7 @@ class File:
|
||||||
content: Union[str, bytes]
|
content: Union[str, bytes]
|
||||||
"""Content of the file."""
|
"""Content of the file."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(self, filename: str, content_type: str, content: Union[str, bytes]) -> None:
|
||||||
self, filename: str, content_type: str, content: Union[str, bytes]
|
|
||||||
) -> None:
|
|
||||||
self.filename = filename
|
self.filename = filename
|
||||||
self.content_type = content_type
|
self.content_type = content_type
|
||||||
self.content = content
|
self.content = content
|
||||||
|
|
@ -114,11 +108,7 @@ class File:
|
||||||
with open(file.filename, "wb") as f:
|
with open(file.filename, "wb") as f:
|
||||||
f.write(file.content_bytes)
|
f.write(file.content_bytes)
|
||||||
"""
|
"""
|
||||||
return (
|
return self.content.encode("utf-8") if isinstance(self.content, str) else self.content
|
||||||
self.content.encode("utf-8")
|
|
||||||
if isinstance(self.content, str)
|
|
||||||
else self.content
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def size(self) -> int:
|
def size(self) -> int:
|
||||||
|
|
@ -127,11 +117,11 @@ class File:
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
filename, content_type, size = (
|
filename, content_type, size = (
|
||||||
repr(self.filename),
|
self.filename,
|
||||||
repr(self.content_type),
|
self.content_type,
|
||||||
repr(self.size),
|
self.size,
|
||||||
)
|
)
|
||||||
return f"{self.__class__.__name__}({filename=}, {content_type=}, {size=})"
|
return f"<{self.__class__.__name__} {filename=}, {content_type=}, {size=}>"
|
||||||
|
|
||||||
|
|
||||||
class Files(_IFieldStorage):
|
class Files(_IFieldStorage):
|
||||||
|
|
@ -179,11 +169,11 @@ class FormData(_IXSSSafeFieldStorage):
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _check_is_supported_content_type(content_type: str) -> None:
|
def _check_is_supported_content_type(content_type: str) -> None:
|
||||||
return content_type in (
|
return content_type in {
|
||||||
"application/x-www-form-urlencoded",
|
"application/x-www-form-urlencoded",
|
||||||
"multipart/form-data",
|
"multipart/form-data",
|
||||||
"text/plain",
|
"text/plain",
|
||||||
)
|
}
|
||||||
|
|
||||||
def __init__(self, data: bytes, headers: Headers, *, debug: bool = False) -> None:
|
def __init__(self, data: bytes, headers: Headers, *, debug: bool = False) -> None:
|
||||||
self._storage = {}
|
self._storage = {}
|
||||||
|
|
@ -233,9 +223,7 @@ class FormData(_IXSSSafeFieldStorage):
|
||||||
# TODO: Other text content types (e.g. application/json) should be decoded as well and
|
# TODO: Other text content types (e.g. application/json) should be decoded as well and
|
||||||
|
|
||||||
if filename is not None:
|
if filename is not None:
|
||||||
self.files._add_field_value( # pylint: disable=protected-access
|
self.files._add_field_value(field_name, File(filename, content_type, value))
|
||||||
field_name, File(filename, content_type, value)
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
self._add_field_value(field_name, value)
|
self._add_field_value(field_name, value)
|
||||||
|
|
||||||
|
|
@ -260,10 +248,10 @@ class FormData(_IXSSSafeFieldStorage):
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
class_name = self.__class__.__name__
|
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.
|
Incoming request, constructed from raw incoming bytes.
|
||||||
It is passed as first argument to all route handlers.
|
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.
|
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.
|
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__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
server: "Server",
|
server: "Server",
|
||||||
connection: Union["SocketPool.Socket", "socket.socket"],
|
connection: _ISocket,
|
||||||
client_address: Tuple[str, int],
|
client_address: Tuple[str, int],
|
||||||
raw_request: bytes = None,
|
raw_request: bytes = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
@ -367,9 +355,7 @@ class Request: # pylint: disable=too-many-instance-attributes
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: value.strip('"')
|
name: value.strip('"')
|
||||||
for name, value in [
|
for name, value in [cookie.strip().split("=", 1) for cookie in cookie_header.split(";")]
|
||||||
cookie.strip().split("=", 1) for cookie in cookie_header.split(";")
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|
@ -443,7 +429,7 @@ class Request: # pylint: disable=too-many-instance-attributes
|
||||||
"""
|
"""
|
||||||
return (
|
return (
|
||||||
json.loads(self.body)
|
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
|
else None
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -467,9 +453,7 @@ class Request: # pylint: disable=too-many-instance-attributes
|
||||||
) -> Tuple[str, str, QueryParams, str, Headers]:
|
) -> Tuple[str, str, QueryParams, str, Headers]:
|
||||||
"""Parse HTTP Start line to method, path, query_params and http_version."""
|
"""Parse HTTP Start line to method, path, query_params and http_version."""
|
||||||
|
|
||||||
start_line, headers_string = (
|
start_line, headers_string = header_bytes.decode("utf-8").strip().split("\r\n", 1)
|
||||||
header_bytes.decode("utf-8").strip().split("\r\n", 1)
|
|
||||||
)
|
|
||||||
|
|
||||||
method, path, http_version = start_line.strip().split()
|
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
|
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:
|
def _debug_unsupported_form_content_type(content_type: str) -> None:
|
||||||
"""Warns when an unsupported form content type is used."""
|
"""Warns when an unsupported form content type is used."""
|
||||||
|
|
|
||||||
|
|
@ -8,16 +8,14 @@
|
||||||
"""
|
"""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from typing import Optional, Dict, Union, Tuple, Generator, Any
|
from typing import Any, Dict, Generator, Optional, Tuple, Union
|
||||||
from socket import socket
|
|
||||||
from socketpool import SocketPool
|
|
||||||
except ImportError:
|
except ImportError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
import os
|
|
||||||
import json
|
import json
|
||||||
|
import os
|
||||||
from binascii import b2a_base64
|
from binascii import b2a_base64
|
||||||
from errno import EAGAIN, ECONNRESET, ETIMEDOUT, ENOTCONN
|
from errno import EAGAIN, ECONNRESET, ENOTCONN, ETIMEDOUT
|
||||||
|
|
||||||
try:
|
try:
|
||||||
try:
|
try:
|
||||||
|
|
@ -35,21 +33,22 @@ from .exceptions import (
|
||||||
FileNotExistsError,
|
FileNotExistsError,
|
||||||
ParentDirectoryReferenceError,
|
ParentDirectoryReferenceError,
|
||||||
)
|
)
|
||||||
|
from .headers import Headers
|
||||||
|
from .interfaces import _ISocket
|
||||||
from .mime_types import MIMETypes
|
from .mime_types import MIMETypes
|
||||||
from .request import Request
|
from .request import Request
|
||||||
from .status import (
|
from .status import (
|
||||||
Status,
|
|
||||||
SWITCHING_PROTOCOLS_101,
|
|
||||||
OK_200,
|
|
||||||
MOVED_PERMANENTLY_301,
|
|
||||||
FOUND_302,
|
FOUND_302,
|
||||||
TEMPORARY_REDIRECT_307,
|
MOVED_PERMANENTLY_301,
|
||||||
|
OK_200,
|
||||||
PERMANENT_REDIRECT_308,
|
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.
|
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")
|
return Response(request, body='Some content', content_type="text/plain")
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__( # pylint: disable=too-many-arguments
|
def __init__(
|
||||||
self,
|
self,
|
||||||
request: Request,
|
request: Request,
|
||||||
body: Union[str, bytes] = "",
|
body: Union[str, bytes] = "",
|
||||||
|
|
@ -85,9 +84,7 @@ class Response: # pylint: disable=too-few-public-methods
|
||||||
self._request = request
|
self._request = request
|
||||||
self._body = body
|
self._body = body
|
||||||
self._status = status if isinstance(status, Status) else Status(*status)
|
self._status = status if isinstance(status, Status) else Status(*status)
|
||||||
self._headers = (
|
self._headers = headers.copy() if isinstance(headers, Headers) else Headers(headers)
|
||||||
headers.copy() if isinstance(headers, Headers) else Headers(headers)
|
|
||||||
)
|
|
||||||
self._cookies = cookies.copy() if cookies else {}
|
self._cookies = cookies.copy() if cookies else {}
|
||||||
self._content_type = content_type
|
self._content_type = content_type
|
||||||
self._size = 0
|
self._size = 0
|
||||||
|
|
@ -99,13 +96,9 @@ class Response: # pylint: disable=too-few-public-methods
|
||||||
) -> None:
|
) -> None:
|
||||||
headers = self._headers.copy()
|
headers = self._headers.copy()
|
||||||
|
|
||||||
response_message_header = (
|
response_message_header = f"HTTP/1.1 {self._status.code} {self._status.text}\r\n"
|
||||||
f"HTTP/1.1 {self._status.code} {self._status.text}\r\n"
|
|
||||||
)
|
|
||||||
|
|
||||||
headers.setdefault(
|
headers.setdefault("Content-Type", content_type or self._content_type or MIMETypes.DEFAULT)
|
||||||
"Content-Type", content_type or self._content_type or MIMETypes.DEFAULT
|
|
||||||
)
|
|
||||||
headers.setdefault("Content-Length", content_length)
|
headers.setdefault("Content-Length", content_length)
|
||||||
headers.setdefault("Connection", "close")
|
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 += f"{header}: {value}\r\n"
|
||||||
response_message_header += "\r\n"
|
response_message_header += "\r\n"
|
||||||
|
|
||||||
self._send_bytes(
|
self._send_bytes(self._request.connection, response_message_header.encode("utf-8"))
|
||||||
self._request.connection, response_message_header.encode("utf-8")
|
|
||||||
)
|
|
||||||
|
|
||||||
def _send(self) -> None:
|
def _send(self) -> None:
|
||||||
encoded_body = (
|
encoded_body = self._body.encode("utf-8") if isinstance(self._body, str) else self._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_headers(len(encoded_body), self._content_type)
|
||||||
self._send_bytes(self._request.connection, encoded_body)
|
self._send_bytes(self._request.connection, encoded_body)
|
||||||
|
|
@ -132,7 +121,7 @@ class Response: # pylint: disable=too-few-public-methods
|
||||||
|
|
||||||
def _send_bytes(
|
def _send_bytes(
|
||||||
self,
|
self,
|
||||||
conn: Union["SocketPool.Socket", "socket.socket"],
|
conn: _ISocket,
|
||||||
buffer: Union[bytes, bytearray, memoryview],
|
buffer: Union[bytes, bytearray, memoryview],
|
||||||
):
|
):
|
||||||
bytes_sent: int = 0
|
bytes_sent: int = 0
|
||||||
|
|
@ -156,7 +145,7 @@ class Response: # pylint: disable=too-few-public-methods
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class FileResponse(Response): # pylint: disable=too-few-public-methods
|
class FileResponse(Response):
|
||||||
"""
|
"""
|
||||||
Specialized version of `Response` class for sending files.
|
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')
|
return FileResponse(request, filename='index.html', root_path='/www')
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__( # pylint: disable=too-many-arguments
|
def __init__(
|
||||||
self,
|
self,
|
||||||
request: Request,
|
request: Request,
|
||||||
filename: str = "index.html",
|
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._filename = filename + "index.html" if filename.endswith("/") else filename
|
||||||
self._root_path = root_path or self._request.server.root_path
|
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._full_file_path = self._combine_path(self._root_path, self._filename)
|
||||||
self._content_type = content_type or MIMETypes.get_for_filename(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)
|
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
|
# Check for backslashes
|
||||||
if "\\" in file_path: # pylint: disable=anomalous-backslash-in-string
|
if "\\" in file_path:
|
||||||
raise BackslashInPathError(file_path)
|
raise BackslashInPathError(file_path)
|
||||||
|
|
||||||
# Check each component of the path for parent directory references
|
# 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
|
assert (st_mode & 0o170000) == 0o100000 # Check if it is a regular file
|
||||||
return st_size
|
return st_size
|
||||||
except (OSError, AssertionError):
|
except (OSError, AssertionError):
|
||||||
raise FileNotExistsError(file_path) # pylint: disable=raise-missing-from
|
raise FileNotExistsError(file_path)
|
||||||
|
|
||||||
def _send(self) -> None:
|
def _send(self) -> None:
|
||||||
self._send_headers(self._file_length, self._content_type)
|
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()
|
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.
|
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")
|
return ChunkedResponse(request, body, content_type="text/plain")
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__( # pylint: disable=too-many-arguments
|
def __init__(
|
||||||
self,
|
self,
|
||||||
request: Request,
|
request: Request,
|
||||||
body: Generator[Union[str, bytes], Any, Any],
|
body: Generator[Union[str, bytes], Any, Any],
|
||||||
|
|
@ -353,7 +346,7 @@ class ChunkedResponse(Response): # pylint: disable=too-few-public-methods
|
||||||
self._close_connection()
|
self._close_connection()
|
||||||
|
|
||||||
|
|
||||||
class JSONResponse(Response): # pylint: disable=too-few-public-methods
|
class JSONResponse(Response):
|
||||||
"""
|
"""
|
||||||
Specialized version of `Response` class for sending JSON data.
|
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()
|
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.
|
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):
|
if status is not None and (permanent or preserve_method):
|
||||||
raise ValueError(
|
raise ValueError("Cannot specify both status and permanent/preserve_method argument")
|
||||||
"Cannot specify both status and permanent/preserve_method argument"
|
|
||||||
)
|
|
||||||
|
|
||||||
if status is None:
|
if status is None:
|
||||||
if preserve_method:
|
if preserve_method:
|
||||||
|
|
@ -463,7 +454,7 @@ class Redirect(Response): # pylint: disable=too-few-public-methods
|
||||||
self._close_connection()
|
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.
|
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()
|
sse.close()
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__( # pylint: disable=too-many-arguments
|
def __init__(
|
||||||
self,
|
self,
|
||||||
request: Request,
|
request: Request,
|
||||||
headers: Union[Headers, Dict[str, str]] = None,
|
headers: Union[Headers, Dict[str, str]] = None,
|
||||||
|
|
@ -520,11 +511,11 @@ class SSEResponse(Response): # pylint: disable=too-few-public-methods
|
||||||
def _send(self) -> None:
|
def _send(self) -> None:
|
||||||
self._send_headers()
|
self._send_headers()
|
||||||
|
|
||||||
def send_event( # pylint: disable=too-many-arguments
|
def send_event(
|
||||||
self,
|
self,
|
||||||
data: str,
|
data: str,
|
||||||
event: str = None,
|
event: str = None,
|
||||||
id: int = None, # pylint: disable=redefined-builtin,invalid-name
|
id: int = None,
|
||||||
retry: int = None,
|
retry: int = None,
|
||||||
custom_fields: Dict[str, str] = None,
|
custom_fields: Dict[str, str] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
@ -561,7 +552,7 @@ class SSEResponse(Response): # pylint: disable=too-few-public-methods
|
||||||
self._close_connection()
|
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.
|
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()
|
return b2a_base64(response_key.digest()).strip().decode()
|
||||||
|
|
||||||
def __init__( # pylint: disable=too-many-arguments
|
def __init__(
|
||||||
self,
|
self,
|
||||||
request: Request,
|
request: Request,
|
||||||
headers: Union[Headers, Dict[str, str]] = None,
|
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:
|
if fin != Websocket.FIN and opcode == Websocket.CONT:
|
||||||
return Websocket.CONT, None
|
return Websocket.CONT, None
|
||||||
|
|
||||||
payload = bytes()
|
payload = b""
|
||||||
if fin == Websocket.FIN and opcode == Websocket.CLOSE:
|
if fin == Websocket.FIN and opcode == Websocket.CLOSE:
|
||||||
return Websocket.CLOSE, payload
|
return Websocket.CLOSE, payload
|
||||||
|
|
||||||
|
|
@ -708,7 +699,7 @@ class Websocket(Response): # pylint: disable=too-few-public-methods
|
||||||
length -= min(payload_length, length)
|
length -= min(payload_length, length)
|
||||||
|
|
||||||
if has_mask:
|
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
|
return opcode, payload
|
||||||
|
|
||||||
|
|
@ -743,9 +734,7 @@ class Websocket(Response): # pylint: disable=too-few-public-methods
|
||||||
if self.closed:
|
if self.closed:
|
||||||
if fail_silently:
|
if fail_silently:
|
||||||
return None
|
return None
|
||||||
raise RuntimeError(
|
raise RuntimeError("Websocket connection is closed, cannot receive messages")
|
||||||
"Websocket connection is closed, cannot receive messages"
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
opcode, payload = self._read_frame()
|
opcode, payload = self._read_frame()
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
"""
|
"""
|
||||||
|
|
||||||
try:
|
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:
|
if TYPE_CHECKING:
|
||||||
from .response import Response
|
from .response import Response
|
||||||
|
|
@ -52,9 +52,7 @@ class Route:
|
||||||
self._validate_path(path, append_slash)
|
self._validate_path(path, append_slash)
|
||||||
|
|
||||||
self.path = path
|
self.path = path
|
||||||
self.methods = (
|
self.methods = set(methods) if isinstance(methods, (set, list, tuple)) else set([methods])
|
||||||
set(methods) if isinstance(methods, (set, list, tuple)) else set([methods])
|
|
||||||
)
|
|
||||||
self.handler = handler
|
self.handler = handler
|
||||||
self.parameters_names = [
|
self.parameters_names = [
|
||||||
name[1:-1] for name in re.compile(r"/[^<>]*/?").split(path) if name != ""
|
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))
|
return True, dict(zip(self.parameters_names, url_parameters_values))
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
path = repr(self.path)
|
path = self.path
|
||||||
methods = repr(self.methods)
|
methods = self.methods
|
||||||
handler = repr(self.handler)
|
handler = self.handler
|
||||||
|
|
||||||
return f"Route({path=}, {methods=}, {handler=})"
|
return f"<Route {path=}, {methods=}, {handler=}>"
|
||||||
|
|
||||||
|
|
||||||
def as_route(
|
def as_route(
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,7 @@
|
||||||
"""
|
"""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from typing import Callable, Protocol, Union, List, Tuple, Dict, Iterable
|
from typing import Callable, Dict, Iterable, List, Tuple, Union
|
||||||
from socket import socket
|
|
||||||
from socketpool import SocketPool
|
|
||||||
except ImportError:
|
except ImportError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
@ -19,20 +17,36 @@ from sys import implementation
|
||||||
from time import monotonic, sleep
|
from time import monotonic, sleep
|
||||||
from traceback import print_exception
|
from traceback import print_exception
|
||||||
|
|
||||||
from .authentication import Basic, Token, Bearer, require_authentication
|
from .authentication import Basic, Bearer, Token, require_authentication
|
||||||
from .exceptions import (
|
from .exceptions import (
|
||||||
ServerStoppedError,
|
|
||||||
AuthenticationError,
|
AuthenticationError,
|
||||||
FileNotExistsError,
|
FileNotExistsError,
|
||||||
InvalidPathError,
|
InvalidPathError,
|
||||||
|
ServerStoppedError,
|
||||||
ServingFilesDisabledError,
|
ServingFilesDisabledError,
|
||||||
)
|
)
|
||||||
from .headers import Headers
|
from .headers import Headers
|
||||||
|
from .interfaces import _ISocket, _ISocketPool
|
||||||
from .methods import GET, HEAD
|
from .methods import GET, HEAD
|
||||||
from .request import Request
|
from .request import Request
|
||||||
from .response import Response, FileResponse
|
from .response import FileResponse, Response
|
||||||
from .route import Route
|
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"
|
NO_REQUEST = "no_request"
|
||||||
|
|
@ -40,8 +54,11 @@ CONNECTION_TIMED_OUT = "connection_timed_out"
|
||||||
REQUEST_HANDLED_NO_RESPONSE = "request_handled_no_response"
|
REQUEST_HANDLED_NO_RESPONSE = "request_handled_no_response"
|
||||||
REQUEST_HANDLED_RESPONSE_SENT = "request_handled_response_sent"
|
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."""
|
"""A basic socket-based HTTP server."""
|
||||||
|
|
||||||
host: str
|
host: str
|
||||||
|
|
@ -53,8 +70,50 @@ class Server: # pylint: disable=too-many-instance-attributes
|
||||||
root_path: str
|
root_path: str
|
||||||
"""Root directory to serve files from. ``None`` if serving files is disabled."""
|
"""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__(
|
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:
|
) -> None:
|
||||||
"""Create a server, and get it ready to run.
|
"""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.
|
in CircuitPython or the `socket` module in CPython.
|
||||||
:param str root_path: Root directory to serve files from
|
:param str root_path: Root directory to serve files from
|
||||||
:param bool debug: Enables debug messages useful during development
|
: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._buffer = bytearray(1024)
|
||||||
self._timeout = 1
|
self._timeout = 1
|
||||||
|
|
||||||
|
self._auths = []
|
||||||
self._routes: "List[Route]" = []
|
self._routes: "List[Route]" = []
|
||||||
|
self.headers = Headers()
|
||||||
|
|
||||||
self._socket_source = socket_source
|
self._socket_source = socket_source
|
||||||
self._sock = None
|
self._sock = None
|
||||||
self.headers = Headers()
|
|
||||||
self.host, self.port = None, None
|
self.host, self.port = None, None
|
||||||
self.root_path = root_path
|
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)
|
_debug_warning_exposed_files(root_path)
|
||||||
self.stopped = True
|
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
|
raise RuntimeError(f"Cannot start server on {host}:{port}") from error
|
||||||
|
|
||||||
def serve_forever(
|
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:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Wait for HTTP requests at the given host and port. Does not return.
|
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
|
except KeyboardInterrupt: # Exit on Ctrl-C e.g. during development
|
||||||
self.stop()
|
self.stop()
|
||||||
return
|
return
|
||||||
except Exception: # pylint: disable=broad-except
|
except Exception:
|
||||||
pass # Ignore exceptions in handler function
|
pass # Ignore exceptions in handler function
|
||||||
|
|
||||||
def _set_socket_level_to_reuse_address(self) -> None:
|
@staticmethod
|
||||||
"""
|
def _create_server_socket(
|
||||||
Only for CPython, prevents "Address already in use" error when restarting the server.
|
socket_source: _ISocketPool,
|
||||||
"""
|
ssl_context: "SSLContext | None",
|
||||||
self._sock.setsockopt(
|
host: str,
|
||||||
self._socket_source.SOL_SOCKET, self._socket_source.SO_REUSEADDR, 1
|
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
|
Start the HTTP server at the given host and port. Requires calling
|
||||||
``.poll()`` in a while loop to handle incoming requests.
|
``.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.host, self.port = host, port
|
||||||
|
|
||||||
self.stopped = False
|
self.stopped = False
|
||||||
self._sock = self._socket_source.socket(
|
self._sock = self._create_server_socket(self._socket_source, self._ssl_context, host, port)
|
||||||
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
|
|
||||||
|
|
||||||
if self.debug:
|
if self.debug:
|
||||||
_debug_started_server(self)
|
_debug_started_server(self)
|
||||||
|
|
@ -244,11 +324,9 @@ class Server: # pylint: disable=too-many-instance-attributes
|
||||||
if self.debug:
|
if self.debug:
|
||||||
_debug_stopped_server(self)
|
_debug_stopped_server(self)
|
||||||
|
|
||||||
def _receive_header_bytes(
|
def _receive_header_bytes(self, sock: _ISocket) -> bytes:
|
||||||
self, sock: Union["SocketPool.Socket", "socket.socket"]
|
|
||||||
) -> bytes:
|
|
||||||
"""Receive bytes until a empty line is received."""
|
"""Receive bytes until a empty line is received."""
|
||||||
received_bytes = bytes()
|
received_bytes = b""
|
||||||
while b"\r\n\r\n" not in received_bytes:
|
while b"\r\n\r\n" not in received_bytes:
|
||||||
try:
|
try:
|
||||||
length = sock.recv_into(self._buffer, len(self._buffer))
|
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(
|
def _receive_body_bytes(
|
||||||
self,
|
self,
|
||||||
sock: Union["SocketPool.Socket", "socket.socket"],
|
sock: _ISocket,
|
||||||
received_body_bytes: bytes,
|
received_body_bytes: bytes,
|
||||||
content_length: int,
|
content_length: int,
|
||||||
) -> bytes:
|
) -> bytes:
|
||||||
|
|
@ -282,7 +360,7 @@ class Server: # pylint: disable=too-many-instance-attributes
|
||||||
|
|
||||||
def _receive_request(
|
def _receive_request(
|
||||||
self,
|
self,
|
||||||
sock: Union["SocketPool.Socket", "socket.socket"],
|
sock: _ISocket,
|
||||||
client_address: Tuple[str, int],
|
client_address: Tuple[str, int],
|
||||||
) -> Request:
|
) -> Request:
|
||||||
"""Receive bytes from socket until the whole request is received."""
|
"""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
|
received_body_bytes = request.body
|
||||||
|
|
||||||
# Receiving remaining body bytes
|
# Receiving remaining body bytes
|
||||||
request.body = self._receive_body_bytes(
|
request.body = self._receive_body_bytes(sock, received_body_bytes, content_length)
|
||||||
sock, received_body_bytes, content_length
|
|
||||||
)
|
|
||||||
|
|
||||||
return request
|
return request
|
||||||
|
|
||||||
def _find_handler( # pylint: disable=cell-var-from-loop
|
def _find_handler(self, method: str, path: str) -> Union[Callable[..., "Response"], None]:
|
||||||
self, method: str, path: str
|
|
||||||
) -> Union[Callable[..., "Response"], None]:
|
|
||||||
"""
|
"""
|
||||||
Finds a handler for a given route.
|
Finds a handler for a given route.
|
||||||
|
|
||||||
|
|
@ -352,7 +426,7 @@ class Server: # pylint: disable=too-many-instance-attributes
|
||||||
raise ServingFilesDisabledError
|
raise ServingFilesDisabledError
|
||||||
|
|
||||||
# Method is GET or HEAD, try to serve a file from the filesystem.
|
# 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(
|
return FileResponse(
|
||||||
request,
|
request,
|
||||||
filename=request.path,
|
filename=request.path,
|
||||||
|
|
@ -384,11 +458,11 @@ class Server: # pylint: disable=too-many-instance-attributes
|
||||||
|
|
||||||
def _set_default_server_headers(self, response: Response) -> None:
|
def _set_default_server_headers(self, response: Response) -> None:
|
||||||
for name, value in self.headers.items():
|
for name, value in self.headers.items():
|
||||||
response._headers.setdefault( # pylint: disable=protected-access
|
response._headers.setdefault(name, value)
|
||||||
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
|
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.
|
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
|
conn = None
|
||||||
try:
|
try:
|
||||||
|
if self.debug:
|
||||||
|
_debug_start_time = monotonic()
|
||||||
|
|
||||||
conn, client_address = self._sock.accept()
|
conn, client_address = self._sock.accept()
|
||||||
conn.settimeout(self._timeout)
|
conn.settimeout(self._timeout)
|
||||||
|
|
||||||
_debug_start_time = monotonic()
|
|
||||||
|
|
||||||
# Receive the whole request
|
# Receive the whole request
|
||||||
if (request := self._receive_request(conn, client_address)) is None:
|
if (request := self._receive_request(conn, client_address)) is None:
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
@ -424,16 +499,15 @@ class Server: # pylint: disable=too-many-instance-attributes
|
||||||
self._set_default_server_headers(response)
|
self._set_default_server_headers(response)
|
||||||
|
|
||||||
# Send the response
|
# Send the response
|
||||||
response._send() # pylint: disable=protected-access
|
response._send()
|
||||||
|
|
||||||
_debug_end_time = monotonic()
|
|
||||||
|
|
||||||
if self.debug:
|
if self.debug:
|
||||||
|
_debug_end_time = monotonic()
|
||||||
_debug_response_sent(response, _debug_end_time - _debug_start_time)
|
_debug_response_sent(response, _debug_end_time - _debug_start_time)
|
||||||
|
|
||||||
return REQUEST_HANDLED_RESPONSE_SENT
|
return REQUEST_HANDLED_RESPONSE_SENT
|
||||||
|
|
||||||
except Exception as error: # pylint: disable=broad-except
|
except Exception as error:
|
||||||
if isinstance(error, OSError):
|
if isinstance(error, OSError):
|
||||||
# There is no data available right now, try again later.
|
# There is no data available right now, try again later.
|
||||||
if error.errno == EAGAIN:
|
if error.errno == EAGAIN:
|
||||||
|
|
@ -441,6 +515,15 @@ class Server: # pylint: disable=too-many-instance-attributes
|
||||||
# Connection reset by peer, try again later.
|
# Connection reset by peer, try again later.
|
||||||
if error.errno == ECONNRESET:
|
if error.errno == ECONNRESET:
|
||||||
return NO_REQUEST
|
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:
|
if self.debug:
|
||||||
_debug_exception_in_handler(error)
|
_debug_exception_in_handler(error)
|
||||||
|
|
@ -530,26 +613,33 @@ class Server: # pylint: disable=too-many-instance-attributes
|
||||||
else:
|
else:
|
||||||
raise ValueError("Server.socket_timeout must be a positive numeric value.")
|
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):
|
def _debug_warning_exposed_files(root_path: str):
|
||||||
"""Warns about exposing all files on the device."""
|
"""Warns about exposing all files on the device."""
|
||||||
print(
|
print(
|
||||||
f"WARNING: Setting root_path to '{root_path}' will expose all files on your device through"
|
f"WARNING: Setting root_path to '{root_path}' will expose all files on your device "
|
||||||
" the webserver, including potentially sensitive files like settings.toml or secrets.py. "
|
"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."
|
"Consider making a sub-directory on your device and using that for your root_path instead."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _debug_started_server(server: "Server"):
|
def _debug_started_server(server: "Server"):
|
||||||
"""Prints a message when the server starts."""
|
"""Prints a message when the server starts."""
|
||||||
|
scheme = "https" if server.https else "http"
|
||||||
host, port = server.host, server.port
|
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):
|
def _debug_response_sent(response: "Response", time_elapsed: float):
|
||||||
"""Prints a message after a response is sent."""
|
"""Prints a message after a response is sent."""
|
||||||
# pylint: disable=protected-access
|
|
||||||
client_ip = response._request.client_address[0]
|
client_ip = response._request.client_address[0]
|
||||||
method = response._request.method
|
method = response._request.method
|
||||||
query_params = response._request.query_params
|
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."""
|
"""Prints a message after the server stops."""
|
||||||
print("Stopped development server")
|
print("Stopped development server")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
class Status: # pylint: disable=too-few-public-methods
|
class Status:
|
||||||
"""HTTP status code."""
|
"""HTTP status code."""
|
||||||
|
|
||||||
def __init__(self, code: int, text: str):
|
def __init__(self, code: int, text: str):
|
||||||
|
|
@ -21,14 +21,17 @@ class Status: # pylint: disable=too-few-public-methods
|
||||||
self.code = code
|
self.code = code
|
||||||
self.text = text
|
self.text = text
|
||||||
|
|
||||||
def __repr__(self):
|
def __eq__(self, other: "Status"):
|
||||||
return f'Status({self.code}, "{self.text}")'
|
return self.code == other.code and self.text == other.text
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.code} {self.text}"
|
return f"{self.code} {self.text}"
|
||||||
|
|
||||||
def __eq__(self, other: "Status"):
|
def __repr__(self):
|
||||||
return self.code == other.code and self.text == other.text
|
code = self.code
|
||||||
|
text = self.text
|
||||||
|
|
||||||
|
return f'<Status {code}, "{text}">'
|
||||||
|
|
||||||
|
|
||||||
SWITCHING_PROTOCOLS_101 = Status(101, "Switching Protocols")
|
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)
|
.. 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"
|
.. use this format as the module name: "adafruit_foo.foo"
|
||||||
|
|
||||||
|
API Reference
|
||||||
|
#############
|
||||||
|
|
||||||
.. automodule:: adafruit_httpserver
|
.. automodule:: adafruit_httpserver
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,10 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
# SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries
|
# SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries
|
||||||
#
|
#
|
||||||
# SPDX-License-Identifier: MIT
|
# SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
import datetime
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import datetime
|
|
||||||
|
|
||||||
sys.path.insert(0, os.path.abspath(".."))
|
sys.path.insert(0, os.path.abspath(".."))
|
||||||
|
|
||||||
|
|
@ -54,9 +52,7 @@ project = "Adafruit CircuitPython HTTPServer Library"
|
||||||
creation_year = "2022"
|
creation_year = "2022"
|
||||||
current_year = str(datetime.datetime.now().year)
|
current_year = str(datetime.datetime.now().year)
|
||||||
year_duration = (
|
year_duration = (
|
||||||
current_year
|
current_year if current_year == creation_year else creation_year + " - " + current_year
|
||||||
if current_year == creation_year
|
|
||||||
else creation_year + " - " + current_year
|
|
||||||
)
|
)
|
||||||
copyright = year_duration + " Dan Halbert"
|
copyright = year_duration + " Dan Halbert"
|
||||||
author = "Dan Halbert"
|
author = "Dan Halbert"
|
||||||
|
|
@ -116,7 +112,6 @@ napoleon_numpy_docstring = False
|
||||||
import sphinx_rtd_theme
|
import sphinx_rtd_theme
|
||||||
|
|
||||||
html_theme = "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,
|
# 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,
|
# 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.**
|
Different ways of starting the server
|
||||||
**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.**
|
|
||||||
|
|
||||||
This is the minimal example of using the library with CircuitPython.
|
There are several ways to start the server on CircuitPython, mostly depending on the device you are using and
|
||||||
This example is serving a simple static text message.
|
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
|
Below you can find examples of different ways to start the server:
|
||||||
:caption: examples/httpserver_simpletest_manual.py
|
|
||||||
:emphasize-lines: 12-17
|
|
||||||
:linenos:
|
|
||||||
|
|
||||||
It is also possible to use Ethernet instead of WiFi.
|
.. toctree::
|
||||||
The only difference in usage is related to configuring the ``socket_source`` differently.
|
|
||||||
|
|
||||||
.. literalinclude:: ../examples/httpserver_ethernet_simpletest.py
|
starting_methods
|
||||||
: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:
|
|
||||||
|
|
||||||
CPython usage
|
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
|
.. literalinclude:: ../examples/httpserver_static_files_serving.py
|
||||||
:caption: 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:
|
:linenos:
|
||||||
|
|
||||||
You can also serve a specific file from the handler.
|
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
|
.. literalinclude:: ../examples/httpserver_handler_serves_file.py
|
||||||
:caption: examples/httpserver_handler_serves_file.py
|
:caption: examples/httpserver_handler_serves_file.py
|
||||||
:emphasize-lines: 13,22
|
:emphasize-lines: 12,21
|
||||||
:linenos:
|
:linenos:
|
||||||
|
|
||||||
.. literalinclude:: ../examples/home.html
|
.. literalinclude:: ../examples/home.html
|
||||||
|
|
@ -100,7 +74,7 @@ a running total of the last 10 samples.
|
||||||
|
|
||||||
.. literalinclude:: ../examples/httpserver_start_and_poll.py
|
.. literalinclude:: ../examples/httpserver_start_and_poll.py
|
||||||
:caption: examples/httpserver_start_and_poll.py
|
:caption: examples/httpserver_start_and_poll.py
|
||||||
:emphasize-lines: 29,38
|
:emphasize-lines: 28,37
|
||||||
:linenos:
|
:linenos:
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -112,7 +86,7 @@ without needing to manually manage the timing of each task.
|
||||||
|
|
||||||
.. literalinclude:: ../examples/httpserver_start_and_poll_asyncio.py
|
.. literalinclude:: ../examples/httpserver_start_and_poll_asyncio.py
|
||||||
:caption: 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:
|
:linenos:
|
||||||
|
|
||||||
Server with MDNS
|
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
|
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.
|
when accessing it via the hostname.
|
||||||
|
|
||||||
In this example, the server is accessible via the IP and ``http://custom-mdns-hostname.local/``.
|
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/``, but **this is not guaranteed to work**.
|
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
|
.. literalinclude:: ../examples/httpserver_mdns.py
|
||||||
:caption: examples/httpserver_mdns.py
|
:caption: examples/httpserver_mdns.py
|
||||||
:emphasize-lines: 12-14
|
:emphasize-lines: 11-13
|
||||||
:linenos:
|
:linenos:
|
||||||
|
|
||||||
Get CPU information
|
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
|
.. literalinclude:: ../examples/httpserver_cpu_information.py
|
||||||
:caption: examples/httpserver_cpu_information.py
|
:caption: examples/httpserver_cpu_information.py
|
||||||
:emphasize-lines: 9,15-18,33
|
:emphasize-lines: 9,14-17,32
|
||||||
:linenos:
|
:linenos:
|
||||||
|
|
||||||
Handling different methods
|
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
|
.. literalinclude:: ../examples/httpserver_methods.py
|
||||||
:caption: examples/httpserver_methods.py
|
:caption: examples/httpserver_methods.py
|
||||||
:emphasize-lines: 8,19,26,30,49
|
:emphasize-lines: 8,18,25,29,46
|
||||||
:linenos:
|
:linenos:
|
||||||
|
|
||||||
Change NeoPixel color
|
Change NeoPixel color
|
||||||
|
|
@ -182,7 +156,7 @@ Tested on ESP32-S2 Feather.
|
||||||
|
|
||||||
.. literalinclude:: ../examples/httpserver_neopixel.py
|
.. literalinclude:: ../examples/httpserver_neopixel.py
|
||||||
:caption: 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:
|
:linenos:
|
||||||
|
|
||||||
Templates
|
Templates
|
||||||
|
|
@ -207,7 +181,7 @@ You can find more information about the template syntax in the
|
||||||
|
|
||||||
.. literalinclude:: ../examples/httpserver_templates.py
|
.. literalinclude:: ../examples/httpserver_templates.py
|
||||||
:caption: examples/httpserver_templates.py
|
:caption: examples/httpserver_templates.py
|
||||||
:emphasize-lines: 12-15,49-55
|
:emphasize-lines: 12-15,51-59
|
||||||
:linenos:
|
:linenos:
|
||||||
|
|
||||||
Form data parsing
|
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.
|
- ``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
|
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"``
|
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``.
|
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\...``.
|
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.
|
- ``text/plain`` - For sending text data with special characters.
|
||||||
|
|
@ -235,7 +209,7 @@ return only the first one.
|
||||||
|
|
||||||
.. literalinclude:: ../examples/httpserver_form_data.py
|
.. literalinclude:: ../examples/httpserver_form_data.py
|
||||||
:caption: examples/httpserver_form_data.py
|
:caption: examples/httpserver_form_data.py
|
||||||
:emphasize-lines: 32,47,50
|
:emphasize-lines: 31,46,49
|
||||||
:linenos:
|
:linenos:
|
||||||
|
|
||||||
Cookies
|
Cookies
|
||||||
|
|
@ -249,7 +223,7 @@ In order to set cookies, pass ``cookies`` dictionary to ``Response`` constructo
|
||||||
|
|
||||||
.. literalinclude:: ../examples/httpserver_cookies.py
|
.. literalinclude:: ../examples/httpserver_cookies.py
|
||||||
:caption: examples/httpserver_cookies.py
|
:caption: examples/httpserver_cookies.py
|
||||||
:emphasize-lines: 70,74-75,82
|
:emphasize-lines: 69,73-74,81
|
||||||
:linenos:
|
:linenos:
|
||||||
|
|
||||||
Chunked response
|
Chunked response
|
||||||
|
|
@ -261,7 +235,7 @@ constructor.
|
||||||
|
|
||||||
.. literalinclude:: ../examples/httpserver_chunked.py
|
.. literalinclude:: ../examples/httpserver_chunked.py
|
||||||
:caption: examples/httpserver_chunked.py
|
:caption: examples/httpserver_chunked.py
|
||||||
:emphasize-lines: 8,21-26,28
|
:emphasize-lines: 8,20-25,27
|
||||||
:linenos:
|
:linenos:
|
||||||
|
|
||||||
URL parameters and wildcards
|
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
|
.. literalinclude:: ../examples/httpserver_url_parameters.py
|
||||||
:caption: 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:
|
:linenos:
|
||||||
|
|
||||||
Authentication
|
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
|
.. literalinclude:: ../examples/httpserver_authentication_server.py
|
||||||
:caption: examples/httpserver_authentication_server.py
|
:caption: examples/httpserver_authentication_server.py
|
||||||
:emphasize-lines: 8,11-16,20
|
:emphasize-lines: 8,10-15,19
|
||||||
:linenos:
|
:linenos:
|
||||||
|
|
||||||
On the other hand, if you want to apply authentication to a set of routes, you need to call ``require_authentication`` function.
|
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
|
.. literalinclude:: ../examples/httpserver_authentication_handlers.py
|
||||||
:caption: 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:
|
:linenos:
|
||||||
|
|
||||||
Redirects
|
Redirects
|
||||||
|
|
@ -335,7 +309,7 @@ Alternatively, you can pass a ``status`` object directly to ``Redirect`` constru
|
||||||
|
|
||||||
.. literalinclude:: ../examples/httpserver_redirects.py
|
.. literalinclude:: ../examples/httpserver_redirects.py
|
||||||
:caption: 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:
|
:linenos:
|
||||||
|
|
||||||
Server-Sent Events
|
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 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.
|
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
|
.. literalinclude:: ../examples/httpserver_sse.py
|
||||||
:caption: examples/httpserver_sse.py
|
:caption: examples/httpserver_sse.py
|
||||||
:emphasize-lines: 10,17,46-53,63
|
:emphasize-lines: 11,17,46-53,63
|
||||||
:linenos:
|
:linenos:
|
||||||
|
|
||||||
Websockets
|
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,
|
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.
|
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.
|
.. warning::
|
||||||
This might change in the future, but for now, it is recommended to use Websocket only with one client at a time.**
|
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
|
.. literalinclude:: ../examples/httpserver_websocket.py
|
||||||
:caption: 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:
|
: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
|
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**.
|
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.
|
**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.
|
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
|
.. literalinclude:: ../examples/httpserver_multiple_servers.py
|
||||||
:caption: 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:
|
:linenos:
|
||||||
|
|
||||||
Debug mode
|
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::
|
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 /" 194 -- "200 OK" 154 -- 96ms
|
||||||
192.168.0.101 -- "GET /example" 134 -- "404 Not Found" 172 -- 123ms
|
192.168.0.101 -- "GET /example" 134 -- "404 Not Found" 172 -- 123ms
|
||||||
192.168.0.102 -- "POST /api" 1241 -- "401 Unauthorized" 95 -- 64ms
|
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
|
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_...``.
|
functions at the bottom of ``adafruit_httpserver/server.py`` that start with ``_debug_...``.
|
||||||
|
|
||||||
NOTE:
|
.. note::
|
||||||
*This is an advanced usage that might change in the future. It is not recommended to modify other parts of the code.*
|
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 socketpool
|
||||||
import wifi
|
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 (
|
from adafruit_httpserver.authentication import (
|
||||||
AuthenticationError,
|
AuthenticationError,
|
||||||
Basic,
|
Basic,
|
||||||
Token,
|
|
||||||
Bearer,
|
Bearer,
|
||||||
|
Token,
|
||||||
check_authentication,
|
check_authentication,
|
||||||
require_authentication,
|
require_authentication,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
pool = socketpool.SocketPool(wifi.radio)
|
pool = socketpool.SocketPool(wifi.radio)
|
||||||
server = Server(pool, debug=True)
|
server = Server(pool, debug=True)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,7 @@
|
||||||
import socketpool
|
import socketpool
|
||||||
import wifi
|
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.
|
# Create a list of available authentication methods.
|
||||||
auths = [
|
auths = [
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,7 @@
|
||||||
import socketpool
|
import socketpool
|
||||||
import wifi
|
import wifi
|
||||||
|
|
||||||
from adafruit_httpserver import Server, Request, ChunkedResponse
|
from adafruit_httpserver import ChunkedResponse, Request, Server
|
||||||
|
|
||||||
|
|
||||||
pool = socketpool.SocketPool(wifi.radio)
|
pool = socketpool.SocketPool(wifi.radio)
|
||||||
server = Server(pool, debug=True)
|
server = Server(pool, debug=True)
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,7 @@
|
||||||
import socketpool
|
import socketpool
|
||||||
import wifi
|
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)
|
pool = socketpool.SocketPool(wifi.radio)
|
||||||
server = Server(pool, debug=True)
|
server = Server(pool, debug=True)
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,7 @@ import microcontroller
|
||||||
import socketpool
|
import socketpool
|
||||||
import wifi
|
import wifi
|
||||||
|
|
||||||
from adafruit_httpserver import Server, Request, JSONResponse
|
from adafruit_httpserver import JSONResponse, Request, Server
|
||||||
|
|
||||||
|
|
||||||
pool = socketpool.SocketPool(wifi.radio)
|
pool = socketpool.SocketPool(wifi.radio)
|
||||||
server = Server(pool, debug=True)
|
server = Server(pool, debug=True)
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,7 @@
|
||||||
|
|
||||||
import socket
|
import socket
|
||||||
|
|
||||||
from adafruit_httpserver import Server, Request, Response
|
from adafruit_httpserver import Request, Response, Server
|
||||||
|
|
||||||
|
|
||||||
pool = socket
|
pool = socket
|
||||||
server = Server(pool, "/static", debug=True)
|
server = Server(pool, "/static", debug=True)
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,7 @@
|
||||||
import socketpool
|
import socketpool
|
||||||
import wifi
|
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)
|
pool = socketpool.SocketPool(wifi.radio)
|
||||||
server = Server(pool, debug=True)
|
server = Server(pool, debug=True)
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,7 @@
|
||||||
import socketpool
|
import socketpool
|
||||||
import wifi
|
import wifi
|
||||||
|
|
||||||
from adafruit_httpserver import Server, Request, FileResponse
|
from adafruit_httpserver import FileResponse, Request, Server
|
||||||
|
|
||||||
|
|
||||||
pool = socketpool.SocketPool(wifi.radio)
|
pool = socketpool.SocketPool(wifi.radio)
|
||||||
server = Server(pool, "/default-static-folder", debug=True)
|
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 socketpool
|
||||||
import wifi
|
import wifi
|
||||||
|
|
||||||
from adafruit_httpserver import Server, Request, FileResponse
|
from adafruit_httpserver import FileResponse, Request, Server
|
||||||
|
|
||||||
|
|
||||||
mdns_server = mdns.Server(wifi.radio)
|
mdns_server = mdns.Server(wifi.radio)
|
||||||
mdns_server.hostname = "custom-mdns-hostname"
|
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)
|
pool = socketpool.SocketPool(wifi.radio)
|
||||||
server = Server(pool, "/static", debug=True)
|
server = Server(pool, "/static", debug=True)
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,7 @@
|
||||||
import socketpool
|
import socketpool
|
||||||
import wifi
|
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)
|
pool = socketpool.SocketPool(wifi.radio)
|
||||||
server = Server(pool, debug=True)
|
server = Server(pool, debug=True)
|
||||||
|
|
@ -27,7 +26,7 @@ def api(request: Request):
|
||||||
return JSONResponse(request, objects)
|
return JSONResponse(request, objects)
|
||||||
|
|
||||||
# Upload or update objects
|
# Upload or update objects
|
||||||
if request.method in [POST, PUT]:
|
if request.method in {POST, PUT}:
|
||||||
uploaded_object = request.json()
|
uploaded_object = request.json()
|
||||||
|
|
||||||
# Find object with same ID
|
# Find object with same ID
|
||||||
|
|
@ -41,9 +40,7 @@ def api(request: Request):
|
||||||
|
|
||||||
# If not found, add it
|
# If not found, add it
|
||||||
objects.append(uploaded_object)
|
objects.append(uploaded_object)
|
||||||
return JSONResponse(
|
return JSONResponse(request, {"message": "Object added", "object": uploaded_object})
|
||||||
request, {"message": "Object added", "object": uploaded_object}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Delete objects
|
# Delete objects
|
||||||
if request.method == DELETE:
|
if request.method == DELETE:
|
||||||
|
|
@ -59,9 +56,7 @@ def api(request: Request):
|
||||||
)
|
)
|
||||||
|
|
||||||
# If not found, return error
|
# If not found, return error
|
||||||
return JSONResponse(
|
return JSONResponse(request, {"message": "Object not found", "object": deleted_object})
|
||||||
request, {"message": "Object not found", "object": deleted_object}
|
|
||||||
)
|
|
||||||
|
|
||||||
# If we get here, something went wrong
|
# If we get here, something went wrong
|
||||||
return JSONResponse(request, {"message": "Something went wrong"})
|
return JSONResponse(request, {"message": "Something went wrong"})
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,7 @@
|
||||||
import socketpool
|
import socketpool
|
||||||
import wifi
|
import wifi
|
||||||
|
|
||||||
from adafruit_httpserver import Server, Request, Response
|
from adafruit_httpserver import Request, Response, Server
|
||||||
|
|
||||||
|
|
||||||
pool = socketpool.SocketPool(wifi.radio)
|
pool = socketpool.SocketPool(wifi.radio)
|
||||||
|
|
||||||
|
|
@ -42,11 +41,11 @@ def home(request: Request):
|
||||||
return Response(request, "Hello from home!")
|
return Response(request, "Hello from home!")
|
||||||
|
|
||||||
|
|
||||||
id_address = str(wifi.radio.ipv4_address)
|
ip_address = str(wifi.radio.ipv4_address)
|
||||||
|
|
||||||
# Start the servers.
|
# Start the servers.
|
||||||
bedroom_server.start(id_address, 5000)
|
bedroom_server.start(ip_address, 5000)
|
||||||
office_server.start(id_address, 8000)
|
office_server.start(ip_address, 8000)
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,7 @@ import neopixel
|
||||||
import socketpool
|
import socketpool
|
||||||
import wifi
|
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)
|
pool = socketpool.SocketPool(wifi.radio)
|
||||||
server = Server(pool, "/static", debug=True)
|
server = Server(pool, "/static", debug=True)
|
||||||
|
|
|
||||||
|
|
@ -6,16 +6,15 @@ import socketpool
|
||||||
import wifi
|
import wifi
|
||||||
|
|
||||||
from adafruit_httpserver import (
|
from adafruit_httpserver import (
|
||||||
Server,
|
MOVED_PERMANENTLY_301,
|
||||||
|
NOT_FOUND_404,
|
||||||
|
POST,
|
||||||
|
Redirect,
|
||||||
Request,
|
Request,
|
||||||
Response,
|
Response,
|
||||||
Redirect,
|
Server,
|
||||||
POST,
|
|
||||||
NOT_FOUND_404,
|
|
||||||
MOVED_PERMANENTLY_301,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
pool = socketpool.SocketPool(wifi.radio)
|
pool = socketpool.SocketPool(wifi.radio)
|
||||||
server = Server(pool, debug=True)
|
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 socketpool
|
||||||
import wifi
|
import wifi
|
||||||
|
|
||||||
from adafruit_httpserver import Server, Request, Response
|
from adafruit_httpserver import Request, Response, Server
|
||||||
|
|
||||||
|
|
||||||
pool = socketpool.SocketPool(wifi.radio)
|
pool = socketpool.SocketPool(wifi.radio)
|
||||||
server = Server(pool, "/static", debug=True)
|
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-FileCopyrightText: 2023 Tim C for Adafruit Industries
|
||||||
|
#
|
||||||
# SPDX-License-Identifier: MIT
|
# SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
import adafruit_connection_manager
|
||||||
import board
|
import board
|
||||||
import digitalio
|
import digitalio
|
||||||
|
|
||||||
from adafruit_wiznet5k.adafruit_wiznet5k import WIZNET5K
|
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
|
# For Adafruit Ethernet FeatherWing
|
||||||
cs = digitalio.DigitalInOut(board.D10)
|
cs = digitalio.DigitalInOut(board.D10)
|
||||||
|
|
||||||
# For Particle Ethernet FeatherWing
|
# For Particle Ethernet FeatherWing
|
||||||
# cs = digitalio.DigitalInOut(board.D5)
|
# cs = digitalio.DigitalInOut(board.D5)
|
||||||
|
|
||||||
spi_bus = board.SPI()
|
spi_bus = board.SPI()
|
||||||
|
|
||||||
# Initialize ethernet interface with DHCP
|
# Initialize ethernet interface with DHCP
|
||||||
eth = WIZNET5K(spi_bus, cs)
|
eth = WIZNET5K(spi_bus, cs)
|
||||||
|
|
||||||
# Set the interface on the socket source
|
pool = adafruit_connection_manager.get_radio_socketpool(eth)
|
||||||
socket.set_interface(eth)
|
|
||||||
|
|
||||||
# Initialize the server
|
server = Server(pool, "/static", debug=True)
|
||||||
server = Server(socket, "/static", debug=True)
|
|
||||||
|
|
||||||
|
|
||||||
@server.route("/")
|
@server.route("/")
|
||||||
|
|
@ -2,21 +2,20 @@
|
||||||
#
|
#
|
||||||
# SPDX-License-Identifier: Unlicense
|
# SPDX-License-Identifier: Unlicense
|
||||||
|
|
||||||
import os
|
|
||||||
|
|
||||||
import socketpool
|
import socketpool
|
||||||
import wifi
|
import wifi
|
||||||
|
|
||||||
from adafruit_httpserver import Server, Request, Response
|
from adafruit_httpserver import Request, Response, Server
|
||||||
|
|
||||||
ssid = os.getenv("WIFI_SSID")
|
WIFI_SSID = "..."
|
||||||
password = os.getenv("WIFI_PASSWORD")
|
WIFI_PASSWORD = "..."
|
||||||
|
|
||||||
print("Connecting to", ssid)
|
print(f"Connecting to {WIFI_SSID}...")
|
||||||
wifi.radio.connect(ssid, password)
|
wifi.radio.connect(WIFI_SSID, WIFI_PASSWORD)
|
||||||
print("Connected to", ssid)
|
print(f"Connected to {WIFI_SSID}")
|
||||||
|
|
||||||
pool = socketpool.SocketPool(wifi.radio)
|
pool = socketpool.SocketPool(wifi.radio)
|
||||||
|
|
||||||
server = Server(pool, "/static", debug=True)
|
server = Server(pool, "/static", debug=True)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -3,12 +3,12 @@
|
||||||
# SPDX-License-Identifier: Unlicense
|
# SPDX-License-Identifier: Unlicense
|
||||||
|
|
||||||
from time import monotonic
|
from time import monotonic
|
||||||
|
|
||||||
import microcontroller
|
import microcontroller
|
||||||
import socketpool
|
import socketpool
|
||||||
import wifi
|
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)
|
pool = socketpool.SocketPool(wifi.radio)
|
||||||
server = Server(pool, debug=True)
|
server = Server(pool, debug=True)
|
||||||
|
|
@ -43,7 +43,7 @@ def client(request: Request):
|
||||||
|
|
||||||
@server.route("/connect-client", GET)
|
@server.route("/connect-client", GET)
|
||||||
def connect_client(request: Request):
|
def connect_client(request: Request):
|
||||||
global sse_response # pylint: disable=global-statement
|
global sse_response
|
||||||
|
|
||||||
if sse_response is not None:
|
if sse_response is not None:
|
||||||
sse_response.close() # Close any existing connection
|
sse_response.close() # Close any existing connection
|
||||||
|
|
|
||||||
|
|
@ -6,13 +6,12 @@ import socketpool
|
||||||
import wifi
|
import wifi
|
||||||
|
|
||||||
from adafruit_httpserver import (
|
from adafruit_httpserver import (
|
||||||
Server,
|
|
||||||
REQUEST_HANDLED_RESPONSE_SENT,
|
REQUEST_HANDLED_RESPONSE_SENT,
|
||||||
Request,
|
|
||||||
FileResponse,
|
FileResponse,
|
||||||
|
Request,
|
||||||
|
Server,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
pool = socketpool.SocketPool(wifi.radio)
|
pool = socketpool.SocketPool(wifi.radio)
|
||||||
server = Server(pool, "/static", debug=True)
|
server = Server(pool, "/static", debug=True)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,18 +2,19 @@
|
||||||
#
|
#
|
||||||
# SPDX-License-Identifier: Unlicense
|
# 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 socketpool
|
||||||
import wifi
|
import wifi
|
||||||
|
|
||||||
from adafruit_httpserver import (
|
from adafruit_httpserver import (
|
||||||
Server,
|
|
||||||
REQUEST_HANDLED_RESPONSE_SENT,
|
REQUEST_HANDLED_RESPONSE_SENT,
|
||||||
Request,
|
|
||||||
FileResponse,
|
FileResponse,
|
||||||
|
Request,
|
||||||
|
Server,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
pool = socketpool.SocketPool(wifi.radio)
|
pool = socketpool.SocketPool(wifi.radio)
|
||||||
server = Server(pool, "/static", debug=True)
|
server = Server(pool, "/static", debug=True)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,7 @@
|
||||||
import socketpool
|
import socketpool
|
||||||
import wifi
|
import wifi
|
||||||
|
|
||||||
from adafruit_httpserver import Server, MIMETypes
|
from adafruit_httpserver import MIMETypes, Server
|
||||||
|
|
||||||
|
|
||||||
MIMETypes.configure(
|
MIMETypes.configure(
|
||||||
default_to="text/plain",
|
default_to="text/plain",
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import re
|
||||||
import socketpool
|
import socketpool
|
||||||
import wifi
|
import wifi
|
||||||
|
|
||||||
from adafruit_httpserver import Server, Request, Response, FileResponse
|
from adafruit_httpserver import FileResponse, Request, Response, Server
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from adafruit_templateengine import render_template
|
from adafruit_templateengine import render_template
|
||||||
|
|
|
||||||
|
|
@ -5,22 +5,21 @@
|
||||||
import socketpool
|
import socketpool
|
||||||
import wifi
|
import wifi
|
||||||
|
|
||||||
from adafruit_httpserver import Server, Request, Response
|
from adafruit_httpserver import Request, Response, Server
|
||||||
|
|
||||||
|
|
||||||
pool = socketpool.SocketPool(wifi.radio)
|
pool = socketpool.SocketPool(wifi.radio)
|
||||||
server = Server(pool, debug=True)
|
server = Server(pool, debug=True)
|
||||||
|
|
||||||
|
|
||||||
class Device:
|
class Device:
|
||||||
def turn_on(self): # pylint: disable=no-self-use
|
def turn_on(self):
|
||||||
print("Turning on device.")
|
print("Turning on device.")
|
||||||
|
|
||||||
def turn_off(self): # pylint: disable=no-self-use
|
def turn_off(self):
|
||||||
print("Turning off device.")
|
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.
|
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/<device_id>/action/<action>")
|
||||||
@server.route("/device/emergency-power-off/<device_id>")
|
@server.route("/device/emergency-power-off/<device_id>")
|
||||||
def perform_action(
|
def perform_action(request: Request, device_id: str, action: str = "emergency_power_off"):
|
||||||
request: Request, device_id: str, action: str = "emergency_power_off"
|
|
||||||
):
|
|
||||||
"""
|
"""
|
||||||
Performs an "action" on a specified device.
|
Performs an "action" on a specified device.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
device = get_device(device_id)
|
device = get_device(device_id)
|
||||||
|
|
||||||
if action in ["turn_on"]:
|
if action in {"turn_on"}:
|
||||||
device.turn_on()
|
device.turn_on()
|
||||||
elif action in ["turn_off", "emergency_power_off"]:
|
elif action in {"turn_off", "emergency_power_off"}:
|
||||||
device.turn_off()
|
device.turn_off()
|
||||||
else:
|
else:
|
||||||
return Response(request, f"Unknown action ({action})")
|
return Response(request, f"Unknown action ({action})")
|
||||||
|
|
||||||
return Response(
|
return Response(request, f"Action ({action}) performed on device with ID: {device_id}")
|
||||||
request, f"Action ({action}) performed on device with ID: {device_id}"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@server.route("/device/<device_id>/status/<date>")
|
@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
|
# 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 board
|
||||||
import microcontroller
|
import microcontroller
|
||||||
import neopixel
|
import neopixel
|
||||||
import socketpool
|
import socketpool
|
||||||
import wifi
|
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)
|
pool = socketpool.SocketPool(wifi.radio)
|
||||||
server = Server(pool, debug=True)
|
server = Server(pool, debug=True)
|
||||||
|
|
@ -62,7 +63,7 @@ def client(request: Request):
|
||||||
|
|
||||||
@server.route("/connect-websocket", GET)
|
@server.route("/connect-websocket", GET)
|
||||||
def connect_client(request: Request):
|
def connect_client(request: Request):
|
||||||
global websocket # pylint: disable=global-statement
|
global websocket
|
||||||
|
|
||||||
if websocket is not None:
|
if websocket is not None:
|
||||||
websocket.close() # Close any existing connection
|
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