diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..d54c593 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,5 @@ +# SPDX-FileCopyrightText: 2024 Justin Myers for Adafruit Industries +# +# SPDX-License-Identifier: Unlicense + +* text eol=lf diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7b20fd7..f27b786 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,47 +1,22 @@ # SPDX-FileCopyrightText: 2020 Diego Elio Pettenò +# SPDX-FileCopyrightText: 2024 Justin Myers # # SPDX-License-Identifier: Unlicense repos: - - repo: https://github.com/python/black - rev: 24.2.0 - hooks: - - id: black - - repo: https://github.com/PyCQA/isort - rev: 5.13.2 - hooks: - - id: isort - args: ["--profile", "black", "--filter-files"] - - repo: https://github.com/fsfe/reuse-tool - rev: v1.1.2 - hooks: - - id: reuse - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.4.0 + rev: v4.5.0 hooks: - id: check-yaml - id: end-of-file-fixer - id: trailing-whitespace - - repo: https://github.com/pycqa/pylint - rev: v2.17.4 + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.3.4 + hooks: + - id: ruff-format + - id: ruff + args: ["--fix"] + - repo: https://github.com/fsfe/reuse-tool + rev: v3.0.1 hooks: - - id: pylint - name: pylint (library code) - types: [python] - args: - - --disable=consider-using-f-string - exclude: "^(docs/|examples/|tests/|setup.py$)" - - id: pylint - name: pylint (example code) - description: Run pylint rules on "examples/*.py" files - types: [python] - files: "^examples/" - args: - - --disable=consider-using-f-string,duplicate-code,missing-docstring,invalid-name - - id: pylint - name: pylint (test code) - description: Run pylint rules on "tests/*.py" files - types: [python] - files: "^tests/" - args: - - --disable=consider-using-f-string,duplicate-code,missing-docstring,invalid-name,protected-access,redefined-outer-name + - id: reuse diff --git a/.pylintrc b/.pylintrc deleted file mode 100644 index f945e92..0000000 --- a/.pylintrc +++ /dev/null @@ -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*(# )??$ - -# 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 diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 33c2a61..255dafd 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -8,8 +8,11 @@ # Required version: 2 +sphinx: + configuration: docs/conf.py + build: - os: ubuntu-20.04 + os: ubuntu-lts-latest tools: python: "3" diff --git a/README.rst b/README.rst index a04f450..4e73438 100755 --- a/README.rst +++ b/README.rst @@ -13,9 +13,9 @@ Introduction :target: https://github.com/adafruit/Adafruit_CircuitPython_Requests/actions/ :alt: Build Status -.. image:: https://img.shields.io/badge/code%20style-black-000000.svg - :target: https://github.com/psf/black - :alt: Code Style: Black +.. image:: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json + :target: https://github.com/astral-sh/ruff + :alt: Code Style: Ruff A requests-like library for HTTP commands. diff --git a/adafruit_requests.py b/adafruit_requests.py index 771bfcc..d15db69 100644 --- a/adafruit_requests.py +++ b/adafruit_requests.py @@ -41,13 +41,16 @@ import errno import json as json_module +import os import sys from adafruit_connection_manager import get_connection_manager +SEEK_END = 2 + if not sys.implementation.name == "circuitpython": from types import TracebackType - from typing import Any, Dict, Optional, Type + from typing import IO, Any, Dict, Optional, Type from circuitpython_typing.socket import ( SocketpoolModuleType, @@ -72,7 +75,7 @@ def read(self, size: int = -1) -> bytes: def readinto(self, buf: bytearray) -> int: """Read as much as available into buf or until it is full. Returns the number of bytes read into buf.""" - return self._response._readinto(buf) # pylint: disable=protected-access + return self._response._readinto(buf) class OutOfRetries(Exception): @@ -82,15 +85,29 @@ class OutOfRetries(Exception): class Response: """The response from a request, contains all the headers/content""" - # pylint: disable=too-many-instance-attributes - encoding = None + socket: SocketType + """The underlying socket object (CircuitPython extension, not in standard requests) + + Under the following circumstances, calling code may directly access the underlying + socket object: + + * The request was made with ``stream=True`` + * The request headers included ``{'connection': 'close'}`` + * No methods or properties on the Response object that access the response content + may be used + + Methods and properties that access response headers may be accessed. + + It is still necessary to ``close`` the response object for correct management of + sockets, including doing so implicitly via ``with requests.get(...) as response``.""" - def __init__(self, sock: SocketType, session: "Session") -> None: + def __init__(self, sock: SocketType, session: "Session", method: str) -> None: self.socket = sock self.encoding = "utf-8" self._cached = None self._headers = {} + self._method = method # _start_index and _receive_buffer are used when parsing headers. # _receive_buffer will grow by 32 bytes everytime it is too small. @@ -175,9 +192,7 @@ def _read_from_buffer( def _readinto(self, buf: bytearray) -> int: if not self.socket: - raise RuntimeError( - "Newer Response closed this one. Use Responses immediately." - ) + raise RuntimeError("Newer Response closed this one. Use Responses immediately.") if not self._remaining: # Consume the chunk header if need be. @@ -227,26 +242,15 @@ def _throw_away(self, nbytes: int) -> None: to_read -= self._recv_into(buf, to_read) def close(self) -> None: - """Drain the remaining ESP socket buffers. We assume we already got what we wanted.""" + """Close out the socket. If we have a session free it instead.""" if not self.socket: return - # Make sure we've read all of our response. - if self._cached is None: - if self._remaining and self._remaining > 0: - self._throw_away(self._remaining) - elif self._chunked: - while True: - chunk_header = bytes(self._readto(b"\r\n")).split(b";", 1)[0] - chunk_size = int(bytes(chunk_header), 16) - if chunk_size == 0: - break - self._throw_away(chunk_size + 2) - self._parse_headers() + if self._session: - # pylint: disable=protected-access self._session._connection_manager.free_socket(self.socket) else: self.socket.close() + self.socket = None def _parse_headers(self) -> None: @@ -258,7 +262,8 @@ def _parse_headers(self) -> None: header = self._readto(b"\r\n") if not header: break - title, content = bytes(header).split(b": ", 1) + title, content = bytes(header).split(b":", 1) + content = content.strip() if title and content: # enforce that all headers are lowercase title = str(title, "utf-8").lower() @@ -272,12 +277,18 @@ def _parse_headers(self) -> None: else: self._headers[title] = content - def _validate_not_gzip(self) -> None: - """gzip encoding is not supported. Raise an exception if found.""" + # does the body have a fixed length? (of zero) if ( - "content-encoding" in self.headers - and self.headers["content-encoding"] == "gzip" + self.status_code == 204 + or self.status_code == 304 + or 100 <= self.status_code < 200 # 1xx codes + or self._method == "HEAD" ): + self._remaining = 0 + + def _validate_not_gzip(self) -> None: + """gzip encoding is not supported. Raise an exception if found.""" + if "content-encoding" in self.headers and self.headers["content-encoding"] == "gzip": raise ValueError( "Content-encoding is gzip, data cannot be accessed as json or text. " "Use content property to access raw bytes." @@ -331,7 +342,7 @@ def json(self) -> Any: obj = json_module.load(self._raw) if not self._cached: self._cached = obj - self.close() + return obj def iter_content(self, chunk_size: int = 1, decode_unicode: bool = False) -> bytes: @@ -367,23 +378,83 @@ def __init__( self._session_id = session_id self._last_response = None + def _build_boundary_data(self, files: dict): # pylint: disable=too-many-locals + boundary_string = self._build_boundary_string() + content_length = 0 + boundary_objects = [] + + for field_name, field_values in files.items(): + file_name = field_values[0] + file_handle = field_values[1] + + boundary_objects.append( + f'--{boundary_string}\r\nContent-Disposition: form-data; name="{field_name}"' + ) + if file_name is not None: + boundary_objects.append(f'; filename="{file_name}"') + boundary_objects.append("\r\n") + if len(field_values) >= 3: + file_content_type = field_values[2] + boundary_objects.append(f"Content-Type: {file_content_type}\r\n") + if len(field_values) >= 4: + file_headers = field_values[3] + for file_header_key, file_header_value in file_headers.items(): + boundary_objects.append(f"{file_header_key}: {file_header_value}\r\n") + boundary_objects.append("\r\n") + + if hasattr(file_handle, "read"): + content_length += self._get_file_length(file_handle) + + boundary_objects.append(file_handle) + boundary_objects.append("\r\n") + + boundary_objects.append(f"--{boundary_string}--\r\n") + + for boundary_object in boundary_objects: + if isinstance(boundary_object, str): + content_length += len(boundary_object) + + return boundary_string, content_length, boundary_objects + + @staticmethod + def _build_boundary_string(): + return os.urandom(16).hex() + @staticmethod def _check_headers(headers: Dict[str, str]): if not isinstance(headers, dict): - raise AttributeError("headers must be in dict format") + raise TypeError("Headers must be in dict format") for key, value in headers.items(): if isinstance(value, (str, bytes)) or value is None: continue - raise AttributeError( + raise TypeError( f"Header part ({value}) from {key} must be of type str or bytes, not {type(value)}" ) + @staticmethod + def _get_file_length(file_handle: IO): + is_binary = False + try: + file_handle.seek(0) + # read at least 4 bytes incase we are reading a b64 stream + content = file_handle.read(4) + is_binary = isinstance(content, bytes) + except UnicodeError: + is_binary = False + + if not is_binary: + raise ValueError("Files must be opened in binary mode") + + file_handle.seek(0, SEEK_END) + content_length = file_handle.tell() + file_handle.seek(0) + return content_length + @staticmethod def _send(socket: SocketType, data: bytes): total_sent = 0 while total_sent < len(data): - # ESP32SPI sockets raise a RuntimeError when unable to send. try: sent = socket.send(data[total_sent:]) except OSError as exc: @@ -393,6 +464,7 @@ def _send(socket: SocketType, data: bytes): # Some worse error. raise except RuntimeError as exc: + # ESP32SPI sockets raise a RuntimeError when unable to send. raise OSError(errno.EIO) from exc if sent is None: sent = len(data) @@ -404,6 +476,22 @@ def _send(socket: SocketType, data: bytes): def _send_as_bytes(self, socket: SocketType, data: str): return self._send(socket, bytes(data, "utf-8")) + def _send_boundary_objects(self, socket: SocketType, boundary_objects: Any): + for boundary_object in boundary_objects: + if isinstance(boundary_object, str): + self._send_as_bytes(socket, boundary_object) + else: + self._send_file(socket, boundary_object) + + def _send_file(self, socket: SocketType, file_handle: IO): + chunk_size = 36 + b = bytearray(chunk_size) + while True: + size = file_handle.readinto(b) + if size == 0: + break + self._send(socket, b[:size]) + def _send_header(self, socket, header, value): if value is None: return @@ -415,8 +503,8 @@ def _send_header(self, socket, header, value): self._send_as_bytes(socket, value) self._send(socket, b"\r\n") - # pylint: disable=too-many-arguments - def _send_request( + # noqa: PLR0912 Too many branches + def _send_request( # noqa: PLR0913,PLR0912 Too many arguments in function definition,Too many branches self, socket: SocketType, host: str, @@ -425,6 +513,7 @@ def _send_request( headers: Dict[str, str], data: Any, json: Any, + files: Optional[Dict[str, tuple]], ): # Check headers self._check_headers(headers) @@ -435,15 +524,17 @@ def _send_request( # If json is sent, set content type header and convert to string if json is not None: assert data is None + assert files is None content_type_header = "application/json" data = json_module.dumps(json) # If data is sent and it's a dict, set content type header and convert to string if data and isinstance(data, dict): + assert files is None content_type_header = "application/x-www-form-urlencoded" _post_data = "" for k in data: - _post_data = "{}&{}={}".format(_post_data, k, data[k]) + _post_data = f"{_post_data}&{k}={data[k]}" # remove first "&" from concatenation data = _post_data[1:] @@ -451,6 +542,21 @@ def _send_request( if data and isinstance(data, str): data = bytes(data, "utf-8") + # If files are send, build data to send and calculate length + content_length = 0 + data_is_file = False + boundary_objects = None + if files and isinstance(files, dict): + boundary_string, content_length, boundary_objects = self._build_boundary_data(files) + content_type_header = f"multipart/form-data; boundary={boundary_string}" + elif data and hasattr(data, "read"): + data_is_file = True + content_length = self._get_file_length(data) + else: + if data is None: + data = b"" + content_length = len(data) + self._send_as_bytes(socket, method) self._send(socket, b" /") self._send_as_bytes(socket, path) @@ -466,19 +572,22 @@ def _send_request( self._send_header(socket, "User-Agent", "Adafruit CircuitPython") if content_type_header and not "content-type" in supplied_headers: self._send_header(socket, "Content-Type", content_type_header) - if data and not "content-length" in supplied_headers: - self._send_header(socket, "Content-Length", str(len(data))) + if (data or files) and not "content-length" in supplied_headers: + self._send_header(socket, "Content-Length", str(content_length)) # Iterate over keys to avoid tuple alloc for header in headers: self._send_header(socket, header, headers[header]) self._send(socket, b"\r\n") # Send data - if data: + if data_is_file: + self._send_file(socket, data) + elif data: self._send(socket, bytes(data)) + elif boundary_objects: + self._send_boundary_objects(socket, boundary_objects) - # pylint: disable=too-many-branches, too-many-statements, unused-argument, too-many-arguments, too-many-locals - def request( + def request( # noqa: PLR0912,PLR0913,PLR0915 Too many branches,Too many arguments in function definition,Too many statements self, method: str, url: str, @@ -488,6 +597,7 @@ def request( stream: bool = False, timeout: float = 60, allow_redirects: bool = True, + files: Optional[Dict[str, tuple]] = None, ) -> Response: """Perform an HTTP request to the given url which we will parse to determine whether to use SSL ('https://') or not. We can also send some provided 'data' @@ -522,6 +632,10 @@ def request( # We may fail to send the request if the socket we got is closed already. So, try a second # time in that case. + # Note that the loop below actually tries a second time in other failure cases too, + # namely timeout and no data from socket. This was not covered in the stated intent of the + # commit that introduced the loop, but removing the retry from those cases could prove + # problematic to callers that now depend on that resiliency. retry_count = 0 last_exc = None while retry_count < 2: @@ -536,31 +650,37 @@ def request( ) ok = True try: - self._send_request(socket, host, method, path, headers, data, json) + self._send_request(socket, host, method, path, headers, data, json, files) except OSError as exc: last_exc = exc ok = False if ok: # Read the H of "HTTP/1.1" to make sure the socket is alive. send can appear to work # even when the socket is closed. - if hasattr(socket, "recv"): - result = socket.recv(1) - else: - result = bytearray(1) - try: + # Both recv/recv_into can raise OSError; when that happens, we need to call + # _connection_manager.close_socket(socket) or future calls to + # _connection_manager.get_socket() for the same parameter set will fail + try: + if hasattr(socket, "recv"): + result = socket.recv(1) + else: + result = bytearray(1) socket.recv_into(result) - except OSError: - pass - if result == b"H": - # Things seem to be ok so break with socket set. - break + if result == b"H": + # Things seem to be ok so break with socket set. + break + else: + raise RuntimeError("no data from socket") + except (OSError, RuntimeError) as exc: + last_exc = exc + pass self._connection_manager.close_socket(socket) socket = None if not socket: raise OutOfRetries("Repeated socket failures") from last_exc - resp = Response(socket, self) # our response + resp = Response(socket, self, method) # our response if allow_redirects: if "location" in resp.headers and 300 <= resp.status_code <= 399: # a naive handler for redirects @@ -588,6 +708,10 @@ def request( self._last_response = resp return resp + def options(self, url: str, **kw) -> Response: + """Send HTTP OPTIONS request""" + return self.request("OPTIONS", url, **kw) + def head(self, url: str, **kw) -> Response: """Send HTTP HEAD request""" return self.request("HEAD", url, **kw) diff --git a/docs/conf.py b/docs/conf.py index 4e19482..689c1af 100755 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - # SPDX-FileCopyrightText: 2021 ladyada for Adafruit Industries # # SPDX-License-Identifier: MIT @@ -48,9 +46,7 @@ creation_year = "2019" current_year = str(datetime.datetime.now().year) year_duration = ( - current_year - if current_year == creation_year - else creation_year + " - " + current_year + current_year if current_year == creation_year else creation_year + " - " + current_year ) copyright = year_duration + " ladyada" author = "ladyada" @@ -104,7 +100,6 @@ import sphinx_rtd_theme html_theme = "sphinx_rtd_theme" -html_theme_path = [sphinx_rtd_theme.get_html_theme_path(), "."] # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, diff --git a/examples/cpython/requests_cpython_advanced.py b/examples/cpython/requests_cpython_advanced.py index 89d715b..b19b37b 100644 --- a/examples/cpython/requests_cpython_advanced.py +++ b/examples/cpython/requests_cpython_advanced.py @@ -15,17 +15,13 @@ headers = {"user-agent": "blinka/1.0.0"} print("Fetching JSON data from %s..." % JSON_GET_URL) -response = requests.get(JSON_GET_URL, headers=headers) -print("-" * 60) - -json_data = response.json() -headers = json_data["headers"] -print("Response's Custom User-Agent Header: {0}".format(headers["User-Agent"])) -print("-" * 60) - -# Read Response's HTTP status code -print("Response HTTP Status Code: ", response.status_code) -print("-" * 60) - -# Close, delete and collect the response data -response.close() +with requests.get(JSON_GET_URL, headers=headers) as response: + print("-" * 60) + json_data = response.json() + headers = json_data["headers"] + print("Response's Custom User-Agent Header: {0}".format(headers["User-Agent"])) + print("-" * 60) + + # Read Response's HTTP status code + print("Response HTTP Status Code: ", response.status_code) + print("-" * 60) diff --git a/examples/cpython/requests_cpython_simpletest.py b/examples/cpython/requests_cpython_simpletest.py index 70bdfde..953e09c 100644 --- a/examples/cpython/requests_cpython_simpletest.py +++ b/examples/cpython/requests_cpython_simpletest.py @@ -14,39 +14,31 @@ JSON_POST_URL = "https://httpbin.org/post" print("Fetching text from %s" % TEXT_URL) -response = requests.get(TEXT_URL) -print("-" * 40) - -print("Text Response: ", response.text) -print("-" * 40) -response.close() +with requests.get(TEXT_URL) as response: + print("-" * 40) + print("Text Response: ", response.text) + print("-" * 40) print("Fetching JSON data from %s" % JSON_GET_URL) -response = requests.get(JSON_GET_URL) -print("-" * 40) - -print("JSON Response: ", response.json()) -print("-" * 40) -response.close() +with requests.get(JSON_GET_URL) as response: + print("-" * 40) + print("JSON Response: ", response.json()) + print("-" * 40) data = "31F" -print("POSTing data to {0}: {1}".format(JSON_POST_URL, data)) -response = requests.post(JSON_POST_URL, data=data) -print("-" * 40) - -json_resp = response.json() -# Parse out the 'data' key from json_resp dict. -print("Data received from server:", json_resp["data"]) -print("-" * 40) -response.close() +print(f"POSTing data to {JSON_POST_URL}: {data}") +with requests.post(JSON_POST_URL, data=data) as response: + print("-" * 40) + json_resp = response.json() + # Parse out the 'data' key from json_resp dict. + print("Data received from server:", json_resp["data"]) + print("-" * 40) json_data = {"Date": "July 25, 2019"} -print("POSTing data to {0}: {1}".format(JSON_POST_URL, json_data)) -response = requests.post(JSON_POST_URL, json=json_data) -print("-" * 40) - -json_resp = response.json() -# Parse out the 'json' key from json_resp dict. -print("JSON Data received from server:", json_resp["json"]) -print("-" * 40) -response.close() +print(f"POSTing data to {JSON_POST_URL}: {json_data}") +with requests.post(JSON_POST_URL, json=json_data) as response: + print("-" * 40) + json_resp = response.json() + # Parse out the 'json' key from json_resp dict. + print("JSON Data received from server:", json_resp["json"]) + print("-" * 40) diff --git a/examples/esp32spi/requests_esp32spi_advanced.py b/examples/esp32spi/requests_esp32spi_advanced.py index 3b0bb41..170589f 100644 --- a/examples/esp32spi/requests_esp32spi_advanced.py +++ b/examples/esp32spi/requests_esp32spi_advanced.py @@ -4,7 +4,6 @@ import os import adafruit_connection_manager -import adafruit_esp32spi.adafruit_esp32spi_socket as pool import board import busio from adafruit_esp32spi import adafruit_esp32spi @@ -41,10 +40,11 @@ except RuntimeError as e: print("could not connect to AP, retrying: ", e) continue -print("Connected to", str(radio.ssid, "utf-8"), "\tRSSI:", radio.rssi) +print("Connected to", str(radio.ap_info.ssid, "utf-8"), "\tRSSI:", radio.ap_info.rssi) # Initialize a requests session -ssl_context = adafruit_connection_manager.create_fake_ssl_context(pool, radio) +pool = adafruit_connection_manager.get_radio_socketpool(radio) +ssl_context = adafruit_connection_manager.get_radio_ssl_context(radio) requests = adafruit_requests.Session(pool, ssl_context) JSON_GET_URL = "https://httpbin.org/get" @@ -53,17 +53,14 @@ headers = {"user-agent": "blinka/1.0.0"} print("Fetching JSON data from %s..." % JSON_GET_URL) -response = requests.get(JSON_GET_URL, headers=headers) -print("-" * 60) +with requests.get(JSON_GET_URL, headers=headers) as response: + print("-" * 60) -json_data = response.json() -headers = json_data["headers"] -print("Response's Custom User-Agent Header: {0}".format(headers["User-Agent"])) -print("-" * 60) + json_data = response.json() + headers = json_data["headers"] + print("Response's Custom User-Agent Header: {0}".format(headers["User-Agent"])) + print("-" * 60) -# Read Response's HTTP status code -print("Response HTTP Status Code: ", response.status_code) -print("-" * 60) - -# Close, delete and collect the response data -response.close() + # Read Response's HTTP status code + print("Response HTTP Status Code: ", response.status_code) + print("-" * 60) diff --git a/examples/esp32spi/requests_esp32spi_simpletest.py b/examples/esp32spi/requests_esp32spi_simpletest.py index 01d8204..3e856c0 100644 --- a/examples/esp32spi/requests_esp32spi_simpletest.py +++ b/examples/esp32spi/requests_esp32spi_simpletest.py @@ -4,7 +4,6 @@ import os import adafruit_connection_manager -import adafruit_esp32spi.adafruit_esp32spi_socket as pool import board import busio from adafruit_esp32spi import adafruit_esp32spi @@ -41,10 +40,11 @@ except RuntimeError as e: print("could not connect to AP, retrying: ", e) continue -print("Connected to", str(radio.ssid, "utf-8"), "\tRSSI:", radio.rssi) +print("Connected to", str(radio.ap_info.ssid, "utf-8"), "\tRSSI:", radio.ap_info.rssi) # Initialize a requests session -ssl_context = adafruit_connection_manager.create_fake_ssl_context(pool, radio) +pool = adafruit_connection_manager.get_radio_socketpool(radio) +ssl_context = adafruit_connection_manager.get_radio_ssl_context(radio) requests = adafruit_requests.Session(pool, ssl_context) TEXT_URL = "http://wifitest.adafruit.com/testwifi/index.html" @@ -52,39 +52,31 @@ JSON_POST_URL = "https://httpbin.org/post" print("Fetching text from %s" % TEXT_URL) -response = requests.get(TEXT_URL) -print("-" * 40) - -print("Text Response: ", response.text) -print("-" * 40) -response.close() +with requests.get(TEXT_URL) as response: + print("-" * 40) + print("Text Response: ", response.text) + print("-" * 40) print("Fetching JSON data from %s" % JSON_GET_URL) -response = requests.get(JSON_GET_URL) -print("-" * 40) - -print("JSON Response: ", response.json()) -print("-" * 40) -response.close() +with requests.get(JSON_GET_URL) as response: + print("-" * 40) + print("JSON Response: ", response.json()) + print("-" * 40) data = "31F" -print("POSTing data to {0}: {1}".format(JSON_POST_URL, data)) -response = requests.post(JSON_POST_URL, data=data) -print("-" * 40) - -json_resp = response.json() -# Parse out the 'data' key from json_resp dict. -print("Data received from server:", json_resp["data"]) -print("-" * 40) -response.close() +print(f"POSTing data to {JSON_POST_URL}: {data}") +with requests.post(JSON_POST_URL, data=data) as response: + print("-" * 40) + json_resp = response.json() + # Parse out the 'data' key from json_resp dict. + print("Data received from server:", json_resp["data"]) + print("-" * 40) json_data = {"Date": "July 25, 2019"} -print("POSTing data to {0}: {1}".format(JSON_POST_URL, json_data)) -response = requests.post(JSON_POST_URL, json=json_data) -print("-" * 40) - -json_resp = response.json() -# Parse out the 'json' key from json_resp dict. -print("JSON Data received from server:", json_resp["json"]) -print("-" * 40) -response.close() +print(f"POSTing data to {JSON_POST_URL}: {json_data}") +with requests.post(JSON_POST_URL, json=json_data) as response: + print("-" * 40) + json_resp = response.json() + # Parse out the 'json' key from json_resp dict. + print("JSON Data received from server:", json_resp["json"]) + print("-" * 40) diff --git a/examples/fona/requests_fona_advanced.py b/examples/fona/requests_fona_advanced.py index 33bc6f0..4a50b18 100644 --- a/examples/fona/requests_fona_advanced.py +++ b/examples/fona/requests_fona_advanced.py @@ -10,8 +10,8 @@ import board import busio import digitalio -from adafruit_fona.adafruit_fona import FONA # pylint: disable=unused-import -from adafruit_fona.fona_3g import FONA3G # pylint: disable=unused-import +from adafruit_fona.adafruit_fona import FONA +from adafruit_fona.fona_3g import FONA3G import adafruit_requests @@ -54,17 +54,14 @@ headers = {"user-agent": "blinka/1.0.0"} print("Fetching JSON data from %s..." % JSON_GET_URL) -response = requests.get(JSON_GET_URL, headers=headers) -print("-" * 60) +with requests.get(JSON_GET_URL, headers=headers) as response: + print("-" * 60) -json_data = response.json() -headers = json_data["headers"] -print("Response's Custom User-Agent Header: {0}".format(headers["User-Agent"])) -print("-" * 60) + json_data = response.json() + headers = json_data["headers"] + print("Response's Custom User-Agent Header: {0}".format(headers["User-Agent"])) + print("-" * 60) -# Read Response's HTTP status code -print("Response HTTP Status Code: ", response.status_code) -print("-" * 60) - -# Close, delete and collect the response data -response.close() + # Read Response's HTTP status code + print("Response HTTP Status Code: ", response.status_code) + print("-" * 60) diff --git a/examples/fona/requests_fona_simpletest.py b/examples/fona/requests_fona_simpletest.py index 8841d3d..a87bcba 100644 --- a/examples/fona/requests_fona_simpletest.py +++ b/examples/fona/requests_fona_simpletest.py @@ -10,8 +10,8 @@ import board import busio import digitalio -from adafruit_fona.adafruit_fona import FONA # pylint: disable=unused-import -from adafruit_fona.fona_3g import FONA3G # pylint: disable=unused-import +from adafruit_fona.adafruit_fona import FONA +from adafruit_fona.fona_3g import FONA3G import adafruit_requests @@ -53,39 +53,31 @@ JSON_POST_URL = "http://httpbin.org/post" print("Fetching text from %s" % TEXT_URL) -response = requests.get(TEXT_URL) -print("-" * 40) - -print("Text Response: ", response.text) -print("-" * 40) -response.close() +with requests.get(TEXT_URL) as response: + print("-" * 40) + print("Text Response: ", response.text) + print("-" * 40) print("Fetching JSON data from %s" % JSON_GET_URL) -response = requests.get(JSON_GET_URL) -print("-" * 40) - -print("JSON Response: ", response.json()) -print("-" * 40) -response.close() +with requests.get(JSON_GET_URL) as response: + print("-" * 40) + print("JSON Response: ", response.json()) + print("-" * 40) data = "31F" -print("POSTing data to {0}: {1}".format(JSON_POST_URL, data)) -response = requests.post(JSON_POST_URL, data=data) -print("-" * 40) - -json_resp = response.json() -# Parse out the 'data' key from json_resp dict. -print("Data received from server:", json_resp["data"]) -print("-" * 40) -response.close() +print(f"POSTing data to {JSON_POST_URL}: {data}") +with requests.post(JSON_POST_URL, data=data) as response: + print("-" * 40) + json_resp = response.json() + # Parse out the 'data' key from json_resp dict. + print("Data received from server:", json_resp["data"]) + print("-" * 40) json_data = {"Date": "July 25, 2019"} -print("POSTing data to {0}: {1}".format(JSON_POST_URL, json_data)) -response = requests.post(JSON_POST_URL, json=json_data) -print("-" * 40) - -json_resp = response.json() -# Parse out the 'json' key from json_resp dict. -print("JSON Data received from server:", json_resp["json"]) -print("-" * 40) -response.close() +print(f"POSTing data to {JSON_POST_URL}: {json_data}") +with requests.post(JSON_POST_URL, json=json_data) as response: + print("-" * 40) + json_resp = response.json() + # Parse out the 'json' key from json_resp dict. + print("JSON Data received from server:", json_resp["json"]) + print("-" * 40) diff --git a/examples/wifi/expanded/requests_wifi_adafruit_discord_active_online.py b/examples/wifi/expanded/requests_wifi_adafruit_discord_active_online.py index d43f73f..8f6917c 100644 --- a/examples/wifi/expanded/requests_wifi_adafruit_discord_active_online.py +++ b/examples/wifi/expanded/requests_wifi_adafruit_discord_active_online.py @@ -1,16 +1,12 @@ -# SPDX-FileCopyrightText: 2023 DJDevon3 +# SPDX-FileCopyrightText: 2024 DJDevon3 # SPDX-License-Identifier: MIT -""" -Coded for Circuit Python 8.2.3 -requests_adafruit_discord_active_online -""" -import gc -import json +# Coded for Circuit Python 8.2.x +"""Discord Active Online Shields.IO Example""" + import os -import ssl import time -import socketpool +import adafruit_connection_manager import wifi import adafruit_requests @@ -19,95 +15,75 @@ # JSON web scrape from SHIELDS.IO # Adafruit uses Shields.IO to see online users -# Initialize WiFi Pool (There can be only 1 pool & top of script) -pool = socketpool.SocketPool(wifi.radio) - -# Time in seconds between updates (polling) -# 600 = 10 mins, 900 = 15 mins, 1800 = 30 mins, 3600 = 1 hour -sleep_time = 900 - # Get WiFi details, ensure these are setup in settings.toml ssid = os.getenv("CIRCUITPY_WIFI_SSID") password = os.getenv("CIRCUITPY_WIFI_PASSWORD") +# API Polling Rate +# 900 = 15 mins, 1800 = 30 mins, 3600 = 1 hour +SLEEP_TIME = 900 + +# Initalize Wifi, Socket Pool, Request Session +pool = adafruit_connection_manager.get_radio_socketpool(wifi.radio) +ssl_context = adafruit_connection_manager.get_radio_ssl_context(wifi.radio) +requests = adafruit_requests.Session(pool, ssl_context) + -# Converts seconds to minutes/hours/days def time_calc(input_time): + """Converts seconds to minutes/hours/days""" if input_time < 60: - sleep_int = input_time - time_output = f"{sleep_int:.0f} seconds" - elif 60 <= input_time < 3600: - sleep_int = input_time / 60 - time_output = f"{sleep_int:.0f} minutes" - elif 3600 <= input_time < 86400: - sleep_int = input_time / 60 / 60 - time_output = f"{sleep_int:.0f} hours" - else: - sleep_int = input_time / 60 / 60 / 24 - time_output = f"{sleep_int:.1f} days" - return time_output + return f"{input_time:.0f} seconds" + if input_time < 3600: + return f"{input_time / 60:.0f} minutes" + if input_time < 86400: + return f"{input_time / 60 / 60:.0f} hours" + return f"{input_time / 60 / 60 / 24:.1f} days" # Originally attempted to use SVG. Found JSON exists with same filename. # https://img.shields.io/discord/327254708534116352.svg ADA_DISCORD_JSON = "https://img.shields.io/discord/327254708534116352.json" -# Connect to Wi-Fi -print("\n===============================") -print("Connecting to WiFi...") -requests = adafruit_requests.Session(pool, ssl.create_default_context()) -while not wifi.radio.ipv4_address: - try: - wifi.radio.connect(ssid, password) - except ConnectionError as e: - print("Connection Error:", e) - print("Retrying in 10 seconds") - time.sleep(10) - gc.collect() -print("Connected!\n") - while True: + # Connect to Wi-Fi + print("\nConnecting to WiFi...") + while not wifi.radio.ipv4_address: + try: + wifi.radio.connect(ssid, password) + except ConnectionError as e: + print("❌ Connection Error:", e) + print("Retrying in 10 seconds") + print("✅ Wifi!") try: - print( - "\nAttempting to GET DISCORD SHIELD JSON!" - ) # -------------------------------- - # Print Request to Serial - debug_request = True # Set true to see full request - if debug_request: - print("Full API GET URL: ", ADA_DISCORD_JSON) - print("===============================") + print(" | Attempting to GET Adafruit Discord JSON!") + # Set debug to True for full JSON response. + DEBUG_RESPONSE = True + try: - ada_response = requests.get(ADA_DISCORD_JSON).json() + with requests.get(url=ADA_DISCORD_JSON) as shieldsio_response: + shieldsio_json = shieldsio_response.json() except ConnectionError as e: - print("Connection Error:", e) + print(f"Connection Error: {e}") print("Retrying in 10 seconds") + print(" | ✅ Adafruit Discord JSON!") + + if DEBUG_RESPONSE: + print(" | | Full API GET URL: ", ADA_DISCORD_JSON) + print(" | | JSON Dump: ", shieldsio_json) - # Print Full JSON to Serial - full_ada_json_response = True # Change to true to see full response - if full_ada_json_response: - ada_dump_object = json.dumps(ada_response) - print("JSON Dump: ", ada_dump_object) - - # Print Debugging to Serial - ada_debug = True # Set to True to print Serial data - if ada_debug: - ada_users = ada_response["value"] - print("JSON Value: ", ada_users) - online_string = " online" - replace_with_nothing = "" - string_replace_users = ada_users.replace( - online_string, replace_with_nothing - ) - print("Replaced Value: ", string_replace_users) - print("Monotonic: ", time.monotonic()) + ada_users = shieldsio_json["value"] + ONLINE_STRING = " online" + REPLACE_WITH_NOTHING = "" + active_users = ada_users.replace(ONLINE_STRING, REPLACE_WITH_NOTHING) + print(f" | | Active Online Users: {active_users}") print("\nFinished!") - print("Next Update: ", time_calc(sleep_time)) + print(f"Board Uptime: {time.monotonic()}") + print(f"Next Update: {time_calc(SLEEP_TIME)}") print("===============================") - gc.collect() except (ValueError, RuntimeError) as e: - print("Failed to get data, retrying\n", e) + print(f"Failed to get data, retrying\n {e}") time.sleep(60) - continue - time.sleep(sleep_time) + break + time.sleep(SLEEP_TIME) diff --git a/examples/wifi/expanded/requests_wifi_api_discord.py b/examples/wifi/expanded/requests_wifi_api_discord.py index 91b2b6c..f1b50bc 100644 --- a/examples/wifi/expanded/requests_wifi_api_discord.py +++ b/examples/wifi/expanded/requests_wifi_api_discord.py @@ -1,110 +1,104 @@ -# SPDX-FileCopyrightText: 2023 DJDevon3 +# SPDX-FileCopyrightText: 2024 DJDevon3 # SPDX-License-Identifier: MIT -# Coded for Circuit Python 8.2 -# DJDevon3 Adafruit Feather ESP32-S3 Discord API Example -import json +# Coded for Circuit Python 8.2.x +"""Discord Web Scrape Example""" + import os -import ssl import time -import socketpool +import adafruit_connection_manager import wifi import adafruit_requests -# Active Logged in User Account Required, no tokens required +# Active Logged in User Account Required # WEB SCRAPE authorization key required. Visit URL below. # Learn how: https://github.com/lorenz234/Discord-Data-Scraping # Ensure this is in settings.toml -# "Discord_Authorization": "Request Header Auth here" +# DISCORD_AUTHORIZATION = "Approximately 70 Character Hash Here" # Get WiFi details, ensure these are setup in settings.toml ssid = os.getenv("CIRCUITPY_WIFI_SSID") password = os.getenv("CIRCUITPY_WIFI_PASSWORD") -Discord_Auth = os.getenv("Discord_Authorization") - -# Initialize WiFi Pool (There can be only 1 pool & top of script) -pool = socketpool.SocketPool(wifi.radio) +discord_auth = os.getenv("DISCORD_AUTHORIZATION") # API Polling Rate # 900 = 15 mins, 1800 = 30 mins, 3600 = 1 hour -sleep_time = 900 +SLEEP_TIME = 900 +# Initalize Wifi, Socket Pool, Request Session +pool = adafruit_connection_manager.get_radio_socketpool(wifi.radio) +ssl_context = adafruit_connection_manager.get_radio_ssl_context(wifi.radio) +requests = adafruit_requests.Session(pool, ssl_context) -# Converts seconds to human readable minutes/hours/days -def time_calc(input_time): # input_time in seconds + +def time_calc(input_time): + """Converts seconds to minutes/hours/days""" if input_time < 60: - sleep_int = input_time - time_output = f"{sleep_int:.0f} seconds" - elif 60 <= input_time < 3600: - sleep_int = input_time / 60 - time_output = f"{sleep_int:.0f} minutes" - elif 3600 <= input_time < 86400: - sleep_int = input_time / 60 / 60 - time_output = f"{sleep_int:.1f} hours" - else: - sleep_int = input_time / 60 / 60 / 24 - time_output = f"{sleep_int:.1f} days" - return time_output - - -discord_header = {"Authorization": "" + Discord_Auth} -ADA_SOURCE = ( + return f"{input_time:.0f} seconds" + if input_time < 3600: + return f"{input_time / 60:.0f} minutes" + if input_time < 86400: + return f"{input_time / 60 / 60:.0f} hours" + return f"{input_time / 60 / 60 / 24:.1f} days" + + +DISCORD_HEADER = {"Authorization": "" + discord_auth} +DISCORD_SOURCE = ( "https://discord.com/api/v10/guilds/" + "327254708534116352" # Adafruit Discord ID + "/preview" ) -# Connect to Wi-Fi -print("\n===============================") -print("Connecting to WiFi...") -requests = adafruit_requests.Session(pool, ssl.create_default_context()) -while not wifi.radio.ipv4_address: - try: - wifi.radio.connect(ssid, password) - except ConnectionError as e: - print("Connection Error:", e) - print("Retrying in 10 seconds") - time.sleep(10) -print("Connected!✅") - while True: + # Connect to Wi-Fi + print("\nConnecting to WiFi...") + while not wifi.radio.ipv4_address: + try: + wifi.radio.connect(ssid, password) + except ConnectionError as e: + print("❌ Connection Error:", e) + print("Retrying in 10 seconds") + print("✅ Wifi!") try: - print("\nAttempting to GET Discord Data!") # -------------------------------- - # STREAMER WARNING this will show your credentials! - debug_request = False # Set True to see full request - if debug_request: - print("Full API GET URL: ", ADA_SOURCE) - print("===============================") + print(" | Attempting to GET Discord JSON!") + # Set debug to True for full JSON response. + # WARNING: may include visible credentials + # MICROCONTROLLER WARNING: might crash by returning too much data + DEBUG_RESPONSE = False + try: - ada_res = requests.get(url=ADA_SOURCE, headers=discord_header).json() + with requests.get(url=DISCORD_SOURCE, headers=DISCORD_HEADER) as discord_response: + discord_json = discord_response.json() except ConnectionError as e: - print("Connection Error:", e) + print(f"Connection Error: {e}") print("Retrying in 10 seconds") + print(" | ✅ Discord JSON!") + + if DEBUG_RESPONSE: + print(f" | | Full API GET URL: {DISCORD_SOURCE}") + print(f" | | JSON Dump: {discord_json}") + + discord_name = discord_json["name"] + print(f" | | Name: {discord_name}") - # Print Full JSON to Serial - discord_debug_response = False # Set True to see full response - if discord_debug_response: - ada_discord_dump_object = json.dumps(ada_res) - print("JSON Dump: ", ada_discord_dump_object) + discord_description = discord_json["description"] + print(f" | | Description: {discord_description}") - # Print keys to Serial - discord_debug_keys = True # Set True to print Serial data - if discord_debug_keys: - ada_discord_all_members = ada_res["approximate_member_count"] - print("Members: ", ada_discord_all_members) + discord_all_members = discord_json["approximate_member_count"] + print(f" | | Members: {discord_all_members}") - ada_discord_all_members_online = ada_res["approximate_presence_count"] - print("Online: ", ada_discord_all_members_online) + discord_all_members_online = discord_json["approximate_presence_count"] + print(f" | | Online: {discord_all_members_online}") - print("Finished ✅") - print("Board Uptime: ", time_calc(time.monotonic())) - print("Next Update: ", time_calc(sleep_time)) + print("\nFinished!") + print(f"Board Uptime: {time.monotonic()}") + print(f"Next Update: {time_calc(SLEEP_TIME)}") print("===============================") - except (ConnectionError, ValueError, NameError) as e: - print("Failed to get data, retrying\n", e) + except (ValueError, RuntimeError) as e: + print(f"Failed to get data, retrying\n {e}") time.sleep(60) - continue - time.sleep(sleep_time) + break + time.sleep(SLEEP_TIME) diff --git a/examples/wifi/expanded/requests_wifi_api_fitbit.py b/examples/wifi/expanded/requests_wifi_api_fitbit.py index 022ee22..ee26a0a 100644 --- a/examples/wifi/expanded/requests_wifi_api_fitbit.py +++ b/examples/wifi/expanded/requests_wifi_api_fitbit.py @@ -1,31 +1,20 @@ -# SPDX-FileCopyrightText: 2023 DJDevon3 +# SPDX-FileCopyrightText: 2024 DJDevon3 # SPDX-License-Identifier: MIT -# Coded for Circuit Python 8.2 +# Coded for Circuit Python 8.2.x +"""Fitbit API Example""" import os -import ssl import time +import adafruit_connection_manager import microcontroller -import socketpool import wifi import adafruit_requests -# Initialize WiFi Pool (There can be only 1 pool & top of script) -pool = socketpool.SocketPool(wifi.radio) - -# STREAMER WARNING: private data will be viewable while debug True -debug = False # Set True for full debug view - -# Can use to confirm first instance of NVM is correct refresh token -top_nvm = microcontroller.nvm[0:64].decode() -if debug: - print(f"Top NVM: {top_nvm}") # NVM before settings.toml loaded - # --- Fitbit Developer Account & oAuth App Required: --- # Required: Google Login (Fitbit owned by Google) & Fitbit Device -# Step 1: Create a personal app here: https://dev.fitbit.com +# Step 1: Register a personal app here: https://dev.fitbit.com # Step 2: Use their Tutorial to get the Token and first Refresh Token # Fitbit's Tutorial Step 4 is as far as you need to go. # https://dev.fitbit.com/build/reference/web-api/troubleshooting-guide/oauth2-tutorial/ @@ -39,277 +28,320 @@ # Get WiFi details, ensure these are setup in settings.toml ssid = os.getenv("CIRCUITPY_WIFI_SSID") password = os.getenv("CIRCUITPY_WIFI_PASSWORD") +Fitbit_ClientID = os.getenv("FITBIT_CLIENTID") +Fitbit_Token = os.getenv("FITBIT_ACCESS_TOKEN") +Fitbit_First_Refresh_Token = os.getenv("FITBIT_FIRST_REFRESH_TOKEN") # overides nvm first run only +Fitbit_UserID = os.getenv("FITBIT_USERID") + +# Set debug to True for full INTRADAY JSON response. +# WARNING: may include visible credentials +# MICROCONTROLLER WARNING: might crash by returning too much data +DEBUG = False + +# Set debug to True for full DEVICE (Watch) JSON response. +# WARNING: may include visible credentials +# This will not return enough data to crash your device. +DEBUG_DEVICE = False + +# No data from midnight to 00:15 due to lack of 15 values. +# Debug midnight to display something else in this time frame. +MIDNIGHT_DEBUG = False + +# WARNING: Optional: Resets board nvm to factory default. Clean slate. +# Instructions will be printed to console while reset is True. +RESET_NVM = False # Set True once, then back to False +if RESET_NVM: + microcontroller.nvm[0:64] = bytearray(b"\x00" * 64) +# API Polling Rate +# 900 = 15 mins, 1800 = 30 mins, 3600 = 1 hour +SLEEP_TIME = 900 + +# Initalize Wifi, Socket Pool, Request Session +pool = adafruit_connection_manager.get_radio_socketpool(wifi.radio) +ssl_context = adafruit_connection_manager.get_radio_ssl_context(wifi.radio) +requests = adafruit_requests.Session(pool, ssl_context) -Fitbit_ClientID = os.getenv("Fitbit_ClientID") -Fitbit_Token = os.getenv("Fitbit_Token") -Fitbit_First_Refresh_Token = os.getenv( - "Fitbit_First_Refresh_Token" -) # overides nvm first run only -Fitbit_UserID = os.getenv("Fitbit_UserID") -# Time between API refreshes -# 300 = 5 mins, 900 = 15 mins, 1800 = 30 mins, 3600 = 1 hour -sleep_time = 900 - - -# Converts seconds in minutes/hours/days def time_calc(input_time): + """Converts seconds to minutes/hours/days""" if input_time < 60: - sleep_int = input_time - time_output = f"{sleep_int:.0f} seconds" - elif 60 <= input_time < 3600: - sleep_int = input_time / 60 - time_output = f"{sleep_int:.0f} minutes" - elif 3600 <= input_time < 86400: - sleep_int = input_time / 60 / 60 - time_output = f"{sleep_int:.1f} hours" - else: - sleep_int = input_time / 60 / 60 / 24 - time_output = f"{sleep_int:.1f} days" - return time_output + return f"{input_time:.0f} seconds" + if input_time < 3600: + return f"{input_time / 60:.0f} minutes" + if input_time < 86400: + return f"{input_time / 60 / 60:.0f} hours" + return f"{input_time / 60 / 60 / 24:.1f} days" # Authenticates Client ID & SHA-256 Token to POST -fitbit_oauth_header = {"Content-Type": "application/x-www-form-urlencoded"} -fitbit_oauth_token = "https://api.fitbit.com/oauth2/token" +FITBIT_OAUTH_HEADER = {"Content-Type": "application/x-www-form-urlencoded"} +FITBIT_OAUTH_TOKEN = "https://api.fitbit.com/oauth2/token" -# Connect to Wi-Fi -print("\n===============================") -print("Connecting to WiFi...") -requests = adafruit_requests.Session(pool, ssl.create_default_context()) -while not wifi.radio.ipv4_address: - try: - wifi.radio.connect(ssid, password) - except ConnectionError as e: - print("Connection Error:", e) - print("Retrying in 10 seconds") - time.sleep(10) -print("Connected!\n") - -# First run uses settings.toml token +# Use to confirm first instance of NVM is the correct refresh token +FIRST_RUN = True Refresh_Token = Fitbit_First_Refresh_Token - -if debug: - print(f"Top NVM Again (just to make sure): {top_nvm}") - print(f"Settings.toml Initial Refresh Token: {Fitbit_First_Refresh_Token}") - -latest_15_avg = "Latest 15 Minute Averages" -while True: - # Use Settings.toml refresh token on first run - if top_nvm != Fitbit_First_Refresh_Token: +top_nvm = microcontroller.nvm[0:64].decode() +nvm_bytes = microcontroller.nvm[0:64] +top_nvm_3bytes = nvm_bytes[0:3] +if DEBUG: + print(f"Top NVM Length: {len(top_nvm)}") + print(f"Top NVM: {top_nvm}") + print(f"Top NVM bytes: {top_nvm_3bytes}") +if RESET_NVM: + microcontroller.nvm[0:64] = bytearray(b"\x00" * 64) + if top_nvm_3bytes == b"\x00\x00\x00": + print("TOP NVM IS BRAND NEW! WAITING FOR A FIRST TOKEN") + Fitbit_First_Refresh_Token = top_nvm + print(f"Top NVM RESET: {top_nvm}") # No token should appear Refresh_Token = microcontroller.nvm[0:64].decode() - if debug: - # NVM 64 should match Current Refresh Token - print(f"NVM 64: {microcontroller.nvm[0:64].decode()}") - print(f"Current Refresh_Token: {Refresh_Token}") - else: - if debug: - # First run use settings.toml refresh token instead - print(f"Initial_Refresh_Token: {Refresh_Token}") - - try: - if debug: - print("\n-----Token Refresh POST Attempt -------") - fitbit_oauth_refresh_token = ( - "&grant_type=refresh_token" - + "&client_id=" - + str(Fitbit_ClientID) - + "&refresh_token=" - + str(Refresh_Token) - ) - - # ----------------------------- POST FOR REFRESH TOKEN ----------------------- - if debug: - print( - f"FULL REFRESH TOKEN POST:{fitbit_oauth_token}" - + f"{fitbit_oauth_refresh_token}" - ) - print(f"Current Refresh Token: {Refresh_Token}") - # TOKEN REFRESH POST - fitbit_oauth_refresh_POST = requests.post( - url=fitbit_oauth_token, - data=fitbit_oauth_refresh_token, - headers=fitbit_oauth_header, - ) - try: - fitbit_refresh_oauth_json = fitbit_oauth_refresh_POST.json() - - fitbit_new_token = fitbit_refresh_oauth_json["access_token"] - if debug: - print("Your Private SHA-256 Token: ", fitbit_new_token) - fitbit_access_token = fitbit_new_token # NEW FULL TOKEN - - # If current token valid will respond - fitbit_new_refesh_token = fitbit_refresh_oauth_json["refresh_token"] - Refresh_Token = fitbit_new_refesh_token - fitbit_token_expiration = fitbit_refresh_oauth_json["expires_in"] - fitbit_scope = fitbit_refresh_oauth_json["scope"] - fitbit_token_type = fitbit_refresh_oauth_json["token_type"] - fitbit_user_id = fitbit_refresh_oauth_json["user_id"] - if debug: - print("Next Refresh Token: ", Refresh_Token) - - # Store Next Token into NVM + print(f"Refresh_Token Reset: {Refresh_Token}") # No token should appear +while True: + if not RESET_NVM: + # Connect to Wi-Fi + print("\n📡 Connecting to WiFi...") + while not wifi.radio.ipv4_address: try: - nvmtoken = b"" + fitbit_new_refesh_token + wifi.radio.connect(ssid, password) + except ConnectionError as e: + print("❌ Connection Error:", e) + print("Retrying in 10 seconds") + print("✅ WiFi!") + + if top_nvm is not Refresh_Token and FIRST_RUN is False: + FIRST_RUN = False + Refresh_Token = microcontroller.nvm[0:64].decode() + print(" | INDEFINITE RUN -------") + if DEBUG: + print("Top NVM is Fitbit First Refresh Token") + # NVM 64 should match Current Refresh Token + print(f"NVM 64: {microcontroller.nvm[0:64].decode()}") + print(f"Current Refresh_Token: {Refresh_Token}") + if top_nvm != Fitbit_First_Refresh_Token and FIRST_RUN is True: + if top_nvm_3bytes == b"\x00\x00\x00": + print(" | TOP NVM IS BRAND NEW! WAITING FOR A FIRST TOKEN") + Refresh_Token = Fitbit_First_Refresh_Token + nvmtoken = b"" + Refresh_Token microcontroller.nvm[0:64] = nvmtoken - if debug: - print(f"Next Token for NVM: {nvmtoken.decode()}") - print("Next token written to NVM Successfully!") - except OSError as e: - print("OS Error:", e) - continue - - if debug: - # Extraneous token data for debugging - print("Token Expires in: ", time_calc(fitbit_token_expiration)) - print("Scope: ", fitbit_scope) - print("Token Type: ", fitbit_token_type) - print("UserID: ", fitbit_user_id) - - except KeyError as e: - print("Key Error:", e) - print("Expired token, invalid permission, or (key:value) pair error.") - time.sleep(300) - continue - - # ----------------------------- GET DATA ------------------------------------- - # POST should respond with current & next refresh token we can GET for data - # 64-bit Refresh tokens will "keep alive" SHA-256 token indefinitely - # Fitbit main SHA-256 token expires in 8 hours unless refreshed! - # ---------------------------------------------------------------------------- - detail_level = "1min" # Supported: 1sec | 1min | 5min | 15min - requested_date = "today" # Date format yyyy-MM-dd or today - fitbit_header = { - "Authorization": "Bearer " + fitbit_access_token + "", - "Client-Id": "" + Fitbit_ClientID + "", - } - # Heart Intraday Scope - FITBIT_SOURCE = ( - "https://api.fitbit.com/1/user/" - + Fitbit_UserID - + "/activities/heart/date/today" - + "/1d/" - + detail_level - + ".json" - ) - - print("\nAttempting to GET FITBIT Stats!") - print("===============================") - fitbit_get_response = requests.get(url=FITBIT_SOURCE, headers=fitbit_header) - try: - fitbit_json = fitbit_get_response.json() - intraday_response = fitbit_json["activities-heart-intraday"]["dataset"] - except ConnectionError as e: - print("Connection Error:", e) - print("Retrying in 10 seconds") - - if debug: - print(f"Full API GET URL: {FITBIT_SOURCE}") - print(f"Header: {fitbit_header}") - # print(f"JSON Full Response: {fitbit_json}") - # print(f"Intraday Full Response: {intraday_response}") - + else: + if DEBUG: + print(f"Top NVM: {top_nvm}") + print(f"First Refresh: {Refresh_Token}") + print(f"First Run: {FIRST_RUN}") + Refresh_Token = top_nvm + FIRST_RUN = False + print(" | MANUAL REBOOT TOKEN DIFFERENCE -------") + if DEBUG: + # NVM 64 should not match Current Refresh Token + print("Top NVM is NOT Fitbit First Refresh Token") + print(f"NVM 64: {microcontroller.nvm[0:64].decode()}") + print(f"Current Refresh_Token: {Refresh_Token}") + if top_nvm == Refresh_Token and FIRST_RUN is True: + if DEBUG: + print(f"Top NVM: {top_nvm}") + print(f"First Refresh: {Refresh_Token}") + print(f"First Run: {FIRST_RUN}") + Refresh_Token = Fitbit_First_Refresh_Token + nvmtoken = b"" + Refresh_Token + microcontroller.nvm[0:64] = nvmtoken + FIRST_RUN = False + print(" | FIRST RUN SETTINGS.TOML TOKEN-------") + if DEBUG: + # NVM 64 should match Current Refresh Token + print("Top NVM IS Fitbit First Refresh Token") + print(f"NVM 64: {microcontroller.nvm[0:64].decode()}") + print(f"Current Refresh_Token: {Refresh_Token}") try: - # Fitbit's sync to your mobile device & server every 15 minutes in chunks. - # Pointless to poll their API faster than 15 minute intervals. - activities_heart_value = fitbit_json["activities-heart-intraday"]["dataset"] - response_length = len(activities_heart_value) - if response_length >= 15: - activities_timestamp = fitbit_json["activities-heart"][0]["dateTime"] - print(f"Fitbit Date: {activities_timestamp}") - activities_latest_heart_time = fitbit_json["activities-heart-intraday"][ - "dataset" - ][response_length - 1]["time"] - print(f"Fitbit Time: {activities_latest_heart_time[0:-3]}") - print(f"Today's Logged Pulses : {response_length}") + if DEBUG: + print("\n-----Token Refresh POST Attempt -------") + FITBIT_OAUTH_REFRESH_TOKEN = ( + "&grant_type=refresh_token" + + "&client_id=" + + str(Fitbit_ClientID) + + "&refresh_token=" + + str(Refresh_Token) + ) - # Each 1min heart rate is a 60 second average - activities_latest_heart_value0 = fitbit_json[ - "activities-heart-intraday" - ]["dataset"][response_length - 1]["value"] - activities_latest_heart_value1 = fitbit_json[ - "activities-heart-intraday" - ]["dataset"][response_length - 2]["value"] - activities_latest_heart_value2 = fitbit_json[ - "activities-heart-intraday" - ]["dataset"][response_length - 3]["value"] - activities_latest_heart_value3 = fitbit_json[ - "activities-heart-intraday" - ]["dataset"][response_length - 4]["value"] - activities_latest_heart_value4 = fitbit_json[ - "activities-heart-intraday" - ]["dataset"][response_length - 5]["value"] - activities_latest_heart_value5 = fitbit_json[ - "activities-heart-intraday" - ]["dataset"][response_length - 6]["value"] - activities_latest_heart_value6 = fitbit_json[ - "activities-heart-intraday" - ]["dataset"][response_length - 7]["value"] - activities_latest_heart_value7 = fitbit_json[ - "activities-heart-intraday" - ]["dataset"][response_length - 8]["value"] - activities_latest_heart_value8 = fitbit_json[ - "activities-heart-intraday" - ]["dataset"][response_length - 9]["value"] - activities_latest_heart_value9 = fitbit_json[ - "activities-heart-intraday" - ]["dataset"][response_length - 10]["value"] - activities_latest_heart_value10 = fitbit_json[ - "activities-heart-intraday" - ]["dataset"][response_length - 11]["value"] - activities_latest_heart_value11 = fitbit_json[ - "activities-heart-intraday" - ]["dataset"][response_length - 12]["value"] - activities_latest_heart_value12 = fitbit_json[ - "activities-heart-intraday" - ]["dataset"][response_length - 13]["value"] - activities_latest_heart_value13 = fitbit_json[ - "activities-heart-intraday" - ]["dataset"][response_length - 14]["value"] - activities_latest_heart_value14 = fitbit_json[ - "activities-heart-intraday" - ]["dataset"][response_length - 15]["value"] - latest_15_avg = "Latest 15 Minute Averages" + # ------------------------- POST FOR REFRESH TOKEN -------------------- + print(" | Requesting authorization for next token") + if DEBUG: print( - f"{latest_15_avg}" - + f"{activities_latest_heart_value14}," - + f"{activities_latest_heart_value13}," - + f"{activities_latest_heart_value12}," - + f"{activities_latest_heart_value11}," - + f"{activities_latest_heart_value10}," - + f"{activities_latest_heart_value9}," - + f"{activities_latest_heart_value8}," - + f"{activities_latest_heart_value7}," - + f"{activities_latest_heart_value6}," - + f"{activities_latest_heart_value5}," - + f"{activities_latest_heart_value4}," - + f"{activities_latest_heart_value3}," - + f"{activities_latest_heart_value2}," - + f"{activities_latest_heart_value1}," - + f"{activities_latest_heart_value0}" + "FULL REFRESH TOKEN POST:" + f"{FITBIT_OAUTH_TOKEN}{FITBIT_OAUTH_REFRESH_TOKEN}" ) - else: - print("Waiting for latest 15 values sync...") - print("Not enough values for today to display yet.") - print("No display from midnight to 00:15") - - except KeyError as keyerror: - print(f"Key Error: {keyerror}") - print( - "Too Many Requests, Expired token," - + "invalid permission," - + "or (key:value) pair error." + print(f"Current Refresh Token: {Refresh_Token}") + # TOKEN REFRESH POST + try: + with requests.post( + url=FITBIT_OAUTH_TOKEN, + data=FITBIT_OAUTH_REFRESH_TOKEN, + headers=FITBIT_OAUTH_HEADER, + ) as fitbit_oauth_refresh_POST: + fitbit_refresh_oauth_json = fitbit_oauth_refresh_POST.json() + except adafruit_requests.OutOfRetries as ex: + print(f"OutOfRetries: {ex}") + break + try: + fitbit_new_token = fitbit_refresh_oauth_json["access_token"] + if DEBUG: + print("Your Private SHA-256 Token: ", fitbit_new_token) + fitbit_access_token = fitbit_new_token # NEW FULL TOKEN + + # Overwrites Initial/Old Refresh Token with Next/New Refresh Token + fitbit_new_refesh_token = fitbit_refresh_oauth_json["refresh_token"] + Refresh_Token = fitbit_new_refesh_token + + fitbit_token_expiration = fitbit_refresh_oauth_json["expires_in"] + fitbit_scope = fitbit_refresh_oauth_json["scope"] + fitbit_token_type = fitbit_refresh_oauth_json["token_type"] + fitbit_user_id = fitbit_refresh_oauth_json["user_id"] + if DEBUG: + print("Next Refresh Token: ", Refresh_Token) + try: + # Stores Next token in NVM + nvmtoken = b"" + Refresh_Token + microcontroller.nvm[0:64] = nvmtoken + if DEBUG: + print(f"nvmtoken: {nvmtoken}") + # It's better to always have next token visible. + # You can manually set this token into settings.toml + print(f" | Next Token: {nvmtoken.decode()}") + print(" | 🔑 Next token written to NVM Successfully!") + except OSError as e: + print("OS Error:", e) + continue + if DEBUG: + print("Token Expires in: ", time_calc(fitbit_token_expiration)) + print("Scope: ", fitbit_scope) + print("Token Type: ", fitbit_token_type) + print("UserID: ", fitbit_user_id) + except KeyError as e: + print("Key Error:", e) + print("Expired token, invalid permission, or (key:value) pair error.") + time.sleep(SLEEP_TIME) + continue + # ----------------------------- GET DATA --------------------------------- + # Now that we have POST response with next refresh token we can GET for data + # 64-bit Refresh tokens will "keep alive" SHA-256 token indefinitely + # Fitbit main SHA-256 token expires in 8 hours unless refreshed! + # ------------------------------------------------------------------------ + DETAIL_LEVEL = "1min" # Supported: 1sec | 1min | 5min | 15min + REQUESTED_DATE = "today" # Date format yyyy-MM-dd or "today" + fitbit_header = { + "Authorization": "Bearer " + fitbit_access_token + "", + "Client-Id": "" + Fitbit_ClientID + "", + } + # Heart Intraday Scope + FITBIT_INTRADAY_SOURCE = ( + "https://api.fitbit.com/1/user/" + + Fitbit_UserID + + "/activities/heart/date/" + + REQUESTED_DATE + + "/1d/" + + DETAIL_LEVEL + + ".json" + ) + # Device Details + FITBIT_DEVICE_SOURCE = ( + "https://api.fitbit.com/1/user/" + Fitbit_UserID + "/devices.json" ) - continue - - print("Board Uptime: ", time_calc(time.monotonic())) # Board Up-Time seconds - print("\nFinished!") - print("Next Update in: ", time_calc(sleep_time)) - print("===============================") - except (ValueError, RuntimeError) as e: - print("Failed to get data, retrying\n", e) - time.sleep(60) - continue - time.sleep(sleep_time) + print(" | Attempting to GET Fitbit JSON!") + FBIS = FITBIT_INTRADAY_SOURCE + FBH = fitbit_header + try: + with requests.get(url=FBIS, headers=FBH) as fitbit_get_response: + fitbit_json = fitbit_get_response.json() + except ConnectionError as e: + print("Connection Error:", e) + print("Retrying in 10 seconds") + print(" | ✅ Fitbit Intraday JSON!") + + if DEBUG: + print(f"Full API GET URL: {FBIS}") + print(f"Header: {fitbit_header}") + # This might crash your microcontroller. + # Commented out even in debug. Use only if absolutely necessary. + + # print(f"JSON Full Response: {fitbit_json}") + Intraday_Response = fitbit_json["activities-heart-intraday"]["dataset"] + # print(f"Intraday Full Response: {Intraday_Response}") + try: + # Fitbit's sync to mobile device & server every 15 minutes in chunks. + # Pointless to poll their API faster than 15 minute intervals. + activities_heart_value = fitbit_json["activities-heart-intraday"]["dataset"] + if MIDNIGHT_DEBUG: + RESPONSE_LENGTH = 0 + else: + RESPONSE_LENGTH = len(activities_heart_value) + if RESPONSE_LENGTH >= 15: + activities_timestamp = fitbit_json["activities-heart"][0]["dateTime"] + print(f" | | Fitbit Date: {activities_timestamp}") + if MIDNIGHT_DEBUG: + ACTIVITIES_LATEST_HEART_TIME = "00:05:00" + else: + ACTIVITIES_LATEST_HEART_TIME = fitbit_json["activities-heart-intraday"][ + "dataset" + ][RESPONSE_LENGTH - 1]["time"] + print(f" | | Fitbit Time: {ACTIVITIES_LATEST_HEART_TIME[0:-3]}") + print(f" | | Today's Logged Pulses: {RESPONSE_LENGTH}") + + # Each 1min heart rate is a 60 second average + LATEST_15_AVG = " | | Latest 15 Minute Averages: " + LATEST_15_VALUES = ", ".join( + str(activities_heart_value[i]["value"]) + for i in range(RESPONSE_LENGTH - 1, RESPONSE_LENGTH - 16, -1) + ) + print(f"{LATEST_15_AVG}{LATEST_15_VALUES}") + else: + print(" | Waiting for latest sync...") + print(" | ❌ Not enough values for today to display yet.") + except KeyError as keyerror: + print(f"Key Error: {keyerror}") + print( + "Too Many Requests, " + + "Expired token, " + + "invalid permission, " + + "or (key:value) pair error." + ) + time.sleep(60) + continue + # Getting Fitbit Device JSON (separate from intraday) + # Separate call for Watch Battery Percentage. + print(" | Attempting to GET Device JSON!") + FBDS = FITBIT_DEVICE_SOURCE + FBH = fitbit_header + try: + with requests.get(url=FBDS, headers=FBH) as fitbit_get_device_response: + fitbit_device_json = fitbit_get_device_response.json() + except ConnectionError as e: + print("Connection Error:", e) + print("Retrying in 10 seconds") + print(" | ✅ Fitbit Device JSON!") + + if DEBUG_DEVICE: + print(f"Full API GET URL: {FITBIT_DEVICE_SOURCE}") + print(f"Header: {fitbit_header}") + print(f"JSON Full Response: {fitbit_device_json}") + Device_Response = fitbit_device_json[1]["batteryLevel"] + print(f" | | Watch Battery %: {Device_Response}") + + print("\nFinished!") + print(f"Board Uptime: {time_calc(time.monotonic())}") + print(f"Next Update: {time_calc(SLEEP_TIME)}") + print("===============================") + except (ValueError, RuntimeError) as e: + print("Failed to get data, retrying\n", e) + time.sleep(60) + continue + time.sleep(SLEEP_TIME) + else: + print("🚮 NVM Cleared!") + print( + "⚠️ Save your new access token & refresh token from " + "Fitbits Tutorial (Step 4) to settings.toml now." + ) + print( + "⚠️ If the script runs again" + "(due to settings.toml file save) while reset=True that's ok!" + ) + print("⚠️ Then set RESET_NVM back to False.") + break diff --git a/examples/wifi/expanded/requests_wifi_api_github.py b/examples/wifi/expanded/requests_wifi_api_github.py index f1f86e3..9d6770b 100644 --- a/examples/wifi/expanded/requests_wifi_api_github.py +++ b/examples/wifi/expanded/requests_wifi_api_github.py @@ -1,103 +1,104 @@ -# SPDX-FileCopyrightText: 2022 DJDevon3 for Adafruit Industries +# SPDX-FileCopyrightText: 2024 DJDevon3 # SPDX-License-Identifier: MIT -# Coded for Circuit Python 8.0 -"""DJDevon3 Adafruit Feather ESP32-S2 Github_API_Example""" -import gc -import json +# Coded for Circuit Python 9.x +"""Github API Example""" + import os -import ssl import time -import socketpool +import adafruit_connection_manager import wifi import adafruit_requests -# Initialize WiFi Pool (There can be only 1 pool & top of script) -pool = socketpool.SocketPool(wifi.radio) - -# Time between API refreshes -# 900 = 15 mins, 1800 = 30 mins, 3600 = 1 hour -sleep_time = 900 +# Github developer token required. +username = os.getenv("GITHUB_USERNAME") +token = os.getenv("GITHUB_TOKEN") # Get WiFi details, ensure these are setup in settings.toml ssid = os.getenv("CIRCUITPY_WIFI_SSID") password = os.getenv("CIRCUITPY_WIFI_PASSWORD") -# Github developer token required. -github_username = os.getenv("Github_username") -github_token = os.getenv("Github_token") - -if sleep_time < 60: - sleep_time_conversion = "seconds" - sleep_int = sleep_time -elif 60 <= sleep_time < 3600: - sleep_int = sleep_time / 60 - sleep_time_conversion = "minutes" -elif 3600 <= sleep_time < 86400: - sleep_int = sleep_time / 60 / 60 - sleep_time_conversion = "hours" -else: - sleep_int = sleep_time / 60 / 60 / 24 - sleep_time_conversion = "days" - -github_header = {"Authorization": " token " + github_token} -GH_SOURCE = "https://api.github.com/users/" + github_username - -# Connect to Wi-Fi -print("\n===============================") -print("Connecting to WiFi...") -requests = adafruit_requests.Session(pool, ssl.create_default_context()) -while not wifi.radio.ipv4_address: - try: - wifi.radio.connect(ssid, password) - except ConnectionError as e: - print("Connection Error:", e) - print("Retrying in 10 seconds") - time.sleep(10) - gc.collect() -print("Connected!\n") + +# API Polling Rate +# 900 = 15 mins, 1800 = 30 mins, 3600 = 1 hour +SLEEP_TIME = 900 + +# Set debug to True for full JSON response. +# WARNING: may include visible credentials +DEBUG = False + +# Initalize Wifi, Socket Pool, Request Session +pool = adafruit_connection_manager.get_radio_socketpool(wifi.radio) +ssl_context = adafruit_connection_manager.get_radio_ssl_context(wifi.radio) +requests = adafruit_requests.Session(pool, ssl_context) + +GITHUB_HEADER = {"Authorization": " token " + token} +GITHUB_SOURCE = "https://api.github.com/users/" + username + + +def time_calc(input_time): + """Converts seconds to minutes/hours/days""" + if input_time < 60: + return f"{input_time:.0f} seconds" + if input_time < 3600: + return f"{input_time / 60:.0f} minutes" + if input_time < 86400: + return f"{input_time / 60 / 60:.0f} hours" + return f"{input_time / 60 / 60 / 24:.1f} days" + while True: + # Connect to Wi-Fi + print("\nConnecting to WiFi...") + while not wifi.radio.ipv4_address: + try: + wifi.radio.connect(ssid, password) + except ConnectionError as e: + print("❌ Connection Error:", e) + print("Retrying in 10 seconds") + print("✅ Wifi!") + try: - print("\nAttempting to GET GITHUB Stats!") # -------------------------------- - # Print Request to Serial - debug_request = False # Set true to see full request - if debug_request: - print("Full API GET URL: ", GH_SOURCE) - print("===============================") + print(" | Attempting to GET Github JSON!") try: - github_response = requests.get(url=GH_SOURCE, headers=github_header).json() + with requests.get(url=GITHUB_SOURCE, headers=GITHUB_HEADER) as github_response: + github_json = github_response.json() except ConnectionError as e: print("Connection Error:", e) print("Retrying in 10 seconds") + print(" | ✅ Github JSON!") + + github_joined = github_json["created_at"] + print(" | | Join Date: ", github_joined) + + github_id = github_json["id"] + print(" | | UserID: ", github_id) - # Print Response to Serial - debug_response = False # Set true to see full response - if debug_response: - dump_object = json.dumps(github_response) - print("JSON Dump: ", dump_object) + github_location = github_json["location"] + print(" | | Location: ", github_location) - # Print Keys to Serial - gh_debug_keys = True # Set True to print Serial data - if gh_debug_keys: - github_id = github_response["id"] - print("UserID: ", github_id) + github_name = github_json["name"] + print(" | | Username: ", github_name) - github_username = github_response["name"] - print("Username: ", github_username) + github_repos = github_json["public_repos"] + print(" | | Respositores: ", github_repos) - github_followers = github_response["followers"] - print("Followers: ", github_followers) + github_followers = github_json["followers"] + print(" | | Followers: ", github_followers) + github_bio = github_json["bio"] + print(" | | Bio: ", github_bio) - print("Monotonic: ", time.monotonic()) + if DEBUG: + print("Full API GET URL: ", GITHUB_SOURCE) + print(github_json) print("\nFinished!") - print("Next Update in %s %s" % (int(sleep_int), sleep_time_conversion)) + print(f"Board Uptime: {time_calc(time.monotonic())}") + print(f"Next Update: {time_calc(SLEEP_TIME)}") print("===============================") - gc.collect() except (ValueError, RuntimeError) as e: - print("Failed to get data, retrying\n", e) + print(f"Failed to get data, retrying\n {e}") time.sleep(60) - continue - time.sleep(sleep_time) + break + time.sleep(SLEEP_TIME) diff --git a/examples/wifi/expanded/requests_wifi_api_mastodon.py b/examples/wifi/expanded/requests_wifi_api_mastodon.py index 56f3e8d..bcbf211 100644 --- a/examples/wifi/expanded/requests_wifi_api_mastodon.py +++ b/examples/wifi/expanded/requests_wifi_api_mastodon.py @@ -1,121 +1,112 @@ -# SPDX-FileCopyrightText: 2022 DJDevon3 +# SPDX-FileCopyrightText: 2024 DJDevon3 # SPDX-License-Identifier: MIT -# Coded for Circuit Python 8.0 -"""DJDevon3 Adafruit Feather ESP32-S2 Mastodon_API_Example""" -import gc +# Coded for Circuit Python 8.2.x +"""Mastodon API Example""" + import os -import ssl import time -import socketpool +import adafruit_connection_manager import wifi import adafruit_requests # Mastodon V1 API - Public access (no dev creds or app required) # Visit https://docs.joinmastodon.org/client/public/ for API docs -# For finding your Mastodon User ID -# Login to your mastodon server in a browser, visit your profile, UserID is in the URL. -# Example: https://mastodon.YOURSERVER/web/accounts/YOURUSERIDISHERE +# For finding your Mastodon numerical UserID +# Example: https://mastodon.YOURSERVER/api/v1/accounts/lookup?acct=YourUserName -Mastodon_Server = "mastodon.social" # Set server instance -Mastodon_UserID = "000000000000000000" # Set User ID you want endpoints from +MASTODON_SERVER = "mastodon.social" # Set server instance +MASTODON_USERID = "000000000000000000" # Numerical UserID you want endpoints from # Test in browser first, this will pull up a JSON webpage # https://mastodon.YOURSERVER/api/v1/accounts/YOURUSERIDHERE/statuses?limit=1 -# Initialize WiFi Pool (There can be only 1 pool & top of script) -pool = socketpool.SocketPool(wifi.radio) - -# Time between API refreshes -# 900 = 15 mins, 1800 = 30 mins, 3600 = 1 hour -sleep_time = 900 - # Get WiFi details, ensure these are setup in settings.toml ssid = os.getenv("CIRCUITPY_WIFI_SSID") password = os.getenv("CIRCUITPY_WIFI_PASSWORD") +# API Polling Rate +# 900 = 15 mins, 1800 = 30 mins, 3600 = 1 hour +SLEEP_TIME = 900 + +# Initalize Wifi, Socket Pool, Request Session +pool = adafruit_connection_manager.get_radio_socketpool(wifi.radio) +ssl_context = adafruit_connection_manager.get_radio_ssl_context(wifi.radio) +requests = adafruit_requests.Session(pool, ssl_context) + -# Converts seconds in minutes/hours/days def time_calc(input_time): + """Converts seconds to minutes/hours/days""" if input_time < 60: - sleep_int = input_time - time_output = f"{sleep_int:.0f} seconds" - elif 60 <= input_time < 3600: - sleep_int = input_time / 60 - time_output = f"{sleep_int:.0f} minutes" - elif 3600 <= input_time < 86400: - sleep_int = input_time / 60 / 60 - time_output = f"{sleep_int:.0f} hours" - elif 86400 <= input_time < 432000: - sleep_int = input_time / 60 / 60 / 24 - time_output = f"{sleep_int:.1f} days" - else: # if > 5 days convert float to int & display whole days - sleep_int = input_time / 60 / 60 / 24 - time_output = f"{sleep_int:.0f} days" - return time_output + return f"{input_time:.0f} seconds" + if input_time < 3600: + return f"{input_time / 60:.0f} minutes" + if input_time < 86400: + return f"{input_time / 60 / 60:.0f} hours" + return f"{input_time / 60 / 60 / 24:.1f} days" # Publicly available data no header required MAST_SOURCE = ( - "https://" - + Mastodon_Server - + "/api/v1/accounts/" - + Mastodon_UserID - + "/statuses?limit=1" + "https://" + MASTODON_SERVER + "/api/v1/accounts/" + MASTODON_USERID + "/statuses?limit=1" ) -# Connect to Wi-Fi -print("\n===============================") -print("Connecting to WiFi...") -requests = adafruit_requests.Session(pool, ssl.create_default_context()) -while not wifi.radio.ipv4_address: - try: - wifi.radio.connect(ssid, password) - except ConnectionError as e: - print("Connection Error:", e) - print("Retrying in 10 seconds") - time.sleep(10) - gc.collect() -print("Connected!\n") - while True: + # Connect to Wi-Fi + print("\nConnecting to WiFi...") + while not wifi.radio.ipv4_address: + try: + wifi.radio.connect(ssid, password) + except ConnectionError as e: + print("❌ Connection Error:", e) + print("Retrying in 10 seconds") + print("✅ Wifi!") try: - print("\nAttempting to GET MASTODON Stats!") # ----------------------------- # Print Request to Serial - debug_mastodon_full_response = ( - False # STREAMER WARNING: your client secret will be viewable - ) - print("===============================") - mastodon_response = requests.get(url=MAST_SOURCE) + print(" | Attempting to GET MASTODON JSON!") + + # Set debug to True for full JSON response. + # WARNING: may include visible credentials + # MICROCONTROLLER WARNING: might crash by returning too much data + DEBUG_RESPONSE = False + try: - mastodon_json = mastodon_response.json() + with requests.get(url=MAST_SOURCE) as mastodon_response: + mastodon_json = mastodon_response.json() except ConnectionError as e: - print("Connection Error:", e) + print(f"Connection Error: {e}") print("Retrying in 10 seconds") mastodon_json = mastodon_json[0] - if debug_mastodon_full_response: - print("Full API GET URL: ", MAST_SOURCE) - print(mastodon_json) + print(" | ✅ Mastodon JSON!") + + if DEBUG_RESPONSE: + print(" | | Full API GET URL: ", MAST_SOURCE) mastodon_userid = mastodon_json["account"]["id"] - print("User ID: ", mastodon_userid) + print(f" | | User ID: {mastodon_userid}") + print(mastodon_json) - mastodon_username = mastodon_json["account"]["display_name"] - print("Name: ", mastodon_username) + mastodon_name = mastodon_json["account"]["display_name"] + print(f" | | Name: {mastodon_name}") mastodon_join_date = mastodon_json["account"]["created_at"] - print("Member Since: ", mastodon_join_date) - mastodon_toot_count = mastodon_json["account"]["statuses_count"] - print("Toots: ", mastodon_toot_count) + print(f" | | Member Since: {mastodon_join_date}") mastodon_follower_count = mastodon_json["account"]["followers_count"] - print("Followers: ", mastodon_follower_count) - print("Monotonic: ", time.monotonic()) + print(f" | | Followers: {mastodon_follower_count}") + mastodon_following_count = mastodon_json["account"]["following_count"] + print(f" | | Following: {mastodon_following_count}") + mastodon_toot_count = mastodon_json["account"]["statuses_count"] + print(f" | | Toots: {mastodon_toot_count}") + mastodon_last_toot = mastodon_json["account"]["last_status_at"] + print(f" | | Last Toot: {mastodon_last_toot}") + mastodon_bio = mastodon_json["account"]["note"] + print(f" | | Bio: {mastodon_bio[3:-4]}") # removes included html "
&
" print("\nFinished!") - print("Next Update in: ", time_calc(sleep_time)) + print(f"Board Uptime: {time.monotonic()}") + print(f"Next Update: {time_calc(SLEEP_TIME)}") print("===============================") - gc.collect() except (ValueError, RuntimeError) as e: - print("Failed to get data, retrying\n", e) + print(f"Failed to get data, retrying\n {e}") time.sleep(60) - continue - time.sleep(sleep_time) + break + time.sleep(SLEEP_TIME) diff --git a/examples/wifi/expanded/requests_wifi_api_openskynetwork_private.py b/examples/wifi/expanded/requests_wifi_api_openskynetwork_private.py index 486a4de..0608b30 100644 --- a/examples/wifi/expanded/requests_wifi_api_openskynetwork_private.py +++ b/examples/wifi/expanded/requests_wifi_api_openskynetwork_private.py @@ -1,15 +1,13 @@ -# SPDX-FileCopyrightText: 2023 DJDevon3 +# SPDX-FileCopyrightText: 2024 DJDevon3 # SPDX-License-Identifier: MIT -# Coded for Circuit Python 8.1 -# DJDevon3 ESP32-S3 OpenSkyNetwork_Private_API_Example +# Coded for Circuit Python 9.x +"""OpenSky-Network.org Single Flight Private API Example""" -import json +import binascii import os -import ssl import time -import circuitpython_base64 as base64 -import socketpool +import adafruit_connection_manager import wifi import adafruit_requests @@ -19,135 +17,170 @@ # All active flights JSON: https://opensky-network.org/api/states/all # PICK ONE! :) # JSON order: transponder, callsign, country # ACTIVE transpondes only, for multiple "c822af&icao24=cb3993&icao24=c63923" -transponder = "7c6b2d" +TRANSPONDER = "4b1812" -# Initialize WiFi Pool (There can be only 1 pool & top of script) -pool = socketpool.SocketPool(wifi.radio) +# Get WiFi details, ensure these are setup in settings.toml +ssid = os.getenv("CIRCUITPY_WIFI_SSID") +password = os.getenv("CIRCUITPY_WIFI_PASSWORD") +osnusername = os.getenv("OSN_USERNAME") # Website Credentials +osnpassword = os.getenv("OSN_PASSWORD") # Website Credentials -# Time between API refreshes +# API Polling Rate # 900 = 15 mins, 1800 = 30 mins, 3600 = 1 hour # OpenSky-Networks IP bans for too many requests, check rate limit. # https://openskynetwork.github.io/opensky-api/rest.html#limitations -sleep_time = 1800 +SLEEP_TIME = 1800 + +# Set debug to True for full JSON response. +# WARNING: makes credentials visible +DEBUG = False + +# Initalize Wifi, Socket Pool, Request Session +pool = adafruit_connection_manager.get_radio_socketpool(wifi.radio) +ssl_context = adafruit_connection_manager.get_radio_ssl_context(wifi.radio) +requests = adafruit_requests.Session(pool, ssl_context) + +# -- Base64 Conversion -- +OSN_CREDENTIALS = str(osnusername) + ":" + str(osnpassword) +# base64 encode and strip appended \n from bytearray +OSN_CREDENTIALS_B = binascii.b2a_base64(OSN_CREDENTIALS.encode()).strip() +BASE64_STRING = OSN_CREDENTIALS_B.decode() # bytearray -# Get WiFi details, ensure these are setup in settings.toml -ssid = os.getenv("CIRCUITPY_WIFI_SSID") -password = os.getenv("CIRCUITPY_WIFI_PASSWORD") -osnu = os.getenv("OSN_Username") -osnp = os.getenv("OSN_Password") - -osn_cred = str(osnu) + ":" + str(osnp) -bytes_to_encode = b" " + str(osn_cred) + " " -base64_string = base64.encodebytes(bytes_to_encode) -base64cred = repr(base64_string)[2:-1] - -Debug_Auth = False # STREAMER WARNING this will show your credentials! -if Debug_Auth: - osn_cred = str(osnu) + ":" + str(osnp) - bytes_to_encode = b" " + str(osn_cred) + " " - print(repr(bytes_to_encode)) - base64_string = base64.encodebytes(bytes_to_encode) - print(repr(base64_string)[2:-1]) - base64cred = repr(base64_string)[2:-1] - print("Decoded Bytes:", str(base64cred)) + +if DEBUG: + print("Base64 ByteArray: ", BASE64_STRING) # Requests URL - icao24 is their endpoint required for a transponder # example https://opensky-network.org/api/states/all?icao24=a808c5 -# OSN private requires your username:password to be base64 encoded -osn_header = {"Authorization": "Basic " + str(base64cred)} -OPENSKY_SOURCE = "https://opensky-network.org/api/states/all?" + "icao24=" + transponder +# OSN private: requires your website username:password to be base64 encoded +OPENSKY_HEADER = {"Authorization": "Basic " + BASE64_STRING} +OPENSKY_SOURCE = "https://opensky-network.org/api/states/all?" + "icao24=" + TRANSPONDER -# Converts seconds to human readable minutes/hours/days -def time_calc(input_time): # input_time in seconds +def time_calc(input_time): + """Converts seconds to minutes/hours/days""" if input_time < 60: - sleep_int = input_time - time_output = f"{sleep_int:.0f} seconds" - elif 60 <= input_time < 3600: - sleep_int = input_time / 60 - time_output = f"{sleep_int:.0f} minutes" - elif 3600 <= input_time < 86400: - sleep_int = input_time / 60 / 60 - time_output = f"{sleep_int:.1f} hours" - else: - sleep_int = input_time / 60 / 60 / 24 - time_output = f"{sleep_int:.1f} days" - return time_output + return f"{input_time:.0f} seconds" + if input_time < 3600: + return f"{input_time / 60:.0f} minutes" + if input_time < 86400: + return f"{input_time / 60 / 60:.0f} hours" + return f"{input_time / 60 / 60 / 24:.1f} days" def _format_datetime(datetime): - return "{:02}/{:02}/{} {:02}:{:02}:{:02}".format( - datetime.tm_mon, - datetime.tm_mday, - datetime.tm_year, - datetime.tm_hour, - datetime.tm_min, - datetime.tm_sec, + return ( + f"{datetime.tm_mon:02}/{datetime.tm_mday:02}/{datetime.tm_year} " + f"{datetime.tm_hour:02}:{datetime.tm_min:02}:{datetime.tm_sec:02}" ) -# Connect to Wi-Fi -print("\n===============================") -print("Connecting to WiFi...") -request = adafruit_requests.Session(pool, ssl.create_default_context()) -while not wifi.radio.ipv4_address: +while True: + # Connect to Wi-Fi + print("\nConnecting to WiFi...") + while not wifi.radio.ipv4_address: + try: + wifi.radio.connect(ssid, password) + except ConnectionError as e: + print("❌ Connection Error:", e) + print("Retrying in 10 seconds") + print("✅ Wifi!") + try: - wifi.radio.connect(ssid, password) - except ConnectionError as e: - print("Connection Error:", e) - print("Retrying in 10 seconds") - time.sleep(10) -print("Connected!\n") + print(" | Attempting to GET OpenSky-Network Single Private Flight JSON!") + print(" | Website Credentials Required! Allows more daily calls than Public.") + try: + with requests.get(url=OPENSKY_SOURCE, headers=OPENSKY_HEADER) as opensky_response: + opensky_json = opensky_response.json() + except ConnectionError as e: + print("Connection Error:", e) + print("Retrying in 10 seconds") -while True: - # STREAMER WARNING this will show your credentials! - debug_request = False # Set True to see full request - if debug_request: - print("Full API HEADER: ", str(osn_header)) - print("Full API GET URL: ", OPENSKY_SOURCE) - print("===============================") + print(" | ✅ OpenSky-Network JSON!") - print("\nAttempting to GET OpenSky-Network Data!") - opensky_response = request.get(url=OPENSKY_SOURCE, headers=osn_header).json() + if DEBUG: + print("Full API GET URL: ", OPENSKY_SOURCE) + print("Full API GET Header: ", OPENSKY_HEADER) + print(opensky_json) - # Print Full JSON to Serial (doesn't show credentials) - debug_response = False # Set True to see full response - if debug_response: - dump_object = json.dumps(opensky_response) - print("JSON Dump: ", dump_object) + # ERROR MESSAGE RESPONSES + if "timestamp" in opensky_json: + osn_timestamp = opensky_json["timestamp"] + print(f"❌ Timestamp: {osn_timestamp}") - # Key:Value Serial Debug (doesn't show credentials) - osn_debug_keys = True # Set True to print Serial data - if osn_debug_keys: - try: - osn_flight = opensky_response["time"] - print("Current Unix Time: ", osn_flight) - - current_struct_time = time.localtime(osn_flight) - current_date = "{}".format(_format_datetime(current_struct_time)) - print(f"Unix to Readable Time: {current_date}") - - # Current flight data for single callsign (right now) - osn_single_flight_data = opensky_response["states"] - - if osn_single_flight_data is not None: - print("Flight Data: ", osn_single_flight_data) - transponder = opensky_response["states"][0][0] - print("Transponder: ", transponder) - callsign = opensky_response["states"][0][1] - print("Callsign: ", callsign) - country = opensky_response["states"][0][2] - print("Flight Country: ", country) + if "message" in opensky_json: + osn_message = opensky_json["message"] + print(f"❌ Message: {osn_message}") + + if "error" in opensky_json: + osn_error = opensky_json["error"] + print(f"❌ Error: {osn_error}") + + if "path" in opensky_json: + osn_path = opensky_json["path"] + print(f"❌ Path: {osn_path}") + + if "status" in opensky_json: + osn_status = opensky_json["status"] + print(f"❌ Status: {osn_status}") + + # Current flight data for single callsign (right now) + osn_single_flight_data = opensky_json["states"] + + if osn_single_flight_data is not None: + if DEBUG: + print(f" | | Single Private Flight Data: {osn_single_flight_data}") + + last_contact = opensky_json["states"][0][4] + # print(f" | | Last Contact Unix Time: {last_contact}") + lc_struct_time = time.localtime(last_contact) + lc_readable_time = f"{_format_datetime(lc_struct_time)}" + print(f" | | Last Contact: {lc_readable_time}") + + flight_transponder = opensky_json["states"][0][0] + print(f" | | Transponder: {flight_transponder}") + + callsign = opensky_json["states"][0][1] + print(f" | | Callsign: {callsign}") + + squawk = opensky_json["states"][0][14] + print(f" | | Squawk: {squawk}") + + country = opensky_json["states"][0][2] + print(f" | | Origin: {country}") + + longitude = opensky_json["states"][0][5] + print(f" | | Longitude: {longitude}") + + latitude = opensky_json["states"][0][6] + print(f" | | Latitude: {latitude}") + + # Return Air Flight data if not on ground + on_ground = opensky_json["states"][0][8] + if on_ground is True: + print(f" | | On Ground: {on_ground}") else: - print("Flight has no active data or you're polling too fast.") - - print("\nFinished!") - print("Board Uptime: ", time_calc(time.monotonic())) - print("Next Update: ", time_calc(sleep_time)) - time.sleep(sleep_time) - print("===============================") - - except (ConnectionError, ValueError, NameError) as e: - print("OSN Connection Error:", e) - print("Next Retry: ", time_calc(sleep_time)) - time.sleep(sleep_time) + altitude = opensky_json["states"][0][7] + print(f" | | Barometric Altitude: {altitude}") + + velocity = opensky_json["states"][0][9] + if velocity != "null": + print(f" | | Velocity: {velocity}") + + vertical_rate = opensky_json["states"][0][11] + if vertical_rate != "null": + print(f" | | Vertical Rate: {vertical_rate}") + + else: + print(" | | ❌ Flight has no active data or you're polling too fast.") + + print("\nFinished!") + print(f"Board Uptime: {time_calc(time.monotonic())}") + print(f"Next Update: {time_calc(SLEEP_TIME)}") + print("===============================") + + except (ValueError, RuntimeError) as e: + print(f"Failed to get data, retrying\n {e}") + time.sleep(60) + break + time.sleep(SLEEP_TIME) diff --git a/examples/wifi/expanded/requests_wifi_api_openskynetwork_private_area.py b/examples/wifi/expanded/requests_wifi_api_openskynetwork_private_area.py index 8cd08a2..243c859 100644 --- a/examples/wifi/expanded/requests_wifi_api_openskynetwork_private_area.py +++ b/examples/wifi/expanded/requests_wifi_api_openskynetwork_private_area.py @@ -1,155 +1,159 @@ -# SPDX-FileCopyrightText: 2023 DJDevon3 +# SPDX-FileCopyrightText: 2024 DJDevon3 # SPDX-License-Identifier: MIT -# Coded for Circuit Python 8.1 -# DJDevon3 ESP32-S3 OpenSkyNetwork_Private_Area_API_Example +# Coded for Circuit Python 8.2.x +"""OpenSky-Network.org Private Area API Example""" -import json +import binascii import os -import ssl import time -import circuitpython_base64 as base64 -import socketpool +import adafruit_connection_manager import wifi import adafruit_requests # OpenSky-Network.org Website Login required for this API +# Increased call limit vs Public. # REST API: https://openskynetwork.github.io/opensky-api/rest.html - # Retrieves all traffic within a geographic area (Orlando example) -latmin = "27.22" # east bounding box -latmax = "28.8" # west bounding box -lonmin = "-81.46" # north bounding box -lonmax = "-80.40" # south bounding box +LATMIN = "27.22" # east bounding box +LATMAX = "28.8" # west bounding box +LONMIN = "-81.46" # north bounding box +LONMAX = "-80.40" # south bounding box -# Initialize WiFi Pool (There can be only 1 pool & top of script) -pool = socketpool.SocketPool(wifi.radio) +# Get WiFi details, ensure these are setup in settings.toml +ssid = os.getenv("CIRCUITPY_WIFI_SSID") +password = os.getenv("CIRCUITPY_WIFI_PASSWORD") +osnusername = os.getenv("OSN_USERNAME") # Website Credentials +osnpassword = os.getenv("OSN_PASSWORD") # Website Credentials -# Time between API refreshes +# API Polling Rate # 900 = 15 mins, 1800 = 30 mins, 3600 = 1 hour # OpenSky-Networks IP bans for too many requests, check rate limit. # https://openskynetwork.github.io/opensky-api/rest.html#limitations -sleep_time = 1800 +SLEEP_TIME = 1800 -# Get WiFi details, ensure these are setup in settings.toml -ssid = os.getenv("CIRCUITPY_WIFI_SSID") -password = os.getenv("CIRCUITPY_WIFI_PASSWORD") -# No token required, only website login -osnu = os.getenv("OSN_Username") -osnp = os.getenv("OSN_Password") - -osn_cred = str(osnu) + ":" + str(osnp) -bytes_to_encode = b" " + str(osn_cred) + " " -base64_string = base64.encodebytes(bytes_to_encode) -base64cred = repr(base64_string)[2:-1] - -Debug_Auth = False # STREAMER WARNING this will show your credentials! -if Debug_Auth: - osn_cred = str(osnu) + ":" + str(osnp) - bytes_to_encode = b" " + str(osn_cred) + " " - print(repr(bytes_to_encode)) - base64_string = base64.encodebytes(bytes_to_encode) - print(repr(base64_string)[2:-1]) - base64cred = repr(base64_string)[2:-1] - print("Decoded Bytes:", str(base64cred)) - -# OSN requires your username:password to be base64 encoded -# so technically it's not transmitted in the clear but w/e -osn_header = {"Authorization": "Basic " + str(base64cred)} - -# Example request of all traffic over Florida, geographic areas cost less per call. +# Set debug to True for full JSON response. +# WARNING: makes credentials visible. based on how many flights +# in your area, full response could crash microcontroller +DEBUG = False + +# Initalize Wifi, Socket Pool, Request Session +pool = adafruit_connection_manager.get_radio_socketpool(wifi.radio) +ssl_context = adafruit_connection_manager.get_radio_ssl_context(wifi.radio) +requests = adafruit_requests.Session(pool, ssl_context) + +# -- Base64 Conversion -- +OSN_CREDENTIALS = str(osnusername) + ":" + str(osnpassword) +# base64 encode and strip appended \n from bytearray +OSN_CREDENTIALS_B = binascii.b2a_base64(OSN_CREDENTIALS.encode()).strip() +BASE64_STRING = OSN_CREDENTIALS_B.decode() # bytearray + +if DEBUG: + print("Base64 ByteArray: ", BASE64_STRING) + +# Area requires OpenSky-Network.org username:password to be base64 encoded +OSN_HEADER = {"Authorization": "Basic " + BASE64_STRING} + +# Example request of all traffic over Florida. +# Geographic areas calls cost less against the limit. # https://opensky-network.org/api/states/all?lamin=25.21&lomin=-84.36&lamax=30.0&lomax=-78.40 OPENSKY_SOURCE = ( "https://opensky-network.org/api/states/all?" + "lamin=" - + latmin + + LATMIN + "&lomin=" - + lonmin + + LONMIN + "&lamax=" - + latmax + + LATMAX + "&lomax=" - + lonmax + + LONMAX ) -# Converts seconds to human readable minutes/hours/days -def time_calc(input_time): # input_time in seconds +def time_calc(input_time): + """Converts seconds to minutes/hours/days""" if input_time < 60: - sleep_int = input_time - time_output = f"{sleep_int:.0f} seconds" - elif 60 <= input_time < 3600: - sleep_int = input_time / 60 - time_output = f"{sleep_int:.0f} minutes" - elif 3600 <= input_time < 86400: - sleep_int = input_time / 60 / 60 - time_output = f"{sleep_int:.1f} hours" - else: - sleep_int = input_time / 60 / 60 / 24 - time_output = f"{sleep_int:.1f} days" - return time_output + return f"{input_time:.0f} seconds" + if input_time < 3600: + return f"{input_time / 60:.0f} minutes" + if input_time < 86400: + return f"{input_time / 60 / 60:.0f} hours" + return f"{input_time / 60 / 60 / 24:.1f} days" def _format_datetime(datetime): - return "{:02}/{:02}/{} {:02}:{:02}:{:02}".format( - datetime.tm_mon, - datetime.tm_mday, - datetime.tm_year, - datetime.tm_hour, - datetime.tm_min, - datetime.tm_sec, + return ( + f"{datetime.tm_mon:02}/{datetime.tm_mday:02}/{datetime.tm_year} " + f"{datetime.tm_hour:02}:{datetime.tm_min:02}:{datetime.tm_sec:02}" ) -# Connect to Wi-Fi -print("\n===============================") -print("Connecting to WiFi...") -request = adafruit_requests.Session(pool, ssl.create_default_context()) -while not wifi.radio.ipv4_address: +while True: + # Connect to Wi-Fi + print("\nConnecting to WiFi...") + while not wifi.radio.ipv4_address: + try: + wifi.radio.connect(ssid, password) + except ConnectionError as e: + print("❌ Connection Error:", e) + print("Retrying in 10 seconds") + print("✅ Wifi!") + try: - wifi.radio.connect(ssid, password) - except ConnectionError as e: - print("Connection Error:", e) - print("Retrying in 10 seconds") - time.sleep(10) -print("Connected!\n") + print(" | Attempting to GET OpenSky-Network Area Flights JSON!") + try: + with requests.get(url=OPENSKY_SOURCE, headers=OSN_HEADER) as opensky_response: + opensky_json = opensky_response.json() + except ConnectionError as e: + print("Connection Error:", e) + print("Retrying in 10 seconds") -while True: - # STREAMER WARNING this will show your credentials! - debug_request = False # Set True to see full request - if debug_request: - print("Full API HEADER: ", str(osn_header)) - print("Full API GET URL: ", OPENSKY_SOURCE) - print("===============================") + print(" | ✅ OpenSky-Network JSON!") - print("\nAttempting to GET OpenSky-Network Data!") - opensky_response = request.get(url=OPENSKY_SOURCE, headers=osn_header).json() + if DEBUG: + print("Full API GET URL: ", OPENSKY_SOURCE) + print(opensky_json) - # Print Full JSON to Serial (doesn't show credentials) - debug_response = False # Set True to see full response - if debug_response: - dump_object = json.dumps(opensky_response) - print("JSON Dump: ", dump_object) + # ERROR MESSAGE RESPONSES + if "timestamp" in opensky_json: + osn_timestamp = opensky_json["timestamp"] + print(f"❌ Timestamp: {osn_timestamp}") - # Key:Value Serial Debug (doesn't show credentials) - osn_debug_keys = True # Set True to print Serial data - if osn_debug_keys: - try: - osn_flight = opensky_response["time"] - print("Current Unix Time: ", osn_flight) + if "message" in opensky_json: + osn_message = opensky_json["message"] + print(f"❌ Message: {osn_message}") + + if "error" in opensky_json: + osn_error = opensky_json["error"] + print(f"❌ Error: {osn_error}") + + if "path" in opensky_json: + osn_path = opensky_json["path"] + print(f"❌ Path: {osn_path}") - current_struct_time = time.localtime(osn_flight) - current_date = "{}".format(_format_datetime(current_struct_time)) - print(f"Unix to Readable Time: {current_date}") + if "status" in opensky_json: + osn_status = opensky_json["status"] + print(f"❌ Status: {osn_status}") - # Current flight data for single callsign (right now) - osn_all_flights = opensky_response["states"] + # Current flight data for single callsign (right now) + osn_all_flights = opensky_json["states"] + + if osn_all_flights is not None: + if DEBUG: + print(f" | | Area Flights Full Response: {osn_all_flights}") + + osn_time = opensky_json["time"] + # print(f" | | Last Contact Unix Time: {osn_time}") + osn_struct_time = time.localtime(osn_time) + osn_readable_time = f"{_format_datetime(osn_struct_time)}" + print(f" | | Timestamp: {osn_readable_time}") if osn_all_flights is not None: # print("Flight Data: ", osn_all_flights) for flights in osn_all_flights: - osn_t = f"Trans:{flights[0]} " - osn_c = f"Sign:{flights[1]} " + osn_t = f" | | Trans:{flights[0]} " + osn_c = f"Sign:{flights[1]}" osn_o = f"Origin:{flights[2]} " osn_tm = f"Time:{flights[3]} " osn_l = f"Last:{flights[4]} " @@ -171,16 +175,17 @@ def _format_datetime(datetime): string2 = f"{osn_la}{osn_ba}{osn_g}{osn_v}{osn_h}{osn_vr}" string3 = f"{osn_s}{osn_ga}{osn_sq}{osn_pr}{osn_ps}{osn_ca}" print(f"{string1}{string2}{string3}") - else: - print("Flight has no active data or you're polling too fast.") - - print("\nFinished!") - print("Board Uptime: ", time_calc(time.monotonic())) - print("Next Update: ", time_calc(sleep_time)) - time.sleep(sleep_time) - print("===============================") - - except (ConnectionError, ValueError, NameError) as e: - print("OSN Connection Error:", e) - print("Next Retry: ", time_calc(sleep_time)) - time.sleep(sleep_time) + + else: + print(" | | ❌ Area has no active data or you're polling too fast.") + + print("\nFinished!") + print(f"Board Uptime: {time_calc(time.monotonic())}") + print(f"Next Update: {time_calc(SLEEP_TIME)}") + print("===============================") + + except (ValueError, RuntimeError) as e: + print(f"Failed to get data, retrying\n {e}") + time.sleep(60) + break + time.sleep(SLEEP_TIME) diff --git a/examples/wifi/expanded/requests_wifi_api_openskynetwork_public.py b/examples/wifi/expanded/requests_wifi_api_openskynetwork_public.py index 5bcd69b..f89f88d 100644 --- a/examples/wifi/expanded/requests_wifi_api_openskynetwork_public.py +++ b/examples/wifi/expanded/requests_wifi_api_openskynetwork_public.py @@ -1,13 +1,12 @@ -# SPDX-FileCopyrightText: 2023 DJDevon3 +# SPDX-FileCopyrightText: 2024 DJDevon3 # SPDX-License-Identifier: MIT -# Coded for Circuit Python 8.1 -# Adafruit Feather ESP32-S3 OpenSkyNetwork_Public_API_Example -import json +# Coded for Circuit Python 9.x +"""OpenSky-Network.org Single Flight Public API Example""" + import os -import ssl import time -import socketpool +import adafruit_connection_manager import wifi import adafruit_requests @@ -17,121 +16,155 @@ # All active flights JSON: https://opensky-network.org/api/states/all PICK ONE! # JSON order: transponder, callsign, country # ACTIVE transpondes only, for multiple "c822af&icao24=cb3993&icao24=c63923" -transponder = "ab1644" +TRANSPONDER = "88044d" -# Initialize WiFi Pool (There can be only 1 pool & top of script) -pool = socketpool.SocketPool(wifi.radio) +# Get WiFi details, ensure these are setup in settings.toml +ssid = os.getenv("CIRCUITPY_WIFI_SSID") +password = os.getenv("CIRCUITPY_WIFI_PASSWORD") -# Time between API refreshes +# API Polling Rate # 900 = 15 mins, 1800 = 30 mins, 3600 = 1 hour -# OpenSky-Networks will temp ban your IP for too many daily requests. +# OpenSky-Networks IP bans for too many requests, check rate limit. # https://openskynetwork.github.io/opensky-api/rest.html#limitations -sleep_time = 1800 +SLEEP_TIME = 1800 -# Get WiFi details, ensure these are setup in settings.toml -ssid = os.getenv("CIRCUITPY_WIFI_SSID") -password = os.getenv("CIRCUITPY_WIFI_PASSWORD") +# Set debug to True for full JSON response. +# WARNING: makes credentials visible +DEBUG = False + +# Initalize Wifi, Socket Pool, Request Session +pool = adafruit_connection_manager.get_radio_socketpool(wifi.radio) +ssl_context = adafruit_connection_manager.get_radio_ssl_context(wifi.radio) +requests = adafruit_requests.Session(pool, ssl_context) # Requests URL - icao24 is their endpoint required for a transponder # example https://opensky-network.org/api/states/all?icao24=a808c5 -OPENSKY_SOURCE = "https://opensky-network.org/api/states/all?" + "icao24=" + transponder +OPENSKY_SOURCE = "https://opensky-network.org/api/states/all?" + "icao24=" + TRANSPONDER -# Converts seconds to human readable minutes/hours/days -def time_calc(input_time): # input_time in seconds +def time_calc(input_time): + """Converts seconds to minutes/hours/days""" if input_time < 60: - sleep_int = input_time - time_output = f"{sleep_int:.0f} seconds" - elif 60 <= input_time < 3600: - sleep_int = input_time / 60 - time_output = f"{sleep_int:.0f} minutes" - elif 3600 <= input_time < 86400: - sleep_int = input_time / 60 / 60 - time_output = f"{sleep_int:.1f} hours" - else: - sleep_int = input_time / 60 / 60 / 24 - time_output = f"{sleep_int:.1f} days" - return time_output + return f"{input_time:.0f} seconds" + if input_time < 3600: + return f"{input_time / 60:.0f} minutes" + if input_time < 86400: + return f"{input_time / 60 / 60:.0f} hours" + return f"{input_time / 60 / 60 / 24:.1f} days" def _format_datetime(datetime): - return "{:02}/{:02}/{} {:02}:{:02}:{:02}".format( - datetime.tm_mon, - datetime.tm_mday, - datetime.tm_year, - datetime.tm_hour, - datetime.tm_min, - datetime.tm_sec, + return ( + f"{datetime.tm_mon:02}/{datetime.tm_mday:02}/{datetime.tm_year} " + f"{datetime.tm_hour:02}:{datetime.tm_min:02}:{datetime.tm_sec:02}" ) -# Connect to Wi-Fi -print("\n===============================") -print("Connecting to WiFi...") -requests = adafruit_requests.Session(pool, ssl.create_default_context()) -while not wifi.radio.ipv4_address: - try: - wifi.radio.connect(ssid, password) - except ConnectionError as e: - print("Connection Error:", e) - print("Retrying in 10 seconds") - time.sleep(10) -print("Connected!\n") - while True: - debug_request = True # Set true to see full request - if debug_request: - print("Full API GET URL: ", OPENSKY_SOURCE) - print("===============================") + # Connect to Wi-Fi + print("\nConnecting to WiFi...") + while not wifi.radio.ipv4_address: + try: + wifi.radio.connect(ssid, password) + except ConnectionError as e: + print("❌ Connection Error:", e) + print("Retrying in 10 seconds") + print("✅ Wifi!") + try: - print("\nAttempting to GET OpenSky-Network Stats!") - opensky_response = requests.get(url=OPENSKY_SOURCE) - osn_json = opensky_response.json() - except (ConnectionError, ValueError, NameError) as e: - print("Host No Response Error:", e) - - # Print Full JSON to Serial - debug_response = False # Set true to see full response - if debug_response: - dump_object = json.dumps(osn_json) - print("JSON Dump: ", dump_object) - - # Print to Serial - osn_debug_keys = True # Set true to print Serial data - if osn_debug_keys: + print(" | Attempting to GET OpenSky-Network Single Public Flight JSON!") + print(" | Website Credentials NOT Required! Less daily calls than Private.") try: - osn_flight = osn_json["time"] - print("Current Unix Time: ", osn_flight) - - current_struct_time = time.localtime(osn_flight) - current_date = "{}".format(_format_datetime(current_struct_time)) - print(f"Unix to Readable Time: {current_date}") - - osn_single_flight_data = osn_json["states"] - if osn_single_flight_data is not None: - print("Flight Data: ", osn_single_flight_data) - transponder = osn_json["states"][0][0] - print("Transponder: ", transponder) - callsign = osn_json["states"][0][1] - print("Callsign: ", callsign) - country = osn_json["states"][0][2] - print("Flight Country: ", country) + with requests.get(url=OPENSKY_SOURCE) as opensky_response: + opensky_json = opensky_response.json() + except ConnectionError as e: + print("Connection Error:", e) + print("Retrying in 10 seconds") + + print(" | ✅ OpenSky-Network Public JSON!") + + if DEBUG: + print("Full API GET URL: ", OPENSKY_SOURCE) + print(opensky_json) + + # ERROR MESSAGE RESPONSES + if "timestamp" in opensky_json: + osn_timestamp = opensky_json["timestamp"] + print(f"❌ Timestamp: {osn_timestamp}") + + if "message" in opensky_json: + osn_message = opensky_json["message"] + print(f"❌ Message: {osn_message}") + + if "error" in opensky_json: + osn_error = opensky_json["error"] + print(f"❌ Error: {osn_error}") + + if "path" in opensky_json: + osn_path = opensky_json["path"] + print(f"❌ Path: {osn_path}") + + if "status" in opensky_json: + osn_status = opensky_json["status"] + print(f"❌ Status: {osn_status}") + + # Current flight data for single callsign (right now) + osn_single_flight_data = opensky_json["states"] + + if osn_single_flight_data is not None: + if DEBUG: + print(f" | | Single Flight Public Data: {osn_single_flight_data}") + + last_contact = opensky_json["states"][0][4] + # print(f" | | Last Contact Unix Time: {last_contact}") + lc_struct_time = time.localtime(last_contact) + lc_readable_time = f"{_format_datetime(lc_struct_time)}" + print(f" | | Last Contact: {lc_readable_time}") + + flight_transponder = opensky_json["states"][0][0] + print(f" | | Transponder: {flight_transponder}") + + callsign = opensky_json["states"][0][1] + print(f" | | Callsign: {callsign}") + + squawk = opensky_json["states"][0][14] + print(f" | | Squawk: {squawk}") + + country = opensky_json["states"][0][2] + print(f" | | Origin: {country}") + + longitude = opensky_json["states"][0][5] + print(f" | | Longitude: {longitude}") + + latitude = opensky_json["states"][0][6] + print(f" | | Latitude: {latitude}") + + # Return Air Flight data if not on ground + on_ground = opensky_json["states"][0][8] + if on_ground is True: + print(f" | | On Ground: {on_ground}") else: - print("This flight has no active data or you're polling too fast.") - print( - "Read: https://openskynetwork.github.io/opensky-api/rest.html#limitations" - ) - print( - "Public Limits: 10 second max poll rate & 400 weighted calls daily" - ) - - print("\nFinished!") - print("Board Uptime: ", time_calc(time.monotonic())) - print("Next Update: ", time_calc(sleep_time)) - time.sleep(sleep_time) - print("===============================") - - except (ConnectionError, ValueError, NameError) as e: - print("OSN Connection Error:", e) - print("Next Retry: ", time_calc(sleep_time)) - time.sleep(sleep_time) + altitude = opensky_json["states"][0][7] + print(f" | | Barometric Altitude: {altitude}") + + velocity = opensky_json["states"][0][9] + if velocity != "null": + print(f" | | Velocity: {velocity}") + + vertical_rate = opensky_json["states"][0][11] + if vertical_rate != "null": + print(f" | | Vertical Rate: {vertical_rate}") + else: + print("This flight has no active data or you're polling too fast.") + print("Public Limits: 10 second max poll & 400 weighted calls daily") + + print("\nFinished!") + print(f"Board Uptime: {time_calc(time.monotonic())}") + print(f"Next Update: {time_calc(SLEEP_TIME)}") + print("===============================") + + except (ValueError, RuntimeError) as e: + print(f"Failed to get data, retrying\n {e}") + time.sleep(60) + break + time.sleep(SLEEP_TIME) diff --git a/examples/wifi/expanded/requests_wifi_api_premiereleague.py b/examples/wifi/expanded/requests_wifi_api_premiereleague.py new file mode 100644 index 0000000..5f61e1a --- /dev/null +++ b/examples/wifi/expanded/requests_wifi_api_premiereleague.py @@ -0,0 +1,83 @@ +# SPDX-FileCopyrightText: 2024 DJDevon3 +# SPDX-License-Identifier: MIT +# Coded for Circuit Python 8.2.x +"""Premiere League Total Players API Example""" + +import os +import time + +import adafruit_connection_manager +import adafruit_json_stream as json_stream +import wifi + +import adafruit_requests + +# Public API. No user or token required + +# Get WiFi details, ensure these are setup in settings.toml +ssid = os.getenv("CIRCUITPY_WIFI_SSID") +password = os.getenv("CIRCUITPY_WIFI_PASSWORD") + +# API Polling Rate +# 900 = 15 mins, 1800 = 30 mins, 3600 = 1 hour +SLEEP_TIME = 900 + +# Initalize Wifi, Socket Pool, Request Session +pool = adafruit_connection_manager.get_radio_socketpool(wifi.radio) +ssl_context = adafruit_connection_manager.get_radio_ssl_context(wifi.radio) +requests = adafruit_requests.Session(pool, ssl_context) + +# Publicly available data no header required +PREMIERE_LEAGUE_SOURCE = "https://fantasy.premierleague.com/api/bootstrap-static/" + + +def time_calc(input_time): + """Converts seconds to minutes/hours/days""" + if input_time < 60: + return f"{input_time:.0f} seconds" + if input_time < 3600: + return f"{input_time / 60:.0f} minutes" + if input_time < 86400: + return f"{input_time / 60 / 60:.0f} hours" + return f"{input_time / 60 / 60 / 24:.1f} days" + + +while True: + # Connect to Wi-Fi + print("\nConnecting to WiFi...") + while not wifi.radio.ipv4_address: + try: + wifi.radio.connect(ssid, password) + except ConnectionError as e: + print("❌ Connection Error:", e) + print("Retrying in 10 seconds") + print("✅ Wifi!") + + try: + print(" | Attempting to GET Premiere League JSON!") + + # Set debug to True for full JSON response. + # WARNING: may include visible credentials + # MICROCONTROLLER WARNING: might crash by returning too much data + DEBUG_RESPONSE = False + + try: + with requests.get(url=PREMIERE_LEAGUE_SOURCE) as PREMIERE_LEAGUE_RESPONSE: + pl_json = json_stream.load(PREMIERE_LEAGUE_RESPONSE.iter_content(32)) + except ConnectionError as e: + print(f"Connection Error: {e}") + print("Retrying in 10 seconds") + print(" | ✅ Premiere League JSON!") + + print(f" | Total Premiere League Players: {pl_json['total_players']}") + + print("\nFinished!") + print(f"Board Uptime: {time.monotonic()}") + print(f"Next Update: {time_calc(SLEEP_TIME)}") + print("===============================") + + except (ValueError, RuntimeError) as e: + print(f"Failed to get data, retrying\n {e}") + time.sleep(60) + break + time.sleep(SLEEP_TIME) diff --git a/examples/wifi/expanded/requests_wifi_api_queuetimes.py b/examples/wifi/expanded/requests_wifi_api_queuetimes.py new file mode 100644 index 0000000..b6fecaf --- /dev/null +++ b/examples/wifi/expanded/requests_wifi_api_queuetimes.py @@ -0,0 +1,86 @@ +# SPDX-FileCopyrightText: 2024 DJDevon3 +# SPDX-License-Identifier: MIT +# Coded for Circuit Python 9.x +"""Queue-Times.com API Example""" + +import os + +import adafruit_connection_manager +import wifi + +import adafruit_requests + +# Initalize Wifi, Socket Pool, Request Session +pool = adafruit_connection_manager.get_radio_socketpool(wifi.radio) +ssl_context = adafruit_connection_manager.get_radio_ssl_context(wifi.radio) +requests = adafruit_requests.Session(pool, ssl_context) + +# Time between API refreshes +# 900 = 15 mins, 1800 = 30 mins, 3600 = 1 hour +SLEEP_TIME = 300 + +# Get WiFi details, ensure these are setup in settings.toml +ssid = os.getenv("CIRCUITPY_WIFI_SSID") +password = os.getenv("CIRCUITPY_WIFI_PASSWORD") + +# Publicly Open API (no credentials required) +QTIMES_SOURCE = "https://queue-times.com/parks/16/queue_times.json" + + +def time_calc(input_time): + """Converts seconds to minutes/hours/days""" + if input_time < 60: + return f"{input_time:.0f} seconds" + if input_time < 3600: + return f"{input_time / 60:.0f} minutes" + if input_time < 86400: + return f"{input_time / 60 / 60:.0f} hours" + return f"{input_time / 60 / 60 / 24:.1f} days" + + +qtimes_json = {} + +# Connect to Wi-Fi +print("\n===============================") +print("Connecting to WiFi...") +while not wifi.radio.ipv4_address: + try: + wifi.radio.connect(ssid, password) + except ConnectionError as e: + print("❌ Connection Error:", e) + print("Retrying in 10 seconds") +print("✅ WiFi!") + +try: + with requests.get(url=QTIMES_SOURCE) as qtimes_response: + qtimes_json = qtimes_response.json() + + print(" | ✅ Queue-Times JSON\n") + DEBUG_QTIMES = False + if DEBUG_QTIMES: + print("Full API GET URL: ", QTIMES_SOURCE) + print(qtimes_json) + + # Poll Once and end script + for land in qtimes_json["lands"]: + qtimes_lands = str(land["name"]) + print(f" | Land: {qtimes_lands}") + + # Loop through each ride in the land + for ride in land["rides"]: + qtimes_rides = str(ride["name"]) + qtimes_queuetime = str(ride["wait_time"]) + qtimes_isopen = str(ride["is_open"]) + + print(f" | | Ride: {qtimes_rides}") + print(f" | | Queue Time: {qtimes_queuetime} Minutes") + if qtimes_isopen == "False": + print(" | | Status: Closed\n") + elif qtimes_isopen == "True": + print(" | | Status: Open\n") + else: + print(" | | Status: Unknown\n") + + +except ConnectionError as e: + print("Connection Error:", e) diff --git a/examples/wifi/expanded/requests_wifi_api_rocketlaunch_live.py b/examples/wifi/expanded/requests_wifi_api_rocketlaunch_live.py new file mode 100644 index 0000000..67034bb --- /dev/null +++ b/examples/wifi/expanded/requests_wifi_api_rocketlaunch_live.py @@ -0,0 +1,132 @@ +# SPDX-FileCopyrightText: 2024 DJDevon3 +# SPDX-License-Identifier: MIT +# Coded for Circuit Python 9.0 +"""RocketLaunch.Live API Example""" + +import os +import time + +import adafruit_connection_manager +import wifi + +import adafruit_requests + +# Time between API refreshes +# 900 = 15 mins, 1800 = 30 mins, 3600 = 1 hour +SLEEP_TIME = 43200 + +# Get WiFi details, ensure these are setup in settings.toml +ssid = os.getenv("CIRCUITPY_WIFI_SSID") +password = os.getenv("CIRCUITPY_WIFI_PASSWORD") + + +def time_calc(input_time): + """Converts seconds to minutes/hours/days""" + if input_time < 60: + return f"{input_time:.0f} seconds" + if input_time < 3600: + return f"{input_time / 60:.0f} minutes" + if input_time < 86400: + return f"{input_time / 60 / 60:.0f} hours" + return f"{input_time / 60 / 60 / 24:.1f} days" + + +# Publicly available data no header required +# The number at the end is the amount of launches (max 5 free api) +ROCKETLAUNCH_SOURCE = "https://fdo.rocketlaunch.live/json/launches/next/1" + +# Initalize Wifi, Socket Pool, Request Session +pool = adafruit_connection_manager.get_radio_socketpool(wifi.radio) +ssl_context = adafruit_connection_manager.get_radio_ssl_context(wifi.radio) +requests = adafruit_requests.Session(pool, ssl_context) + +while True: + # Connect to Wi-Fi + print("\n===============================") + print("Connecting to WiFi...") + while not wifi.radio.ipv4_address: + try: + wifi.radio.connect(ssid, password) + except ConnectionError as e: + print("❌ Connection Error:", e) + print("Retrying in 10 seconds") + print("✅ Wifi!") + try: + # Print Request to Serial + print(" | Attempting to GET RocketLaunch.Live JSON!") + time.sleep(2) + debug_rocketlaunch_full_response = False + + try: + with requests.get(url=ROCKETLAUNCH_SOURCE) as rocketlaunch_response: + rocketlaunch_json = rocketlaunch_response.json() + except ConnectionError as e: + print("Connection Error:", e) + print("Retrying in 10 seconds") + print(" | ✅ RocketLaunch.Live JSON!") + + if debug_rocketlaunch_full_response: + print("Full API GET URL: ", ROCKETLAUNCH_SOURCE) + print(rocketlaunch_json) + + # JSON Endpoints + RLFN = str(rocketlaunch_json["result"][0]["name"]) + RLWO = str(rocketlaunch_json["result"][0]["win_open"]) + TZERO = str(rocketlaunch_json["result"][0]["t0"]) + RLWC = str(rocketlaunch_json["result"][0]["win_close"]) + RLP = str(rocketlaunch_json["result"][0]["provider"]["name"]) + RLVN = str(rocketlaunch_json["result"][0]["vehicle"]["name"]) + RLPN = str(rocketlaunch_json["result"][0]["pad"]["name"]) + RLLS = str(rocketlaunch_json["result"][0]["pad"]["location"]["name"]) + RLLD = str(rocketlaunch_json["result"][0]["launch_description"]) + RLM = str(rocketlaunch_json["result"][0]["mission_description"]) + RLDATE = str(rocketlaunch_json["result"][0]["date_str"]) + + # Print to serial & display label if endpoint not "None" + if RLDATE != "None": + print(f" | | Date: {RLDATE}") + if RLFN != "None": + print(f" | | Flight: {RLFN}") + if RLP != "None": + print(f" | | Provider: {RLP}") + if RLVN != "None": + print(f" | | Vehicle: {RLVN}") + + # Launch time can sometimes be Window Open to Close, T-Zero, or weird combination. + # Should obviously be standardized but they're not input that way. + # Have to account for every combination of 3 conditions. + # T-Zero Launch Time Conditionals + if RLWO == "None" and TZERO != "None" and RLWC != "None": + print(f" | | Window: {TZERO} | {RLWC}") + elif RLWO != "None" and TZERO != "None" and RLWC == "None": + print(f" | | Window: {RLWO} | {TZERO}") + elif RLWO != "None" and TZERO == "None" and RLWC != "None": + print(f" | | Window: {RLWO} | {RLWC}") + elif RLWO != "None" and TZERO != "None" and RLWC != "None": + print(f" | | Window: {RLWO} | {TZERO} | {RLWC}") + elif RLWO == "None" and TZERO != "None" and RLWC == "None": + print(f" | | Window: {TZERO}") + elif RLWO != "None" and TZERO == "None" and RLWC == "None": + print(f" | | Window: {RLWO}") + elif RLWO == "None" and TZERO == "None" and RLWC != "None": + print(f" | | Window: {RLWC}") + + if RLLS != "None": + print(f" | | Site: {RLLS}") + if RLPN != "None": + print(f" | | Pad: {RLPN}") + if RLLD != "None": + print(f" | | Description: {RLLD}") + if RLM != "None": + print(f" | | Mission: {RLM}") + + print("\nFinished!") + print(f"Board Uptime: {time_calc(time.monotonic())}") + print(f"Next Update: {time_calc(SLEEP_TIME)}") + print("===============================") + + except (ValueError, RuntimeError) as e: + print("Failed to get data, retrying\n", e) + time.sleep(60) + break + time.sleep(SLEEP_TIME) diff --git a/examples/wifi/expanded/requests_wifi_api_steam.py b/examples/wifi/expanded/requests_wifi_api_steam.py index 208b6d6..42d1e31 100644 --- a/examples/wifi/expanded/requests_wifi_api_steam.py +++ b/examples/wifi/expanded/requests_wifi_api_steam.py @@ -1,41 +1,47 @@ -# SPDX-FileCopyrightText: 2022 DJDevon3 (Neradoc & Deshipu helped) for Adafruit Industries +# SPDX-FileCopyrightText: 2024 DJDevon3 # SPDX-License-Identifier: MIT -# Coded for Circuit Python 8.0 -"""DJDevon3 Adafruit Feather ESP32-S2 api_steam Example""" -import gc -import json +# Coded for Circuit Python 8.2.x +"""Steam API Get Owned Games Example""" + import os -import ssl import time -import socketpool +import adafruit_connection_manager import wifi import adafruit_requests # Steam API Docs: https://steamcommunity.com/dev # Steam API Key: https://steamcommunity.com/dev/apikey -# Steam Usernumber: Visit https://steamcommunity.com -# click on your profile icon, your usernumber will be in the browser url. +# Numerical Steam ID: Visit https://store.steampowered.com/account/ +# Your account name will be in big bold letters. +# Your numerical STEAM ID will be below in a very small font. # Get WiFi details, ensure these are setup in settings.toml ssid = os.getenv("CIRCUITPY_WIFI_SSID") password = os.getenv("CIRCUITPY_WIFI_PASSWORD") # Requires Steam Developer API key -steam_usernumber = os.getenv("steam_id") -steam_apikey = os.getenv("steam_api_key") - -# Initialize WiFi Pool (There can be only 1 pool & top of script) -pool = socketpool.SocketPool(wifi.radio) +steam_usernumber = os.getenv("STEAM_ID") +steam_apikey = os.getenv("STEAM_API_KEY") -# Time between API refreshes +# API Polling Rate # 900 = 15 mins, 1800 = 30 mins, 3600 = 1 hour -sleep_time = 900 +SLEEP_TIME = 3600 + +# Set debug to True for full JSON response. +# WARNING: Steam's full response will overload most microcontrollers +# SET TO TRUE IF YOU FEEL BRAVE =) +DEBUG = False + +# Initalize Wifi, Socket Pool, Request Session +pool = adafruit_connection_manager.get_radio_socketpool(wifi.radio) +ssl_context = adafruit_connection_manager.get_radio_ssl_context(wifi.radio) +requests = adafruit_requests.Session(pool, ssl_context) # Deconstruct URL (pylint hates long lines) # http://api.steampowered.com/IPlayerService/GetOwnedGames/v0001/ # ?key=XXXXXXXXXXXXXXXXXXXXX&include_played_free_games=1&steamid=XXXXXXXXXXXXXXXX&format=json -Steam_OwnedGames_URL = ( +STEAM_SOURCE = ( "http://api.steampowered.com/IPlayerService/GetOwnedGames/v0001/?" + "key=" + steam_apikey @@ -45,75 +51,76 @@ + "&format=json" ) -if sleep_time < 60: - sleep_time_conversion = "seconds" - sleep_int = sleep_time -elif 60 <= sleep_time < 3600: - sleep_int = sleep_time / 60 - sleep_time_conversion = "minutes" -elif 3600 <= sleep_time < 86400: - sleep_int = sleep_time / 60 / 60 - sleep_time_conversion = "hours" -else: - sleep_int = sleep_time / 60 / 60 / 24 - sleep_time_conversion = "days" - -# Connect to Wi-Fi -print("\n===============================") -print("Connecting to WiFi...") -requests = adafruit_requests.Session(pool, ssl.create_default_context()) -while not wifi.radio.ipv4_address: - try: - wifi.radio.connect(ssid, password) - except ConnectionError as e: - print("Connection Error:", e) - print("Retrying in 10 seconds") - time.sleep(10) - gc.collect() -print("Connected!\n") + +def time_calc(input_time): + """Converts seconds to minutes/hours/days""" + if input_time < 60: + return f"{input_time:.0f} seconds" + if input_time < 3600: + return f"{input_time / 60:.0f} minutes" + if input_time < 86400: + return f"{input_time / 60 / 60:.0f} hours" + return f"{input_time / 60 / 60 / 24:.1f} days" + + +def _format_datetime(datetime): + """F-String formatted struct time conversion""" + return ( + f"{datetime.tm_mon:02}/" + + f"{datetime.tm_mday:02}/" + + f"{datetime.tm_year:02} " + + f"{datetime.tm_hour:02}:" + + f"{datetime.tm_min:02}:" + + f"{datetime.tm_sec:02}" + ) + while True: + # Connect to Wi-Fi + print("\nConnecting to WiFi...") + while not wifi.radio.ipv4_address: + try: + wifi.radio.connect(ssid, password) + except ConnectionError as e: + print("❌ Connection Error:", e) + print("Retrying in 10 seconds") + print("✅ Wifi!") + try: - print("\nAttempting to GET STEAM Stats!") # -------------------------------- - # Print Request to Serial - debug_request = False # Set true to see full request - if debug_request: - print("Full API GET URL: ", Steam_OwnedGames_URL) - print("===============================") + print(" | Attempting to GET Steam API JSON!") try: - steam_response = requests.get(url=Steam_OwnedGames_URL).json() + with requests.get(url=STEAM_SOURCE) as steam_response: + steam_json = steam_response.json() except ConnectionError as e: print("Connection Error:", e) print("Retrying in 10 seconds") - # Print Response to Serial - debug_response = False # Set true to see full response - if debug_response: - dump_object = json.dumps(steam_response) - print("JSON Dump: ", dump_object) - - # Print Keys to Serial - steam_debug_keys = True # Set True to print Serial data - if steam_debug_keys: - game_count = steam_response["response"]["game_count"] - print("Total Games: ", game_count) - total_minutes = 0 - - for game in steam_response["response"]["games"]: - total_minutes += game["playtime_forever"] - total_hours = total_minutes / 60 - total_days = total_minutes / 60 / 24 - print(f"Total Hours: {total_hours}") - print(f"Total Days: {total_days}") - - print("Monotonic: ", time.monotonic()) + print(" | ✅ Steam JSON!") + + if DEBUG: + print("Full API GET URL: ", STEAM_SOURCE) + print(steam_json) + + game_count = steam_json["response"]["game_count"] + print(f" | | Total Games: {game_count}") + TOTAL_MINUTES = 0 + + for game in steam_json["response"]["games"]: + TOTAL_MINUTES += game["playtime_forever"] + total_hours = TOTAL_MINUTES / 60 + total_days = TOTAL_MINUTES / 60 / 24 + total_years = TOTAL_MINUTES / 60 / 24 / 365 + print(f" | | Total Hours: {total_hours}") + print(f" | | Total Days: {total_days}") + print(f" | | Total Years: {total_years:.2f}") + print("\nFinished!") - print("Next Update in %s %s" % (int(sleep_int), sleep_time_conversion)) + print(f"Board Uptime: {time_calc(time.monotonic())}") + print(f"Next Update: {time_calc(SLEEP_TIME)}") print("===============================") - gc.collect() except (ValueError, RuntimeError) as e: - print("Failed to get data, retrying\n", e) + print(f"Failed to get data, retrying\n {e}") time.sleep(60) - continue - time.sleep(sleep_time) + break + time.sleep(SLEEP_TIME) diff --git a/examples/wifi/expanded/requests_wifi_api_twitch.py b/examples/wifi/expanded/requests_wifi_api_twitch.py index 716caa9..15cbcc0 100644 --- a/examples/wifi/expanded/requests_wifi_api_twitch.py +++ b/examples/wifi/expanded/requests_wifi_api_twitch.py @@ -1,44 +1,48 @@ -# SPDX-FileCopyrightText: 2023 DJDevon3 +# SPDX-FileCopyrightText: 2024 DJDevon3 # SPDX-License-Identifier: MIT # Coded for Circuit Python 8.2.x -# Twitch_API_Example +"""Twitch API Example""" import os -import ssl import time -import socketpool +import adafruit_connection_manager import wifi import adafruit_requests -# Initialize WiFi Pool (There can be only 1 pool & top of script) -pool = socketpool.SocketPool(wifi.radio) - # Twitch Developer Account & oauth App Required: # Visit https://dev.twitch.tv/console to create an app - -# Ensure these are in secrets.py or settings.toml -# "Twitch_ClientID": "Your Developer APP ID Here", -# "Twitch_Client_Secret": "APP ID secret here", -# "Twitch_UserID": "Your Twitch UserID here", +# Ensure these are in settings.toml +# TWITCH_CLIENT_ID = "Your Developer APP ID Here" +# TWITCH_CLIENT_SECRET = "APP ID secret here" +# TWITCH_USER_ID = "Your Twitch UserID here" # Get WiFi details, ensure these are setup in settings.toml ssid = os.getenv("CIRCUITPY_WIFI_SSID") password = os.getenv("CIRCUITPY_WIFI_PASSWORD") -twitch_client_id = os.getenv("Twitch_ClientID") -twitch_client_secret = os.getenv("Twitch_Client_Secret") +TWITCH_CID = os.getenv("TWITCH_CLIENT_ID") +TWITCH_CS = os.getenv("TWITCH_CLIENT_SECRET") # For finding your Twitch User ID # https://www.streamweasels.com/tools/convert-twitch-username-to-user-id/ -twitch_user_id = os.getenv("Twitch_UserID") # User ID you want endpoints from +TWITCH_UID = os.getenv("TWITCH_USER_ID") -# Time between API refreshes +# API Polling Rate # 900 = 15 mins, 1800 = 30 mins, 3600 = 1 hour -sleep_time = 900 +SLEEP_TIME = 900 + +# Set DEBUG to True for full JSON response. +# STREAMER WARNING: Credentials will be viewable +DEBUG = False + +# Initalize Wifi, Socket Pool, Request Session +pool = adafruit_connection_manager.get_radio_socketpool(wifi.radio) +ssl_context = adafruit_connection_manager.get_radio_ssl_context(wifi.radio) +requests = adafruit_requests.Session(pool, ssl_context) -# Converts seconds to minutes/hours/days def time_calc(input_time): + """Converts seconds to minutes/hours/days""" if input_time < 60: return f"{input_time:.0f} seconds" if input_time < 3600: @@ -48,117 +52,124 @@ def time_calc(input_time): return f"{input_time / 60 / 60 / 24:.1f} days" +def _format_datetime(datetime): + """F-String formatted struct time conversion""" + return ( + f"{datetime.tm_mon:02}/" + + f"{datetime.tm_mday:02}/" + + f"{datetime.tm_year:02} " + + f"{datetime.tm_hour:02}:" + + f"{datetime.tm_min:02}:" + + f"{datetime.tm_sec:02}" + ) + + # First we use Client ID & Client Secret to create a token with POST # No user interaction is required for this type of scope (implicit grant flow) twitch_0auth_header = {"Content-Type": "application/x-www-form-urlencoded"} TWITCH_0AUTH_TOKEN = "https://id.twitch.tv/oauth2/token" -# Connect to Wi-Fi -print("\n===============================") -print("Connecting to WiFi...") -requests = adafruit_requests.Session(pool, ssl.create_default_context()) -while not wifi.radio.connected: - try: - wifi.radio.connect(ssid, password) - except ConnectionError as e: - print("Connection Error:", e) - print("Retrying in 10 seconds") - time.sleep(10) -print("Connected!\n") - while True: + # Connect to Wi-Fi + print("\nConnecting to WiFi...") + while not wifi.radio.ipv4_address: + try: + wifi.radio.connect(ssid, password) + except ConnectionError as e: + print("❌ Connection Error:", e) + print("Retrying in 10 seconds") + print("✅ Wifi!") + try: - # ----------------------------- POST FOR BEARER TOKEN ----------------------- - print( - "Attempting Bearer Token Request!" - ) # --------------------------------------- - # Print Request to Serial - debug_bearer_request = ( - False # STREAMER WARNING: your client secret will be viewable - ) - if debug_bearer_request: - print("Full API GET URL: ", TWITCH_0AUTH_TOKEN) - print("===============================") + # ------------- POST FOR BEARER TOKEN ----------------- + print(" | Attempting Bearer Token Request!") + if DEBUG: + print(f"Full API GET URL: {TWITCH_0AUTH_TOKEN}") twitch_0auth_data = ( "&client_id=" - + twitch_client_id + + TWITCH_CID + "&client_secret=" - + twitch_client_secret + + TWITCH_CS + "&grant_type=client_credentials" ) # POST REQUEST - twitch_0auth_response = requests.post( - url=TWITCH_0AUTH_TOKEN, data=twitch_0auth_data, headers=twitch_0auth_header - ) try: - twitch_0auth_json = twitch_0auth_response.json() - twitch_access_token = twitch_0auth_json["access_token"] + with requests.post( + url=TWITCH_0AUTH_TOKEN, + data=twitch_0auth_data, + headers=twitch_0auth_header, + ) as twitch_0auth_response: + twitch_0auth_json = twitch_0auth_response.json() + twitch_access_token = twitch_0auth_json["access_token"] except ConnectionError as e: - print("Connection Error:", e) + print(f"Connection Error: {e}") print("Retrying in 10 seconds") + print(" | 🔑 Token Authorized!") - # Print Response to Serial - debug_bearer_response = ( - False # STREAMER WARNING: your client secret will be viewable - ) - if debug_bearer_response: - print("JSON Dump: ", twitch_0auth_json) - print("Header: ", twitch_0auth_header) - print("Access Token: ", twitch_access_token) + # STREAMER WARNING: your client secret will be viewable + if DEBUG: + print(f"JSON Dump: {twitch_0auth_json}") + print(f"Header: {twitch_0auth_header}") + print(f"Access Token: {twitch_access_token}") twitch_token_type = twitch_0auth_json["token_type"] - print("Token Type: ", twitch_token_type) + print(f"Token Type: {twitch_token_type}") - print("Board Uptime: ", time_calc(time.monotonic())) twitch_token_expiration = twitch_0auth_json["expires_in"] - print("Token Expires in: ", time_calc(twitch_token_expiration)) + print(f" | Token Expires in: {time_calc(twitch_token_expiration)}") - # ----------------------------- GET DATA ------------------------------------- + # ----------------------------- GET DATA -------------------- # Bearer token is refreshed every time script runs :) # Twitch sets token expiration to about 64 days # Helix is the name of the current Twitch API # Now that we have POST bearer token we can do a GET for data - # ---------------------------------------------------------------------------- + # ----------------------------------------------------------- twitch_header = { "Authorization": "Bearer " + twitch_access_token + "", - "Client-Id": "" + twitch_client_id + "", + "Client-Id": "" + TWITCH_CID + "", } TWITCH_FOLLOWERS_SOURCE = ( - "https://api.twitch.tv/helix/channels" - + "/followers?" - + "broadcaster_id=" - + twitch_user_id - ) - print( - "\nAttempting to GET TWITCH Stats!" - ) # ------------------------------------------------ - print("===============================") - twitch_followers_response = requests.get( - url=TWITCH_FOLLOWERS_SOURCE, headers=twitch_header + "https://api.twitch.tv/helix/channels" + "/followers?" + "broadcaster_id=" + TWITCH_UID ) + print(" | Attempting to GET Twitch JSON!") try: - twitch_followers_json = twitch_followers_response.json() + with requests.get( + url=TWITCH_FOLLOWERS_SOURCE, headers=twitch_header + ) as twitch_response: + twitch_json = twitch_response.json() except ConnectionError as e: - print("Connection Error:", e) + print(f"Connection Error: {e}") print("Retrying in 10 seconds") - # Print Response to Serial - debug_bearer_response = ( - False # STREAMER WARNING: your bearer token will be viewable - ) - if debug_bearer_response: - print("Full API GET URL: ", TWITCH_FOLLOWERS_SOURCE) - print("Header: ", twitch_header) - print("JSON Full Response: ", twitch_followers_json) - - twitch_followers = twitch_followers_json["total"] - print("Followers: ", twitch_followers) - print("Finished!") - print("Next Update in: ", time_calc(sleep_time)) + if DEBUG: + print(f" | Full API GET URL: {TWITCH_FOLLOWERS_SOURCE}") + print(f" | Header: {twitch_header}") + print(f" | JSON Full Response: {twitch_json}") + + if "status" in twitch_json: + twitch_error_status = twitch_json["status"] + print(f"❌ Status: {twitch_error_status}") + + if "error" in twitch_json: + twitch_error = twitch_json["error"] + print(f"❌ Error: {twitch_error}") + + if "message" in twitch_json: + twitch_error_msg = twitch_json["message"] + print(f"❌ Message: {twitch_error_msg}") + + if "total" in twitch_json: + print(" | ✅ Twitch JSON!") + twitch_followers = twitch_json["total"] + print(f" | | Followers: {twitch_followers}") + + print("\nFinished!") + print(f"Board Uptime: {time_calc(time.monotonic())}") + print(f"Next Update: {time_calc(SLEEP_TIME)}") print("===============================") except (ValueError, RuntimeError) as e: - print("Failed to get data, retrying\n", e) + print(f"Failed to get data, retrying\n {e}") time.sleep(60) - continue - time.sleep(sleep_time) + break + time.sleep(SLEEP_TIME) diff --git a/examples/wifi/expanded/requests_wifi_api_twitter.py b/examples/wifi/expanded/requests_wifi_api_twitter.py index 4dcdfa6..e69de29 100644 --- a/examples/wifi/expanded/requests_wifi_api_twitter.py +++ b/examples/wifi/expanded/requests_wifi_api_twitter.py @@ -1,120 +0,0 @@ -# SPDX-FileCopyrightText: 2022 DJDevon3 for Adafruit Industries -# SPDX-License-Identifier: MIT -# Coded for Circuit Python 8.0 -"""DJDevon3 Adafruit Feather ESP32-S2 Twitter_API_Example""" -import gc -import json -import os -import ssl -import time - -import socketpool -import wifi - -import adafruit_requests - -# Twitter developer account bearer token required. -# Ensure these are uncommented and in secrets.py or .env -# "TW_userid": "Your Twitter user id", # numerical id not username -# "TW_bearer_token": "Your long API Bearer token", - -# Initialize WiFi Pool (There can be only 1 pool & top of script) -pool = socketpool.SocketPool(wifi.radio) - -# Time between API refreshes -# 900 = 15 mins, 1800 = 30 mins, 3600 = 1 hour -sleep_time = 900 - -# Get WiFi details, ensure these are setup in settings.toml -ssid = os.getenv("CIRCUITPY_WIFI_SSID") -password = os.getenv("CIRCUITPY_WIFI_PASSWORD") -tw_userid = os.getenv("TW_userid") -tw_bearer_token = os.getenv("TW_bearer_token") - -if sleep_time < 60: - sleep_time_conversion = "seconds" - sleep_int = sleep_time -elif 60 <= sleep_time < 3600: - sleep_int = sleep_time / 60 - sleep_time_conversion = "minutes" -elif 3600 <= sleep_time < 86400: - sleep_int = sleep_time / 60 / 60 - sleep_time_conversion = "hours" -else: - sleep_int = sleep_time / 60 / 60 / 24 - sleep_time_conversion = "days" - -# Used with any Twitter 0auth request. -twitter_header = {"Authorization": "Bearer " + tw_bearer_token} -TW_SOURCE = ( - "https://api.twitter.com/2/users/" - + tw_userid - + "?user.fields=public_metrics,created_at,pinned_tweet_id" - + "&expansions=pinned_tweet_id" - + "&tweet.fields=created_at,public_metrics,source,context_annotations,entities" -) - -# Connect to Wi-Fi -print("\n===============================") -print("Connecting to WiFi...") -requests = adafruit_requests.Session(pool, ssl.create_default_context()) -while not wifi.radio.ipv4_address: - try: - wifi.radio.connect(ssid, password) - except ConnectionError as e: - print("Connection Error:", e) - print("Retrying in 10 seconds") - time.sleep(10) - gc.collect() -print("Connected!\n") - -while True: - try: - print("\nAttempting to GET Twitter Stats!") # -------------------------------- - debug_request = False # Set true to see full request - if debug_request: - print("Full API GET URL: ", TW_SOURCE) - print("===============================") - try: - twitter_response = requests.get(url=TW_SOURCE, headers=twitter_header) - tw_json = twitter_response.json() - except ConnectionError as e: - print("Connection Error:", e) - print("Retrying in 10 seconds") - - # Print Full JSON to Serial - debug_response = False # Set true to see full response - if debug_response: - dump_object = json.dumps(tw_json) - print("JSON Dump: ", dump_object) - - # Print to Serial - tw_debug_keys = True # Set true to print Serial data - if tw_debug_keys: - tw_userid = tw_json["data"]["id"] - print("User ID: ", tw_userid) - - tw_username = tw_json["data"]["name"] - print("Name: ", tw_username) - - tw_join_date = tw_json["data"]["created_at"] - print("Member Since: ", tw_join_date) - - tw_tweets = tw_json["data"]["public_metrics"]["tweet_count"] - print("Tweets: ", tw_tweets) - - tw_followers = tw_json["data"]["public_metrics"]["followers_count"] - print("Followers: ", tw_followers) - - print("Monotonic: ", time.monotonic()) - - print("\nFinished!") - print("Next Update in %s %s" % (int(sleep_int), sleep_time_conversion)) - print("===============================") - gc.collect() - - except (ValueError, RuntimeError) as e: - print("Failed to get data, retrying\n", e) - time.sleep(60) - continue - time.sleep(sleep_time) diff --git a/examples/wifi/expanded/requests_wifi_api_youtube.py b/examples/wifi/expanded/requests_wifi_api_youtube.py index e9bc6a2..a616d99 100644 --- a/examples/wifi/expanded/requests_wifi_api_youtube.py +++ b/examples/wifi/expanded/requests_wifi_api_youtube.py @@ -1,123 +1,117 @@ -# SPDX-FileCopyrightText: 2022 DJDevon3 for Adafruit Industries +# SPDX-FileCopyrightText: 2024 DJDevon3 # SPDX-License-Identifier: MIT -# Coded for Circuit Python 8.0 -"""DJDevon3 Adafruit Feather ESP32-S2 YouTube_API_Example""" -import gc -import json +# Coded for Circuit Python 8.2.x +"""YouTube API Subscriber Count Example""" + import os -import ssl import time -import socketpool +import adafruit_connection_manager import wifi import adafruit_requests -# Ensure these are uncommented and in secrets.py or .env -# "YT_username": "Your YouTube Username", -# "YT_token" : "Your long API developer token", - -# Initialize WiFi Pool (There can be only 1 pool & top of script) -pool = socketpool.SocketPool(wifi.radio) +# Initalize Wifi, Socket Pool, Request Session +pool = adafruit_connection_manager.get_radio_socketpool(wifi.radio) +ssl_context = adafruit_connection_manager.get_radio_ssl_context(wifi.radio) +requests = adafruit_requests.Session(pool, ssl_context) -# Time between API refreshes +# API Polling Rate # 900 = 15 mins, 1800 = 30 mins, 3600 = 1 hour -sleep_time = 900 +SLEEP_TIME = 900 + +# Set debug to True for full JSON response. +# WARNING: Will show credentials +DEBUG = False + +# Ensure these are uncommented and in settings.toml +# YOUTUBE_USERNAME = "Your YouTube Username", +# YOUTUBE_TOKEN = "Your long API developer token", # Get WiFi details, ensure these are setup in settings.toml ssid = os.getenv("CIRCUITPY_WIFI_SSID") password = os.getenv("CIRCUITPY_WIFI_PASSWORD") -yt_username = os.getenv("YT_username") -yt_token = os.getenv("YT_token") - - -if sleep_time < 60: - sleep_time_conversion = "seconds" - sleep_int = sleep_time -elif 60 <= sleep_time < 3600: - sleep_int = sleep_time / 60 - sleep_time_conversion = "minutes" -elif 3600 <= sleep_time < 86400: - sleep_int = sleep_time / 60 / 60 - sleep_time_conversion = "hours" -else: - sleep_int = sleep_time / 60 / 60 / 24 - sleep_time_conversion = "days" +# Requires YouTube/Google API key +# https://console.cloud.google.com/apis/dashboard +YT_USERNAME = os.getenv("YOUTUBE_USERNAME") +YT_TOKEN = os.getenv("YOUTUBE_TOKEN") + + +def time_calc(input_time): + """Converts seconds to minutes/hours/days""" + if input_time < 60: + return f"{input_time:.0f} seconds" + if input_time < 3600: + return f"{input_time / 60:.0f} minutes" + if input_time < 86400: + return f"{input_time / 60 / 60:.0f} hours" + return f"{input_time / 60 / 60 / 24:.1f} days" + # https://youtube.googleapis.com/youtube/v3/channels?part=statistics&forUsername=[YOUR_USERNAME]&key=[YOUR_API_KEY] -YT_SOURCE = ( - "https://youtube.googleapis.com/youtube/v3/channels?" - + "part=statistics" - + "&forUsername=" - + yt_username +YOUTUBE_SOURCE = ( + "https://youtube.googleapis.com/youtube/v3/channels?part=statistics&forUsername=" + + str(YT_USERNAME) + "&key=" - + yt_token + + str(YT_TOKEN) ) -# Connect to Wi-Fi -print("\n===============================") -print("Connecting to WiFi...") -requests = adafruit_requests.Session(pool, ssl.create_default_context()) -while not wifi.radio.ipv4_address: - try: - wifi.radio.connect(ssid, password) - except ConnectionError as e: - print("Connection Error:", e) - print("Retrying in 10 seconds") - time.sleep(10) - gc.collect() -print("Connected!\n") - while True: + # Connect to Wi-Fi + print("\nConnecting to WiFi...") + while not wifi.radio.ipv4_address: + try: + wifi.radio.connect(ssid, password) + except ConnectionError as e: + print("❌ Connection Error:", e) + print("Retrying in 10 seconds") + print("✅ Wifi!") try: - print("Attempting to GET YouTube Stats!") # ---------------------------------- - debug_request = False # Set true to see full request - if debug_request: - print("Full API GET URL: ", YT_SOURCE) - print("===============================") + print(" | Attempting to GET YouTube JSON...") try: - response = requests.get(YT_SOURCE).json() + with requests.get(url=YOUTUBE_SOURCE) as youtube_response: + youtube_json = youtube_response.json() except ConnectionError as e: print("Connection Error:", e) print("Retrying in 10 seconds") + print(" | ✅ YouTube JSON!") - # Print Full JSON to Serial - debug_response = False # Set true to see full response - if debug_response: - dump_object = json.dumps(response) - print("JSON Dump: ", dump_object) + if DEBUG: + print(f" | Full API GET URL: {YOUTUBE_SOURCE}") + print(f" | Full API Dump: {youtube_json}") - # Print to Serial - yt_debug_keys = True # Set to True to print Serial data - if yt_debug_keys: - print("Matching Results: ", response["pageInfo"]["totalResults"]) + # Key:Value RESPONSES + if "pageInfo" in youtube_json: + totalResults = youtube_json["pageInfo"]["totalResults"] + print(f" | | Matching Results: {totalResults}") - YT_request_kind = response["items"][0]["kind"] - print("Request Kind: ", YT_request_kind) + if "items" in youtube_json: + YT_request_kind = youtube_json["items"][0]["kind"] + print(f" | | Request Kind: {YT_request_kind}") - YT_response_kind = response["kind"] - print("Response Kind: ", YT_response_kind) + YT_channel_id = youtube_json["items"][0]["id"] + print(f" | | Channel ID: {YT_channel_id}") - YT_channel_id = response["items"][0]["id"] - print("Channel ID: ", YT_channel_id) + YT_videoCount = youtube_json["items"][0]["statistics"]["videoCount"] + print(f" | | Videos: {YT_videoCount}") - YT_videoCount = response["items"][0]["statistics"]["videoCount"] - print("Videos: ", YT_videoCount) + YT_viewCount = youtube_json["items"][0]["statistics"]["viewCount"] + print(f" | | Views: {YT_viewCount}") - YT_viewCount = response["items"][0]["statistics"]["viewCount"] - print("Views: ", YT_viewCount) + YT_subsCount = youtube_json["items"][0]["statistics"]["subscriberCount"] + print(f" | | Subscribers: {YT_subsCount}") - YT_subsCount = response["items"][0]["statistics"]["subscriberCount"] - print("Subscribers: ", YT_subsCount) - print("Monotonic: ", time.monotonic()) + if "kind" in youtube_json: + YT_response_kind = youtube_json["kind"] + print(f" | | Response Kind: {YT_response_kind}") print("\nFinished!") - print("Next Update in %s %s" % (int(sleep_int), sleep_time_conversion)) + print(f"Board Uptime: {time_calc(time.monotonic())}") + print(f"Next Update: {time_calc(SLEEP_TIME)}") print("===============================") - gc.collect() except (ValueError, RuntimeError) as e: - print("Failed to get data, retrying\n", e) + print(f"Failed to get data, retrying\n {e}") time.sleep(60) - continue - time.sleep(sleep_time) + break + time.sleep(SLEEP_TIME) diff --git a/examples/wifi/expanded/requests_wifi_file_upload.py b/examples/wifi/expanded/requests_wifi_file_upload.py new file mode 100644 index 0000000..bd9ac2a --- /dev/null +++ b/examples/wifi/expanded/requests_wifi_file_upload.py @@ -0,0 +1,27 @@ +# SPDX-FileCopyrightText: 2024 Tim Cocks for Adafruit Industries +# SPDX-License-Identifier: MIT + +import adafruit_connection_manager +import wifi + +import adafruit_requests + +URL = "https://httpbin.org/post" + +pool = adafruit_connection_manager.get_radio_socketpool(wifi.radio) +ssl_context = adafruit_connection_manager.get_radio_ssl_context(wifi.radio) +requests = adafruit_requests.Session(pool, ssl_context) + +with open("requests_wifi_file_upload_image.png", "rb") as file_handle: + files = { + "file": ( + "requests_wifi_file_upload_image.png", + file_handle, + "image/png", + {"CustomHeader": "BlinkaRocks"}, + ), + "othervalue": (None, "HelloWorld"), + } + + with requests.post(URL, files=files) as response: + print(response.content) diff --git a/examples/wifi/expanded/requests_wifi_file_upload_image.png b/examples/wifi/expanded/requests_wifi_file_upload_image.png new file mode 100644 index 0000000..da2d219 Binary files /dev/null and b/examples/wifi/expanded/requests_wifi_file_upload_image.png differ diff --git a/examples/wifi/expanded/requests_wifi_file_upload_image.png.license b/examples/wifi/expanded/requests_wifi_file_upload_image.png.license new file mode 100644 index 0000000..6e0776a --- /dev/null +++ b/examples/wifi/expanded/requests_wifi_file_upload_image.png.license @@ -0,0 +1,2 @@ +# SPDX-FileCopyrightText: 2024 Tim Cocks +# SPDX-License-Identifier: CC-BY-4.0 diff --git a/examples/wifi/expanded/requests_wifi_multiple_cookies.py b/examples/wifi/expanded/requests_wifi_multiple_cookies.py index 36e4616..21fe9ba 100644 --- a/examples/wifi/expanded/requests_wifi_multiple_cookies.py +++ b/examples/wifi/expanded/requests_wifi_multiple_cookies.py @@ -1,58 +1,60 @@ # SPDX-FileCopyrightText: 2022 Alec Delaney # SPDX-License-Identifier: MIT - -""" -This example was written for the MagTag; changes may be needed -for connecting to the internet depending on your device. -""" +# Coded for Circuit Python 9.0 +"""Multiple Cookies Example written for MagTag""" import os -import ssl -import socketpool +import adafruit_connection_manager import wifi import adafruit_requests -COOKIE_TEST_URL = "https://www.adafruit.com/" - # Get WiFi details, ensure these are setup in settings.toml ssid = os.getenv("CIRCUITPY_WIFI_SSID") password = os.getenv("CIRCUITPY_WIFI_PASSWORD") -# Connect to the Wi-Fi network -print("Connecting to %s" % ssid) -wifi.radio.connect(ssid, password) - -# Set up the requests library -pool = socketpool.SocketPool(wifi.radio) -requests = adafruit_requests.Session(pool, ssl.create_default_context()) - -# GET from the URL -print("Fetching multiple cookies from", COOKIE_TEST_URL) -response = requests.get(COOKIE_TEST_URL) - -# Spilt up the cookies by ", " -elements = response.headers["set-cookie"].split(", ") - -# NOTE: Some cookies use ", " when describing dates. This code will iterate through -# the previously split up 'set-cookie' header value and piece back together cookies -# that were accidentally split up for this reason -days_of_week = ("Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat") -elements_iter = iter(elements) -cookie_list = [] -for element in elements_iter: - captured_day = [day for day in days_of_week if element.endswith(day)] - if captured_day: - cookie_list.append(element + ", " + next(elements_iter)) - else: - cookie_list.append(element) - -# Pring the information about the cookies -print("Number of cookies:", len(cookie_list)) -print("") -print("Cookies received:") -print("-" * 40) -for cookie in cookie_list: - print(cookie) - print("-" * 40) +COOKIE_TEST_URL = "https://www.adafruit.com/" + +# Initalize Wifi, Socket Pool, Request Session +pool = adafruit_connection_manager.get_radio_socketpool(wifi.radio) +ssl_context = adafruit_connection_manager.get_radio_ssl_context(wifi.radio) +requests = adafruit_requests.Session(pool, ssl_context) + +print(f"\nConnecting to {ssid}...") +try: + # Connect to the Wi-Fi network + wifi.radio.connect(ssid, password) +except OSError as e: + print(f"❌ OSError: {e}") +print("✅ Wifi!") + +# URL GET Request +with requests.get(COOKIE_TEST_URL) as response: + print(f" | Fetching Cookies: {COOKIE_TEST_URL}") + + # Spilt up the cookies by ", " + elements = response.headers["set-cookie"].split(", ") + + # NOTE: Some cookies use ", " when describing dates. This code will iterate through + # the previously split up 'set-cookie' header value and piece back together cookies + # that were accidentally split up for this reason + days_of_week = ("Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat") + elements_iter = iter(elements) + cookie_list = [] + for element in elements_iter: + captured_day = [day for day in days_of_week if element.endswith(day)] + if captured_day: + cookie_list.append(element + ", " + next(elements_iter)) + else: + cookie_list.append(element) + + # Pring the information about the cookies + print(f" | Total Cookies: {len(cookie_list)}") + print("-" * 80) + + for cookie in cookie_list: + print(f" | 🍪 {cookie}") + print("-" * 80) + +print("Finished!") diff --git a/examples/wifi/expanded/requests_wifi_rachio_irrigation.py b/examples/wifi/expanded/requests_wifi_rachio_irrigation.py new file mode 100644 index 0000000..55a46a1 --- /dev/null +++ b/examples/wifi/expanded/requests_wifi_rachio_irrigation.py @@ -0,0 +1,246 @@ +# SPDX-FileCopyrightText: 2024 DJDevon3 +# SPDX-License-Identifier: MIT +# Coded for Circuit Python 9.x +"""Rachio Irrigation Timer API Example""" + +import os +import time + +import adafruit_connection_manager +import wifi + +import adafruit_requests + +# Rachio API Key required (comes with purchase of a device) +# API is rate limited to 1700 calls per day. +# https://support.rachio.com/en_us/public-api-documentation-S1UydL1Fv +# https://rachio.readme.io/reference/getting-started +RACHIO_KEY = os.getenv("RACHIO_APIKEY") +RACHIO_PERSONID = os.getenv("RACHIO_PERSONID") + +# Get WiFi details, ensure these are setup in settings.toml +ssid = os.getenv("CIRCUITPY_WIFI_SSID") +password = os.getenv("CIRCUITPY_WIFI_PASSWORD") + +# API Polling Rate +# 900 = 15 mins, 1800 = 30 mins, 3600 = 1 hour +SLEEP_TIME = 900 + +# Set debug to True for full JSON response. +# WARNING: absolutely shows extremely sensitive personal information & credentials +# Including your real name, latitude, longitude, account id, mac address, etc... +DEBUG = False + +# Initalize Wifi, Socket Pool, Request Session +pool = adafruit_connection_manager.get_radio_socketpool(wifi.radio) +ssl_context = adafruit_connection_manager.get_radio_ssl_context(wifi.radio) +requests = adafruit_requests.Session(pool, ssl_context) + +RACHIO_HEADER = {"Authorization": " Bearer " + RACHIO_KEY} +RACHIO_SOURCE = "https://api.rach.io/1/public/person/info/" +RACHIO_PERSON_SOURCE = "https://api.rach.io/1/public/person/" + + +def obfuscating_asterix(obfuscate_object, direction, characters=2): + """ + Obfuscates a string with asterisks except for a specified number of characters. + param object: str The string to obfuscate with asterisks + param direction: str Option either 'prepend', 'append', or 'all' direction + param characters: int The number of characters to keep unobfuscated (default is 2) + """ + object_len = len(obfuscate_object) + if direction not in {"prepend", "append", "all"}: + raise ValueError("Invalid direction. Use 'prepend', 'append', or 'all'.") + if characters >= object_len and direction != "all": + # If characters greater than or equal to string length, + # return the original string as it can't be obfuscated. + return obfuscate_object + asterix_replace = "*" * object_len + if direction == "append": + asterix_final = obfuscate_object[:characters] + "*" * (object_len - characters) + elif direction == "prepend": + asterix_final = "*" * (object_len - characters) + obfuscate_object[-characters:] + elif direction == "all": + # Replace all characters with asterisks + asterix_final = asterix_replace + + return asterix_final + + +def time_calc(input_time): + """Converts seconds to minutes/hours/days""" + if input_time < 60: + return f"{input_time:.0f} seconds" + if input_time < 3600: + return f"{input_time / 60:.0f} minutes" + if input_time < 86400: + return f"{input_time / 60 / 60:.0f} hours" + return f"{input_time / 60 / 60 / 24:.1f} days" + + +def _format_datetime(datetime): + """F-String formatted struct time conversion""" + return ( + f"{datetime.tm_mon:02}/" + + f"{datetime.tm_mday:02}/" + + f"{datetime.tm_year:02} " + + f"{datetime.tm_hour:02}:" + + f"{datetime.tm_min:02}:" + + f"{datetime.tm_sec:02}" + ) + + +while True: + # Connect to Wi-Fi + print("\nConnecting to WiFi...") + while not wifi.radio.ipv4_address: + try: + wifi.radio.connect(ssid, password) + except ConnectionError as e: + print("❌ Connection Error:", e) + print("Retrying in 10 seconds") + print("✅ Wifi!") + + # RETREIVE PERSONID AND PASTE IT TO SETTINGS.TOML + if RACHIO_PERSONID is None or RACHIO_PERSONID == "": + try: + print(" | Attempting to GET Rachio Authorization") + try: + with requests.get(url=RACHIO_SOURCE, headers=RACHIO_HEADER) as rachio_response: + rachio_json = rachio_response.json() + except ConnectionError as e: + print("Connection Error:", e) + print("Retrying in 10 seconds") + print(" | ✅ Authorized") + + rachio_id = rachio_json["id"] + print("\nADD THIS 🔑 TO YOUR SETTINGS.TOML FILE!") + print(f'RACHIO_PERSONID = "{rachio_id}"') + + if DEBUG: + print("\nFull API GET URL: ", RACHIO_SOURCE) + print(rachio_json) + + except (ValueError, RuntimeError) as e: + print(f"Failed to GET data: {e}") + time.sleep(60) + break + print("\nThis script can only continue when a proper APIKey & PersonID is used.") + print("\nFinished!") + print("===============================") + time.sleep(SLEEP_TIME) + + # Main Script + if RACHIO_PERSONID is not None and RACHIO_PERSONID != "": + try: + print(" | Attempting to GET Rachio JSON") + try: + with requests.get( + url=RACHIO_PERSON_SOURCE + RACHIO_PERSONID, headers=RACHIO_HEADER + ) as rachio_response: + rachio_json = rachio_response.json() + except ConnectionError as e: + print("Connection Error:", e) + print("Retrying in 10 seconds") + print(" | ✅ Rachio JSON") + response_headers = rachio_response.headers + if DEBUG: + print(f"Response Headers: {response_headers}") + call_limit = int(response_headers["x-ratelimit-limit"]) + calls_remaining = int(response_headers["x-ratelimit-remaining"]) + calls_made_today = call_limit - calls_remaining + + print(" | | Headers:") + print(f" | | | Date: {response_headers['date']}") + print(f" | | | Maximum Daily Requests: {call_limit}") + print(f" | | | Today's Requests: {calls_made_today}") + print(f" | | | Remaining Requests: {calls_remaining}") + print(f" | | | Limit Reset: {response_headers['x-ratelimit-reset']}") + print(f" | | | Content Type: {response_headers['content-type']}") + + rachio_id = rachio_json["id"] + rachio_id_ast = obfuscating_asterix(rachio_id, "append", 3) + print(" | | PersonID: ", rachio_id_ast) + + rachio_username = rachio_json["username"] + rachio_username_ast = obfuscating_asterix(rachio_username, "append", 3) + print(" | | Username: ", rachio_username_ast) + + rachio_name = rachio_json["fullName"] + rachio_name_ast = obfuscating_asterix(rachio_name, "append", 3) + print(" | | Full Name: ", rachio_name_ast) + + rachio_deleted = rachio_json["deleted"] + if not rachio_deleted: + print(" | | Account Status: Active") + else: + print(" | | Account Status?: Deleted!") + + rachio_createdate = rachio_json["createDate"] + rachio_timezone_offset = rachio_json["devices"][0]["utcOffset"] + # Rachio Unix time is in milliseconds, convert to seconds + rachio_createdate_seconds = rachio_createdate // 1000 + rachio_timezone_offset_seconds = rachio_timezone_offset // 1000 + # Apply timezone offset in seconds + local_unix_time = rachio_createdate_seconds + rachio_timezone_offset_seconds + if DEBUG: + print(f" | | Unix Registration Date: {rachio_createdate}") + print(f" | | Unix Timezone Offset: {rachio_timezone_offset}") + current_struct_time = time.localtime(local_unix_time) + final_timestamp = f"{_format_datetime(current_struct_time)}" + print(f" | | Registration Date: {final_timestamp}") + + rachio_devices = rachio_json["devices"][0]["name"] + print(" | | Device: ", rachio_devices) + + rachio_model = rachio_json["devices"][0]["model"] + print(" | | | Model: ", rachio_model) + + rachio_serial = rachio_json["devices"][0]["serialNumber"] + rachio_serial_ast = obfuscating_asterix(rachio_serial, "append") + print(" | | | Serial Number: ", rachio_serial_ast) + + rachio_mac = rachio_json["devices"][0]["macAddress"] + rachio_mac_ast = obfuscating_asterix(rachio_mac, "append") + print(" | | | MAC Address: ", rachio_mac_ast) + + rachio_status = rachio_json["devices"][0]["status"] + print(" | | | Device Status: ", rachio_status) + + rachio_timezone = rachio_json["devices"][0]["timeZone"] + print(" | | | Time Zone: ", rachio_timezone) + + # Latitude & Longtitude are used for smart watering & rain delays + rachio_latitude = str(rachio_json["devices"][0]["latitude"]) + rachio_lat_ast = obfuscating_asterix(rachio_latitude, "all") + print(" | | | Latitude: ", rachio_lat_ast) + + rachio_longitude = str(rachio_json["devices"][0]["longitude"]) + rachio_long_ast = obfuscating_asterix(rachio_longitude, "all") + print(" | | | Longitude: ", rachio_long_ast) + + rachio_rainsensor = rachio_json["devices"][0]["rainSensorTripped"] + print(" | | | Rain Sensor: ", rachio_rainsensor) + + rachio_zone0 = rachio_json["devices"][0]["zones"][0]["name"] + rachio_zone1 = rachio_json["devices"][0]["zones"][1]["name"] + rachio_zone2 = rachio_json["devices"][0]["zones"][2]["name"] + rachio_zone3 = rachio_json["devices"][0]["zones"][3]["name"] + zones = f"{rachio_zone0}, {rachio_zone1}, {rachio_zone2}, {rachio_zone3}" + print(f" | | | Zones: {zones}") + + if DEBUG: + print(f"\nFull API GET URL: {RACHIO_PERSON_SOURCE+rachio_id}") + print(rachio_json) + + print("\nFinished!") + print(f"Board Uptime: {time_calc(time.monotonic())}") + print(f"Next Update: {time_calc(SLEEP_TIME)}") + print("===============================") + + except (ValueError, RuntimeError) as e: + print(f"Failed to get data, retrying\n {e}") + time.sleep(60) + break + + time.sleep(SLEEP_TIME) diff --git a/examples/wifi/expanded/requests_wifi_status_codes.py b/examples/wifi/expanded/requests_wifi_status_codes.py new file mode 100644 index 0000000..a933c28 --- /dev/null +++ b/examples/wifi/expanded/requests_wifi_status_codes.py @@ -0,0 +1,138 @@ +# SPDX-FileCopyrightText: 2024 DJDevon3 +# SPDX-License-Identifier: MIT +# Updated for Circuit Python 9.0 +# https://help.openai.com/en/articles/6825453-chatgpt-release-notes +# https://chat.openai.com/share/32ef0c5f-ac92-4d36-9d1e-0f91e0c4c574 +"""WiFi Status Codes Example""" + +import os +import time + +import adafruit_connection_manager +import wifi + +import adafruit_requests + +# Get WiFi details, ensure these are setup in settings.toml +ssid = os.getenv("CIRCUITPY_WIFI_SSID") +password = os.getenv("CIRCUITPY_WIFI_PASSWORD") + +# Initalize Wifi, Socket Pool, Request Session +pool = adafruit_connection_manager.get_radio_socketpool(wifi.radio) +ssl_context = adafruit_connection_manager.get_radio_ssl_context(wifi.radio) +requests = adafruit_requests.Session(pool, ssl_context) +rssi = wifi.radio.ap_info.rssi + + +def print_http_status(expected_code, actual_code, description): + """Returns HTTP status code and description""" + if "100" <= actual_code <= "103": + print(f" | ✅ Status Test Expected: {expected_code} Actual: {actual_code} - {description}") + elif "200" == actual_code: + print(f" | 🆗 Status Test Expected: {expected_code} Actual: {actual_code} - {description}") + elif "201" <= actual_code <= "299": + print(f" | ✅ Status Test Expected: {expected_code} Actual: {actual_code} - {description}") + elif "300" <= actual_code <= "600": + print(f" | ❌ Status Test Expected: {expected_code} Actual: {actual_code} - {description}") + else: + print( + f" | Unknown Response Status Expected: {expected_code} " + + f"Actual: {actual_code} - {description}" + ) + + +# All HTTP Status Codes +http_status_codes = { + "100": "Continue", + "101": "Switching Protocols", + "102": "Processing", + "103": "Early Hints", + "200": "OK", + "201": "Created", + "202": "Accepted", + "203": "Non-Authoritative Information", + "204": "No Content", + "205": "Reset Content", + "206": "Partial Content", + "207": "Multi-Status", + "208": "Already Reported", + "226": "IM Used", + "300": "Multiple Choices", + "301": "Moved Permanently", + "302": "Found", + "303": "See Other", + "304": "Not Modified", + "305": "Use Proxy", + "306": "Unused", + "307": "Temporary Redirect", + "308": "Permanent Redirect", + "400": "Bad Request", + "401": "Unauthorized", + "402": "Payment Required", + "403": "Forbidden", + "404": "Not Found", + "405": "Method Not Allowed", + "406": "Not Acceptable", + "407": "Proxy Authentication Required", + "408": "Request Timeout", + "409": "Conflict", + "410": "Gone", + "411": "Length Required", + "412": "Precondition Failed", + "413": "Payload Too Large", + "414": "URI Too Long", + "415": "Unsupported Media Type", + "416": "Range Not Satisfiable", + "417": "Expectation Failed", + "418": "I'm a teapot", + "421": "Misdirected Request", + "422": "Unprocessable Entity", + "423": "Locked", + "424": "Failed Dependency", + "425": "Too Early", + "426": "Upgrade Required", + "428": "Precondition Required", + "429": "Too Many Requests", + "431": "Request Header Fields Too Large", + "451": "Unavailable For Legal Reasons", + "500": "Internal Server Error", + "501": "Not Implemented", + "502": "Bad Gateway", + "503": "Service Unavailable", + "504": "Gateway Timeout", + "505": "HTTP Version Not Supported", + "506": "Variant Also Negotiates", + "507": "Insufficient Storage", + "508": "Loop Detected", + "510": "Not Extended", + "511": "Network Authentication Required", +} + +STATUS_TEST_URL = "https://httpbin.org/status/" + +print(f"\nConnecting to {ssid}...") +print(f"Signal Strength: {rssi}") +try: + # Connect to the Wi-Fi network + wifi.radio.connect(ssid, password) +except OSError as e: + print(f"❌ OSError: {e}") +print("✅ Wifi!") + + +print(f" | Status Code Test: {STATUS_TEST_URL}") +# Some return errors then confirm the error (that's a good thing) +# Demonstrates not all errors have the same behavior +# Some 300 level responses contain redirects that requests automatically follows +# By default the response object will contain the status code from the final +# response after all redirect, so it can differ from the expected status code. +for current_code in sorted(http_status_codes.keys(), key=int): + header_status_test_url = STATUS_TEST_URL + current_code + with requests.get(header_status_test_url) as response: + response_status_code = str(response.status_code) + SORT_STATUS_DESC = http_status_codes.get(response_status_code, "Unknown Status Code") + print_http_status(current_code, response_status_code, SORT_STATUS_DESC) + + # Rate limit ourselves a little to avoid strain on server + time.sleep(0.5) +print("Finished!") diff --git a/examples/wifi/requests_wifi_advanced.py b/examples/wifi/requests_wifi_advanced.py index 3bb0976..bc29df7 100644 --- a/examples/wifi/requests_wifi_advanced.py +++ b/examples/wifi/requests_wifi_advanced.py @@ -1,10 +1,12 @@ # SPDX-FileCopyrightText: 2021 ladyada for Adafruit Industries # SPDX-License-Identifier: MIT +# Updated for Circuit Python 9.0 + +"""WiFi Advanced Example""" import os -import ssl -import socketpool +import adafruit_connection_manager import wifi import adafruit_requests @@ -13,39 +15,40 @@ ssid = os.getenv("CIRCUITPY_WIFI_SSID") password = os.getenv("CIRCUITPY_WIFI_PASSWORD") -# Initialize WiFi Pool (There can be only 1 pool & top of script) -radio = wifi.radio -pool = socketpool.SocketPool(radio) - -print("Connecting to AP...") -while not wifi.radio.ipv4_address: - try: - wifi.radio.connect(ssid, password) - except ConnectionError as e: - print("could not connect to AP, retrying: ", e) -print("Connected to", str(radio.ap_info.ssid, "utf-8"), "\tRSSI:", radio.ap_info.rssi) - -# Initialize a requests session -ssl_context = ssl.create_default_context() +# Initalize Wifi, Socket Pool, Request Session +pool = adafruit_connection_manager.get_radio_socketpool(wifi.radio) +ssl_context = adafruit_connection_manager.get_radio_ssl_context(wifi.radio) requests = adafruit_requests.Session(pool, ssl_context) +rssi = wifi.radio.ap_info.rssi +# URL for GET request JSON_GET_URL = "https://httpbin.org/get" - # Define a custom header as a dict. headers = {"user-agent": "blinka/1.0.0"} -print("Fetching JSON data from %s..." % JSON_GET_URL) -response = requests.get(JSON_GET_URL, headers=headers) -print("-" * 60) +print(f"\nConnecting to {ssid}...") +print(f"Signal Strength: {rssi}") +try: + # Connect to the Wi-Fi network + wifi.radio.connect(ssid, password) +except OSError as e: + print(f"❌ OSError: {e}") +print("✅ Wifi!") -json_data = response.json() -headers = json_data["headers"] -print("Response's Custom User-Agent Header: {0}".format(headers["User-Agent"])) -print("-" * 60) - -# Read Response's HTTP status code -print("Response HTTP Status Code: ", response.status_code) -print("-" * 60) - -# Close, delete and collect the response data -response.close() +# Define a custom header as a dict. +headers = {"user-agent": "blinka/1.0.0"} +print(f" | Fetching URL {JSON_GET_URL}") + +# Use with statement for retreiving GET request data +with requests.get(JSON_GET_URL, headers=headers) as response: + json_data = response.json() + headers = json_data["headers"] + content_type = response.headers.get("content-type", "") + date = response.headers.get("date", "") + if response.status_code == 200: + print(f" | 🆗 Status Code: {response.status_code}") + else: + print(f" | ❌ Status Code: {response.status_code}") + print(f" | | Custom User-Agent Header: {headers['User-Agent']}") + print(f" | | Content-Type: {content_type}") + print(f" | | Response Timestamp: {date}") diff --git a/examples/wifi/requests_wifi_simpletest.py b/examples/wifi/requests_wifi_simpletest.py index 35b835a..ddc70a2 100644 --- a/examples/wifi/requests_wifi_simpletest.py +++ b/examples/wifi/requests_wifi_simpletest.py @@ -1,10 +1,11 @@ # SPDX-FileCopyrightText: 2021 ladyada for Adafruit Industries # SPDX-License-Identifier: MIT +# Updated for Circuit Python 9.0 +"""WiFi Simpletest""" import os -import ssl -import socketpool +import adafruit_connection_manager import wifi import adafruit_requests @@ -13,60 +14,49 @@ ssid = os.getenv("CIRCUITPY_WIFI_SSID") password = os.getenv("CIRCUITPY_WIFI_PASSWORD") -# Initialize WiFi Pool (There can be only 1 pool & top of script) -radio = wifi.radio -pool = socketpool.SocketPool(radio) - -print("Connecting to AP...") -while not wifi.radio.ipv4_address: - try: - wifi.radio.connect(ssid, password) - except ConnectionError as e: - print("could not connect to AP, retrying: ", e) -print("Connected to", str(radio.ap_info.ssid, "utf-8"), "\tRSSI:", radio.ap_info.rssi) - -# Initialize a requests session -ssl_context = ssl.create_default_context() -requests = adafruit_requests.Session(pool, ssl_context) - TEXT_URL = "http://wifitest.adafruit.com/testwifi/index.html" JSON_GET_URL = "https://httpbin.org/get" JSON_POST_URL = "https://httpbin.org/post" -print("Fetching text from %s" % TEXT_URL) -response = requests.get(TEXT_URL) -print("-" * 40) - -print("Text Response: ", response.text) -print("-" * 40) -response.close() - -print("Fetching JSON data from %s" % JSON_GET_URL) -response = requests.get(JSON_GET_URL) -print("-" * 40) - -print("JSON Response: ", response.json()) -print("-" * 40) -response.close() - -data = "31F" -print("POSTing data to {0}: {1}".format(JSON_POST_URL, data)) -response = requests.post(JSON_POST_URL, data=data) -print("-" * 40) - -json_resp = response.json() -# Parse out the 'data' key from json_resp dict. -print("Data received from server:", json_resp["data"]) -print("-" * 40) -response.close() - -json_data = {"Date": "July 25, 2019"} -print("POSTing data to {0}: {1}".format(JSON_POST_URL, json_data)) -response = requests.post(JSON_POST_URL, json=json_data) -print("-" * 40) - -json_resp = response.json() -# Parse out the 'json' key from json_resp dict. -print("JSON Data received from server:", json_resp["json"]) -print("-" * 40) -response.close() +# Initalize Wifi, Socket Pool, Request Session +pool = adafruit_connection_manager.get_radio_socketpool(wifi.radio) +ssl_context = adafruit_connection_manager.get_radio_ssl_context(wifi.radio) +requests = adafruit_requests.Session(pool, ssl_context) +rssi = wifi.radio.ap_info.rssi + +print(f"\nConnecting to {ssid}...") +print(f"Signal Strength: {rssi}") +try: + # Connect to the Wi-Fi network + wifi.radio.connect(ssid, password) +except OSError as e: + print(f"❌ OSError: {e}") +print("✅ Wifi!") + +print(f" | GET Text Test: {TEXT_URL}") +with requests.get(TEXT_URL) as response: + print(f" | ✅ GET Response: {response.text}") +print("-" * 80) + +print(f" | GET Full Response Test: {JSON_GET_URL}") +with requests.get(JSON_GET_URL) as response: + print(f" | ✅ Unparsed Full JSON Response: {response.json()}") +print("-" * 80) + +DATA = "This is an example of a JSON value" +print(f" | ✅ JSON 'value' POST Test: {JSON_POST_URL} {DATA}") +with requests.post(JSON_POST_URL, data=DATA) as response: + json_resp = response.json() + # Parse out the 'data' key from json_resp dict. + print(f" | ✅ JSON 'value' Response: {json_resp['data']}") +print("-" * 80) + +json_data = {"Date": "January 1, 1970"} +print(f" | ✅ JSON 'key':'value' POST Test: {JSON_POST_URL} {json_data}") +with requests.post(JSON_POST_URL, json=json_data) as response: + json_resp = response.json() + # Parse out the 'json' key from json_resp dict. + print(f" | ✅ JSON 'key':'value' Response: {json_resp['json']}") +print("-" * 80) + +print("Finished!") diff --git a/examples/wiznet5k/requests_wiznet5k_advanced.py b/examples/wiznet5k/requests_wiznet5k_advanced.py index a6d9909..1f068ee 100644 --- a/examples/wiznet5k/requests_wiznet5k_advanced.py +++ b/examples/wiznet5k/requests_wiznet5k_advanced.py @@ -2,7 +2,6 @@ # SPDX-License-Identifier: MIT import adafruit_connection_manager -import adafruit_wiznet5k.adafruit_wiznet5k_socket as pool import board import busio from adafruit_wiznet5k.adafruit_wiznet5k import WIZNET5K @@ -17,7 +16,8 @@ radio = WIZNET5K(spi_bus, cs) # Initialize a requests session -ssl_context = adafruit_connection_manager.create_fake_ssl_context(pool, radio) +pool = adafruit_connection_manager.get_radio_socketpool(radio) +ssl_context = adafruit_connection_manager.get_radio_ssl_context(radio) requests = adafruit_requests.Session(pool, ssl_context) JSON_GET_URL = "http://httpbin.org/get" @@ -26,17 +26,14 @@ headers = {"user-agent": "blinka/1.0.0"} print("Fetching JSON data from %s..." % JSON_GET_URL) -response = requests.get(JSON_GET_URL, headers=headers) -print("-" * 60) +with requests.get(JSON_GET_URL, headers=headers) as response: + print("-" * 60) -json_data = response.json() -headers = json_data["headers"] -print("Response's Custom User-Agent Header: {0}".format(headers["User-Agent"])) -print("-" * 60) + json_data = response.json() + headers = json_data["headers"] + print("Response's Custom User-Agent Header: {0}".format(headers["User-Agent"])) + print("-" * 60) -# Read Response's HTTP status code -print("Response HTTP Status Code: ", response.status_code) -print("-" * 60) - -# Close, delete and collect the response data -response.close() + # Read Response's HTTP status code + print("Response HTTP Status Code: ", response.status_code) + print("-" * 60) diff --git a/examples/wiznet5k/requests_wiznet5k_simpletest.py b/examples/wiznet5k/requests_wiznet5k_simpletest.py index 646107f..14c49ec 100644 --- a/examples/wiznet5k/requests_wiznet5k_simpletest.py +++ b/examples/wiznet5k/requests_wiznet5k_simpletest.py @@ -2,7 +2,6 @@ # SPDX-License-Identifier: MIT import adafruit_connection_manager -import adafruit_wiznet5k.adafruit_wiznet5k_socket as pool import board import busio from adafruit_wiznet5k.adafruit_wiznet5k import WIZNET5K @@ -17,7 +16,8 @@ radio = WIZNET5K(spi_bus, cs) # Initialize a requests session -ssl_context = adafruit_connection_manager.create_fake_ssl_context(pool, radio) +pool = adafruit_connection_manager.get_radio_socketpool(radio) +ssl_context = adafruit_connection_manager.get_radio_ssl_context(radio) requests = adafruit_requests.Session(pool, ssl_context) TEXT_URL = "http://wifitest.adafruit.com/testwifi/index.html" @@ -25,39 +25,31 @@ JSON_POST_URL = "http://httpbin.org/post" print("Fetching text from %s" % TEXT_URL) -response = requests.get(TEXT_URL) -print("-" * 40) - -print("Text Response: ", response.text) -print("-" * 40) -response.close() +with requests.get(TEXT_URL) as response: + print("-" * 40) + print("Text Response: ", response.text) + print("-" * 40) print("Fetching JSON data from %s" % JSON_GET_URL) -response = requests.get(JSON_GET_URL) -print("-" * 40) - -print("JSON Response: ", response.json()) -print("-" * 40) -response.close() +with requests.get(JSON_GET_URL) as response: + print("-" * 40) + print("JSON Response: ", response.json()) + print("-" * 40) data = "31F" -print("POSTing data to {0}: {1}".format(JSON_POST_URL, data)) -response = requests.post(JSON_POST_URL, data=data) -print("-" * 40) - -json_resp = response.json() -# Parse out the 'data' key from json_resp dict. -print("Data received from server:", json_resp["data"]) -print("-" * 40) -response.close() +print(f"POSTing data to {JSON_POST_URL}: {data}") +with requests.post(JSON_POST_URL, data=data) as response: + print("-" * 40) + json_resp = response.json() + # Parse out the 'data' key from json_resp dict. + print("Data received from server:", json_resp["data"]) + print("-" * 40) json_data = {"Date": "July 25, 2019"} -print("POSTing data to {0}: {1}".format(JSON_POST_URL, json_data)) -response = requests.post(JSON_POST_URL, json=json_data) -print("-" * 40) - -json_resp = response.json() -# Parse out the 'json' key from json_resp dict. -print("JSON Data received from server:", json_resp["json"]) -print("-" * 40) -response.close() +print(f"POSTing data to {JSON_POST_URL}: {json_data}") +with requests.post(JSON_POST_URL, json=json_data) as response: + print("-" * 40) + json_resp = response.json() + # Parse out the 'json' key from json_resp dict. + print("JSON Data received from server:", json_resp["json"]) + print("-" * 40) diff --git a/optional_requirements.txt b/optional_requirements.txt index d4e27c4..38e5c0c 100644 --- a/optional_requirements.txt +++ b/optional_requirements.txt @@ -1,3 +1,5 @@ # SPDX-FileCopyrightText: 2022 Alec Delaney, for Adafruit Industries # # SPDX-License-Identifier: Unlicense + +requests diff --git a/pyproject.toml b/pyproject.toml index d9ee51a..291a9cc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,6 +38,16 @@ classifiers = [ ] dynamic = ["dependencies", "optional-dependencies"] +[tool.ruff] +target-version = "py38" + +[tool.ruff.lint] +select = ["I", "PL", "UP"] +ignore = ["PLR2004", "UP030"] + +[tool.ruff.format] +line-ending = "lf" + [tool.setuptools] py-modules = ["adafruit_requests"] diff --git a/ruff.toml b/ruff.toml new file mode 100644 index 0000000..db37c83 --- /dev/null +++ b/ruff.toml @@ -0,0 +1,99 @@ +# SPDX-FileCopyrightText: 2024 Tim Cocks for Adafruit Industries +# +# SPDX-License-Identifier: MIT + +target-version = "py38" +line-length = 100 + +[lint] +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 + +] + +[format] +line-ending = "lf" diff --git a/tests/chunk_test.py b/tests/chunk_test.py index ec4606d..4be0e05 100644 --- a/tests/chunk_test.py +++ b/tests/chunk_test.py @@ -2,7 +2,7 @@ # # SPDX-License-Identifier: Unlicense -""" Chunk Tests """ +"""Chunk Tests""" from unittest import mock @@ -28,11 +28,7 @@ def _chunk(response, split, extra=b""): chunk_size = remaining new_i = i + chunk_size chunked += ( - hex(chunk_size)[2:].encode("ascii") - + extra - + b"\r\n" - + response[i:new_i] - + b"\r\n" + hex(chunk_size)[2:].encode("ascii") + extra + b"\r\n" + response[i:new_i] + b"\r\n" ) i = new_i # The final chunk is zero length. diff --git a/tests/chunked_redirect_test.py b/tests/chunked_redirect_test.py index 69f8b27..871dc21 100644 --- a/tests/chunked_redirect_test.py +++ b/tests/chunked_redirect_test.py @@ -2,7 +2,7 @@ # # SPDX-License-Identifier: Unlicense -""" Redirection Tests """ +"""Redirection Tests""" from unittest import mock @@ -27,9 +27,7 @@ b"370cmver1f290kjsnpar5ku2h9g/3llvt5u8njbvat22m9l19db1h4/1656191325000/109226138307867586192/*/" + FILE_REDIRECT ) -RELATIVE_ABSOLUTE_REDIRECT = ( - b"/pub/70cmver1f290kjsnpar5ku2h9g/" + RELATIVE_RELATIVE_REDIRECT -) +RELATIVE_ABSOLUTE_REDIRECT = b"/pub/70cmver1f290kjsnpar5ku2h9g/" + RELATIVE_RELATIVE_REDIRECT ABSOLUTE_ABSOLUTE_REDIRECT = ( b"https://doc-14-2g-sheets.googleusercontent.com/" + RELATIVE_ABSOLUTE_REDIRECT ) @@ -113,7 +111,7 @@ ) -class MocketRecvInto(mocket.Mocket): # pylint: disable=too-few-public-methods +class MocketRecvInto(mocket.Mocket): """A special Mocket to cap the number of bytes returned from recv_into()""" def __init__(self, response): @@ -137,9 +135,7 @@ def test_chunked_absolute_absolute_redirect(): chunk = _chunk(BODY_REDIRECT, len(BODY_REDIRECT)) chunk2 = _chunk(BODY, len(BODY)) - redirect = HEADERS_REDIRECT.replace( - b"NEW_LOCATION_HERE", ABSOLUTE_ABSOLUTE_REDIRECT - ) + redirect = HEADERS_REDIRECT.replace(b"NEW_LOCATION_HERE", ABSOLUTE_ABSOLUTE_REDIRECT) sock1 = MocketRecvInto(redirect + chunk) sock2 = mocket.Mocket(HEADERS + chunk2) pool.socket.side_effect = (sock1, sock2) @@ -148,9 +144,7 @@ def test_chunked_absolute_absolute_redirect(): response = requests_session.get("https://" + HOST + PATH) sock1.connect.assert_called_once_with((HOST, 443)) - sock2.connect.assert_called_once_with( - ("doc-14-2g-sheets.googleusercontent.com", 443) - ) + sock2.connect.assert_called_once_with(("doc-14-2g-sheets.googleusercontent.com", 443)) sock2.send.assert_has_calls([mock.call(RELATIVE_ABSOLUTE_REDIRECT[1:])]) assert response.text == str(BODY, "utf-8") @@ -162,9 +156,7 @@ def test_chunked_relative_absolute_redirect(): chunk = _chunk(BODY_REDIRECT, len(BODY_REDIRECT)) chunk2 = _chunk(BODY, len(BODY)) - redirect = HEADERS_REDIRECT.replace( - b"NEW_LOCATION_HERE", RELATIVE_ABSOLUTE_REDIRECT - ) + redirect = HEADERS_REDIRECT.replace(b"NEW_LOCATION_HERE", RELATIVE_ABSOLUTE_REDIRECT) sock1 = MocketRecvInto(redirect + chunk) sock2 = mocket.Mocket(HEADERS + chunk2) pool.socket.side_effect = (sock1, sock2) @@ -185,9 +177,7 @@ def test_chunked_relative_relative_redirect(): chunk = _chunk(BODY_REDIRECT, len(BODY_REDIRECT)) chunk2 = _chunk(BODY, len(BODY)) - redirect = HEADERS_REDIRECT.replace( - b"NEW_LOCATION_HERE", RELATIVE_RELATIVE_REDIRECT - ) + redirect = HEADERS_REDIRECT.replace(b"NEW_LOCATION_HERE", RELATIVE_RELATIVE_REDIRECT) sock1 = MocketRecvInto(redirect + chunk) sock2 = mocket.Mocket(HEADERS + chunk2) pool.socket.side_effect = (sock1, sock2) @@ -214,13 +204,9 @@ def test_chunked_relative_relative_redirect_backstep(): backstep = b"../" * remove_paths path_base_parts = PATH_BASE.split("/") # PATH_BASE starts with "/" so skip it and also remove from the count - path_base = ( - "/".join(path_base_parts[1 : len(path_base_parts) - remove_paths - 1]) + "/" - ) + path_base = "/".join(path_base_parts[1 : len(path_base_parts) - remove_paths - 1]) + "/" - redirect = HEADERS_REDIRECT.replace( - b"NEW_LOCATION_HERE", backstep + RELATIVE_RELATIVE_REDIRECT - ) + redirect = HEADERS_REDIRECT.replace(b"NEW_LOCATION_HERE", backstep + RELATIVE_RELATIVE_REDIRECT) sock1 = MocketRecvInto(redirect + chunk) sock2 = mocket.Mocket(HEADERS + chunk2) pool.socket.side_effect = (sock1, sock2) @@ -230,9 +216,7 @@ def test_chunked_relative_relative_redirect_backstep(): sock1.connect.assert_called_once_with((HOST, 443)) sock2.connect.assert_called_once_with(("docs.google.com", 443)) - sock2.send.assert_has_calls( - [mock.call(bytes(path_base, "utf-8") + RELATIVE_RELATIVE_REDIRECT)] - ) + sock2.send.assert_has_calls([mock.call(bytes(path_base, "utf-8") + RELATIVE_RELATIVE_REDIRECT)]) assert response.text == str(BODY, "utf-8") @@ -243,9 +227,7 @@ def test_chunked_allow_redirects_false(): chunk = _chunk(BODY_REDIRECT, len(BODY_REDIRECT)) chunk2 = _chunk(BODY, len(BODY)) - redirect = HEADERS_REDIRECT.replace( - b"NEW_LOCATION_HERE", ABSOLUTE_ABSOLUTE_REDIRECT - ) + redirect = HEADERS_REDIRECT.replace(b"NEW_LOCATION_HERE", ABSOLUTE_ABSOLUTE_REDIRECT) sock1 = MocketRecvInto(redirect + chunk) sock2 = mocket.Mocket(HEADERS + chunk2) pool.socket.side_effect = (sock1, sock2) diff --git a/tests/concurrent_test.py b/tests/concurrent_test.py index 58c2ade..d24e3c7 100644 --- a/tests/concurrent_test.py +++ b/tests/concurrent_test.py @@ -2,7 +2,7 @@ # # SPDX-License-Identifier: Unlicense -""" Concurrent Tests """ +"""Concurrent Tests""" import errno from unittest import mock diff --git a/tests/conftest.py b/tests/conftest.py index 94023cb..02aa1a6 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,7 +2,7 @@ # # SPDX-License-Identifier: Unlicense -""" PyTest Setup """ +"""PyTest Setup""" import adafruit_connection_manager import mocket @@ -28,9 +28,7 @@ def sock(): @pytest.fixture def pool(sock): pool = mocket.MocketPool() - pool.getaddrinfo.return_value = ( - (None, None, None, None, (mocket.MOCK_POOL_IP, 80)), - ) + pool.getaddrinfo.return_value = ((None, None, None, None, (mocket.MOCK_POOL_IP, 80)),) pool.socket.return_value = sock return pool diff --git a/tests/files/green_red.png b/tests/files/green_red.png new file mode 100644 index 0000000..532c956 Binary files /dev/null and b/tests/files/green_red.png differ diff --git a/tests/files/green_red.png.license b/tests/files/green_red.png.license new file mode 100644 index 0000000..d41b03e --- /dev/null +++ b/tests/files/green_red.png.license @@ -0,0 +1,2 @@ +# SPDX-FileCopyrightText: 2024 Justin Myers +# SPDX-License-Identifier: Unlicense diff --git a/tests/files/red_green.png b/tests/files/red_green.png new file mode 100644 index 0000000..9d3bdb6 Binary files /dev/null and b/tests/files/red_green.png differ diff --git a/tests/files/red_green.png.license b/tests/files/red_green.png.license new file mode 100644 index 0000000..d41b03e --- /dev/null +++ b/tests/files/red_green.png.license @@ -0,0 +1,2 @@ +# SPDX-FileCopyrightText: 2024 Justin Myers +# SPDX-License-Identifier: Unlicense diff --git a/tests/files_test.py b/tests/files_test.py new file mode 100644 index 0000000..c263cd3 --- /dev/null +++ b/tests/files_test.py @@ -0,0 +1,232 @@ +# SPDX-FileCopyrightText: 2024 Justin Myers +# +# SPDX-License-Identifier: Unlicense + +"""Post Files Tests""" +# pylint: disable=line-too-long + +import re +from unittest import mock + +import mocket +import pytest +import requests as python_requests + + +@pytest.fixture +def log_stream(): + return [] + + +@pytest.fixture +def post_url(): + return "https://httpbin.org/post" + + +@pytest.fixture +def request_logging(log_stream): + """Reset the ConnectionManager, since it's a singlton and will hold data""" + import http.client # pylint: disable=import-outside-toplevel + + def httpclient_log(*args): + log_stream.append(args) + + http.client.print = httpclient_log + http.client.HTTPConnection.debuglevel = 1 + + +def get_actual_request_data(log_stream): + boundary_pattern = r"(?<=boundary=)(.\w*)" + content_length_pattern = r"(?<=Content-Length: )(.\d*)" + + boundary = "" + actual_request_post = "" + content_length = "" + for log in log_stream: + for log_arg in log: + boundary_search = re.findall(boundary_pattern, log_arg) + content_length_search = re.findall(content_length_pattern, log_arg) + if boundary_search: + boundary = boundary_search[0] + if content_length_search: + content_length = content_length_search[0] + if "Content-Disposition" in log_arg or "\\x" in log_arg: + # this will look like: + # b\'{content}\' + # and escaped characters look like: + # \\r + post_data = log_arg[2:-1] + post_bytes = post_data.encode("utf-8") + post_unescaped = post_bytes.decode("unicode_escape") + actual_request_post = post_unescaped.encode("latin1") + + return boundary, content_length, actual_request_post + + +def test_post_file_as_data( # pylint: disable=unused-argument + requests, sock, log_stream, post_url, request_logging +): + with open("tests/files/red_green.png", "rb") as file_1: + python_requests.post(post_url, data=file_1, timeout=30) + __, content_length, actual_request_post = get_actual_request_data(log_stream) + + requests.post("http://" + mocket.MOCK_HOST_1 + "/post", data=file_1) + + sock.connect.assert_called_once_with((mocket.MOCK_POOL_IP, 80)) + sock.send.assert_has_calls( + [ + mock.call(b"Content-Length"), + mock.call(b": "), + mock.call(content_length.encode()), + mock.call(b"\r\n"), + ] + ) + sent = b"".join(sock.sent_data) + assert sent.endswith(actual_request_post) + + +def test_post_files_text( # pylint: disable=unused-argument + sock, requests, log_stream, post_url, request_logging +): + file_data = { + "key_4": (None, "Value 5"), + } + + python_requests.post(post_url, files=file_data, timeout=30) + boundary, content_length, actual_request_post = get_actual_request_data(log_stream) + + requests._build_boundary_string = mock.Mock(return_value=boundary) + requests.post("http://" + mocket.MOCK_HOST_1 + "/post", files=file_data) + + sock.connect.assert_called_once_with((mocket.MOCK_POOL_IP, 80)) + sock.send.assert_has_calls( + [ + mock.call(b"Content-Type"), + mock.call(b": "), + mock.call(f"multipart/form-data; boundary={boundary}".encode()), + mock.call(b"\r\n"), + ] + ) + sock.send.assert_has_calls( + [ + mock.call(b"Content-Length"), + mock.call(b": "), + mock.call(content_length.encode()), + mock.call(b"\r\n"), + ] + ) + + sent = b"".join(sock.sent_data) + assert sent.endswith(actual_request_post) + + +def test_post_files_file( # pylint: disable=unused-argument + sock, requests, log_stream, post_url, request_logging +): + with open("tests/files/red_green.png", "rb") as file_1: + file_data = { + "file_1": ( + "red_green.png", + file_1, + "image/png", + { + "Key_1": "Value 1", + "Key_2": "Value 2", + "Key_3": "Value 3", + }, + ), + } + + python_requests.post(post_url, files=file_data, timeout=30) + boundary, content_length, actual_request_post = get_actual_request_data(log_stream) + + requests._build_boundary_string = mock.Mock(return_value=boundary) + requests.post("http://" + mocket.MOCK_HOST_1 + "/post", files=file_data) + + sock.connect.assert_called_once_with((mocket.MOCK_POOL_IP, 80)) + sock.send.assert_has_calls( + [ + mock.call(b"Content-Type"), + mock.call(b": "), + mock.call(f"multipart/form-data; boundary={boundary}".encode()), + mock.call(b"\r\n"), + ] + ) + sock.send.assert_has_calls( + [ + mock.call(b"Content-Length"), + mock.call(b": "), + mock.call(content_length.encode()), + mock.call(b"\r\n"), + ] + ) + sent = b"".join(sock.sent_data) + assert sent.endswith(actual_request_post) + + +def test_post_files_complex( # pylint: disable=unused-argument + sock, requests, log_stream, post_url, request_logging +): + with open("tests/files/red_green.png", "rb") as file_1, open( + "tests/files/green_red.png", "rb" + ) as file_2: + file_data = { + "file_1": ( + "red_green.png", + file_1, + "image/png", + { + "Key_1": "Value 1", + "Key_2": "Value 2", + "Key_3": "Value 3", + }, + ), + "key_4": (None, "Value 5"), + "file_2": ( + "green_red.png", + file_2, + "image/png", + ), + "key_6": (None, "Value 6"), + } + + python_requests.post(post_url, files=file_data, timeout=30) + boundary, content_length, actual_request_post = get_actual_request_data(log_stream) + + requests._build_boundary_string = mock.Mock(return_value=boundary) + requests.post("http://" + mocket.MOCK_HOST_1 + "/post", files=file_data) + + sock.connect.assert_called_once_with((mocket.MOCK_POOL_IP, 80)) + sock.send.assert_has_calls( + [ + mock.call(b"Content-Type"), + mock.call(b": "), + mock.call(f"multipart/form-data; boundary={boundary}".encode()), + mock.call(b"\r\n"), + ] + ) + sock.send.assert_has_calls( + [ + mock.call(b"Content-Length"), + mock.call(b": "), + mock.call(content_length.encode()), + mock.call(b"\r\n"), + ] + ) + sent = b"".join(sock.sent_data) + assert sent.endswith(actual_request_post) + + +def test_post_files_not_binary(requests): + with open("tests/files/red_green.png") as file_1: + file_data = { + "file_1": ( + "red_green.png", + file_1, + "image/png", + ), + } + + with pytest.raises(ValueError) as context: + requests.post("http://" + mocket.MOCK_HOST_1 + "/post", files=file_data) + assert "Files must be opened in binary mode" in str(context) diff --git a/tests/header_test.py b/tests/header_test.py index 8bcb354..69c16c5 100644 --- a/tests/header_test.py +++ b/tests/header_test.py @@ -2,23 +2,21 @@ # # SPDX-License-Identifier: Unlicense -""" Header Tests """ +"""Header Tests""" import mocket import pytest def test_check_headers_not_dict(requests): - with pytest.raises(AttributeError) as context: + with pytest.raises(TypeError) as context: requests._check_headers("") - assert "headers must be in dict format" in str(context) + assert "Headers must be in dict format" in str(context) def test_check_headers_not_valid(requests): - with pytest.raises(AttributeError) as context: - requests._check_headers( - {"Good1": "a", "Good2": b"b", "Good3": None, "Bad1": True} - ) + with pytest.raises(TypeError) as context: + requests._check_headers({"Good1": "a", "Good2": b"b", "Good3": None, "Bad1": True}) assert "Header part (True) from Bad1 must be of type str or bytes" in str(context) diff --git a/tests/local_test_server.py b/tests/local_test_server.py new file mode 100644 index 0000000..73b52b3 --- /dev/null +++ b/tests/local_test_server.py @@ -0,0 +1,38 @@ +# SPDX-FileCopyrightText: 2025 Tim Cocks +# +# SPDX-License-Identifier: MIT +import json +from http.server import SimpleHTTPRequestHandler + + +class LocalTestServerHandler(SimpleHTTPRequestHandler): + def do_GET(self): + if self.path == "/get": + resp_body = json.dumps({"url": "http://localhost:5000/get"}).encode("utf-8") + self.send_response(200) + self.send_header("Content-type", "application/json") + self.send_header("Content-Length", str(len(resp_body))) + self.end_headers() + self.wfile.write(resp_body) + if self.path.startswith("/status"): + try: + requested_status = int(self.path.split("/")[2]) + except ValueError: + resp_body = json.dumps({"error": "requested status code must be int"}).encode( + "utf-8" + ) + self.send_response(400) + self.send_header("Content-type", "application/json") + self.send_header("Content-Length", str(len(resp_body))) + self.end_headers() + self.wfile.write(resp_body) + return + + if requested_status != 204: + self.send_response(requested_status) + self.send_header("Content-type", "text/html") + self.send_header("Content-Length", "0") + else: + self.send_response(requested_status) + self.send_header("Content-type", "text/html") + self.end_headers() diff --git a/tests/method_test.py b/tests/method_test.py index dac6d07..1f5d9cf 100644 --- a/tests/method_test.py +++ b/tests/method_test.py @@ -2,7 +2,7 @@ # # SPDX-License-Identifier: Unlicense -""" Post Tests """ +"""Post Tests""" from unittest import mock @@ -16,6 +16,7 @@ "DELETE", "GET", "HEAD", + "OPTIONS", "PATCH", "POST", "PUT", @@ -51,7 +52,10 @@ def test_post_string(sock, requests): def test_post_form(sock, requests): - data = {"Date": "July 25, 2019", "Time": "12:00"} + data = { + "Date": "July 25, 2019", + "Time": "12:00", + } requests.post("http://" + mocket.MOCK_HOST_1 + "/post", data=data) sock.connect.assert_called_once_with((mocket.MOCK_POOL_IP, 80)) sock.send.assert_has_calls( @@ -66,7 +70,10 @@ def test_post_form(sock, requests): def test_post_json(sock, requests): - json_data = {"Date": "July 25, 2019", "Time": "12:00"} + json_data = { + "Date": "July 25, 2019", + "Time": "12:00", + } requests.post("http://" + mocket.MOCK_HOST_1 + "/post", json=json_data) sock.connect.assert_called_once_with((mocket.MOCK_POOL_IP, 80)) sock.send.assert_has_calls( diff --git a/tests/mocket.py b/tests/mocket.py index 3155231..5541b49 100644 --- a/tests/mocket.py +++ b/tests/mocket.py @@ -3,7 +3,7 @@ # # SPDX-License-Identifier: Unlicense -""" Mock Socket """ +"""Mock Socket""" from unittest import mock @@ -13,25 +13,22 @@ MOCK_PATH_1 = "/testwifi/index.html" MOCK_ENDPOINT_1 = MOCK_HOST_1 + MOCK_PATH_1 MOCK_ENDPOINT_2 = MOCK_HOST_2 + MOCK_PATH_1 -MOCK_RESPONSE_TEXT = ( - b"This is a test of Adafruit WiFi!\r\nIf you can read this, its working :)" -) +MOCK_RESPONSE_TEXT = b"This is a test of Adafruit WiFi!\r\nIf you can read this, its working :)" MOCK_RESPONSE = b"HTTP/1.0 200 OK\r\nContent-Length: 70\r\n\r\n" + MOCK_RESPONSE_TEXT -class MocketPool: # pylint: disable=too-few-public-methods +class MocketPool: """Mock SocketPool""" SOCK_STREAM = 0 - # pylint: disable=unused-argument def __init__(self, radio=None): self.getaddrinfo = mock.Mock() self.getaddrinfo.return_value = ((None, None, None, None, (MOCK_POOL_IP, 80)),) self.socket = mock.Mock() -class Mocket: # pylint: disable=too-few-public-methods +class Mocket: """Mock Socket""" def __init__(self, response=MOCK_RESPONSE): @@ -78,19 +75,16 @@ def _recv_into(self, buf, nbytes=0): return read -class SSLContext: # pylint: disable=too-few-public-methods +class SSLContext: """Mock SSL Context""" def __init__(self): self.wrap_socket = mock.Mock(side_effect=self._wrap_socket) - def _wrap_socket( - self, sock, server_hostname=None - ): # pylint: disable=no-self-use,unused-argument + def _wrap_socket(self, sock, server_hostname=None): return sock -# pylint: disable=too-few-public-methods class MockRadio: class Radio: pass diff --git a/tests/parse_test.py b/tests/parse_test.py index d9e1a56..7cff9f5 100644 --- a/tests/parse_test.py +++ b/tests/parse_test.py @@ -2,7 +2,7 @@ # # SPDX-License-Identifier: Unlicense -""" Parse Tests """ +"""Parse Tests""" import json @@ -15,13 +15,9 @@ # Padding here tests the case where a header line is exactly 32 bytes buffered by # aligning the Content-Type header after it. HEADERS = ( - ( - "HTTP/1.0 200 OK\r\npadding: 000\r\n" - "Content-Type: application/json\r\nContent-Length: {}\r\n\r\n" - ) - .format(len(ENCODED)) - .encode("utf-8") -) + "HTTP/1.0 200 OK\r\npadding: 000\r\n" + f"Content-Type: application/json\r\nContent-Length: {len(ENCODED)}\r\n\r\n" +).encode() def test_json(pool): diff --git a/tests/protocol_test.py b/tests/protocol_test.py index 4fd7770..e02c462 100644 --- a/tests/protocol_test.py +++ b/tests/protocol_test.py @@ -2,7 +2,7 @@ # # SPDX-License-Identifier: Unlicense -""" Protocol Tests """ +"""Protocol Tests""" from unittest import mock @@ -11,7 +11,7 @@ def test_get_https_no_ssl(requests): - with pytest.raises(AttributeError): + with pytest.raises(ValueError): requests.get("https://" + mocket.MOCK_ENDPOINT_1) diff --git a/tests/real_call_test.py b/tests/real_call_test.py new file mode 100644 index 0000000..b982f00 --- /dev/null +++ b/tests/real_call_test.py @@ -0,0 +1,52 @@ +# SPDX-FileCopyrightText: 2024 Justin Myers +# +# SPDX-License-Identifier: Unlicense + +"""Real call Tests""" + +import socket +import socketserver +import ssl +import threading +import time + +import adafruit_connection_manager +import pytest +from local_test_server import LocalTestServerHandler + +import adafruit_requests + + +def test_gets(): + path_index = 0 + status_code_index = 1 + text_result_index = 2 + json_keys_index = 3 + cases = [ + ("get", 200, None, {"url": "http://localhost:5000/get"}), + ("status/200", 200, "", None), + ("status/204", 204, "", None), + ] + + with socketserver.TCPServer(("127.0.0.1", 5000), LocalTestServerHandler) as server: + server_thread = threading.Thread(target=server.serve_forever) + server_thread.daemon = True + server_thread.start() + + time.sleep(2) # Give the server some time to start + + for case in cases: + requests = adafruit_requests.Session(socket, ssl.create_default_context()) + with requests.get(f"http://127.0.0.1:5000/%7Bcase[path_index]%7D") as response: + assert response.status_code == case[status_code_index] + if case[text_result_index] is not None: + assert response.text == case[text_result_index] + if case[json_keys_index] is not None: + for key, value in case[json_keys_index].items(): + assert response.json()[key] == value + + adafruit_connection_manager.connection_manager_close_all(release_references=True) + + server.shutdown() + server.server_close() + time.sleep(2) diff --git a/tests/reuse_test.py b/tests/reuse_test.py index 8ef5f5d..0c87bf3 100644 --- a/tests/reuse_test.py +++ b/tests/reuse_test.py @@ -2,7 +2,7 @@ # # SPDX-License-Identifier: Unlicense -""" Reuse Tests """ +"""Reuse Tests""" from unittest import mock @@ -57,7 +57,14 @@ def test_get_twice(pool, requests_ssl): def test_get_twice_after_second(pool, requests_ssl): - sock = mocket.Mocket(mocket.MOCK_RESPONSE + mocket.MOCK_RESPONSE) + sock = mocket.Mocket( + b"H" + b"TTP/1.0 200 OK\r\nContent-Length: " + b"70\r\n\r\nHTTP/1.0 2" + b"H" + b"TTP/1.0 200 OK\r\nContent-Length: " + b"70\r\n\r\nHTTP/1.0 2" + ) pool.socket.return_value = sock response = requests_ssl.get("https://" + mocket.MOCK_ENDPOINT_1) @@ -99,7 +106,7 @@ def test_get_twice_after_second(pool, requests_ssl): pool.socket.assert_called_once() with pytest.raises(RuntimeError) as context: - result = response.text # pylint: disable=unused-variable + result = response.text # noqa: F841 Local variable not used assert "Newer Response closed this one. Use Responses immediately." in str(context) diff --git a/tox.ini b/tox.ini index 85530c9..099a9b7 100644 --- a/tox.ini +++ b/tox.ini @@ -10,6 +10,7 @@ envlist = py311 description = run tests deps = pytest==7.4.3 + requests commands = pytest [testenv:coverage] @@ -17,6 +18,7 @@ description = run coverage deps = pytest==7.4.3 pytest-cov==4.1.0 + requests package = editable commands = coverage run --source=. --omit=tests/* --branch {posargs} -m pytest