├── .gitattributes ├── .github ├── PULL_REQUEST_TEMPLATE │ └── adafruit_circuitpython_pr.md └── workflows │ ├── build.yml │ ├── failure-help-text.yml │ ├── release_gh.yml │ └── release_pypi.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .readthedocs.yaml ├── CODE_OF_CONDUCT.md ├── LICENSE ├── LICENSES ├── CC-BY-4.0.txt ├── MIT.txt └── Unlicense.txt ├── README.rst ├── README.rst.license ├── adafruit_requests.py ├── docs ├── _static │ ├── favicon.ico │ └── favicon.ico.license ├── api.rst ├── api.rst.license ├── conf.py ├── examples.rst ├── examples.rst.license ├── index.rst ├── index.rst.license └── requirements.txt ├── examples ├── cpython │ ├── requests_cpython_advanced.py │ └── requests_cpython_simpletest.py ├── esp32spi │ ├── requests_esp32spi_advanced.py │ └── requests_esp32spi_simpletest.py ├── fona │ ├── requests_fona_advanced.py │ └── requests_fona_simpletest.py ├── wifi │ ├── expanded │ │ ├── requests_wifi_adafruit_discord_active_online.py │ │ ├── requests_wifi_api_discord.py │ │ ├── requests_wifi_api_fitbit.py │ │ ├── requests_wifi_api_github.py │ │ ├── requests_wifi_api_mastodon.py │ │ ├── requests_wifi_api_openskynetwork_private.py │ │ ├── requests_wifi_api_openskynetwork_private_area.py │ │ ├── requests_wifi_api_openskynetwork_public.py │ │ ├── requests_wifi_api_premiereleague.py │ │ ├── requests_wifi_api_queuetimes.py │ │ ├── requests_wifi_api_rocketlaunch_live.py │ │ ├── requests_wifi_api_steam.py │ │ ├── requests_wifi_api_twitch.py │ │ ├── requests_wifi_api_twitter.py │ │ ├── requests_wifi_api_youtube.py │ │ ├── requests_wifi_file_upload.py │ │ ├── requests_wifi_file_upload_image.png │ │ ├── requests_wifi_file_upload_image.png.license │ │ ├── requests_wifi_multiple_cookies.py │ │ ├── requests_wifi_rachio_irrigation.py │ │ └── requests_wifi_status_codes.py │ ├── requests_wifi_advanced.py │ └── requests_wifi_simpletest.py └── wiznet5k │ ├── requests_wiznet5k_advanced.py │ └── requests_wiznet5k_simpletest.py ├── optional_requirements.txt ├── pyproject.toml ├── requirements.txt ├── ruff.toml ├── tests ├── chunk_test.py ├── chunked_redirect_test.py ├── concurrent_test.py ├── conftest.py ├── files │ ├── green_red.png │ ├── green_red.png.license │ ├── red_green.png │ └── red_green.png.license ├── files_test.py ├── header_test.py ├── local_test_server.py ├── method_test.py ├── mocket.py ├── parse_test.py ├── protocol_test.py ├── real_call_test.py └── reuse_test.py └── tox.ini /.gitattributes: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Justin Myers for Adafruit Industries 2 | # 3 | # SPDX-License-Identifier: Unlicense 4 | 5 | * text eol=lf 6 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE/adafruit_circuitpython_pr.md: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 Adafruit Industries 2 | # 3 | # SPDX-License-Identifier: MIT 4 | 5 | Thank you for contributing! Before you submit a pull request, please read the following. 6 | 7 | Make sure any changes you're submitting are in line with the CircuitPython Design Guide, available here: https://docs.circuitpython.org/en/latest/docs/design_guide.html 8 | 9 | If your changes are to documentation, please verify that the documentation builds locally by following the steps found here: https://adafru.it/build-docs 10 | 11 | Before submitting the pull request, make sure you've run Pylint and Black locally on your code. You can do this manually or using pre-commit. Instructions are available here: https://adafru.it/check-your-code 12 | 13 | Please remove all of this text before submitting. Include an explanation or list of changes included in your PR, as well as, if applicable, a link to any related issues. 14 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries 2 | # 3 | # SPDX-License-Identifier: MIT 4 | 5 | name: Build CI 6 | 7 | on: [pull_request, push] 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Run Build CI workflow 14 | uses: adafruit/workflows-circuitpython-libs/build@main 15 | -------------------------------------------------------------------------------- /.github/workflows/failure-help-text.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 Scott Shawcroft for Adafruit Industries 2 | # 3 | # SPDX-License-Identifier: MIT 4 | 5 | name: Failure help text 6 | 7 | on: 8 | workflow_run: 9 | workflows: ["Build CI"] 10 | types: 11 | - completed 12 | 13 | jobs: 14 | post-help: 15 | runs-on: ubuntu-latest 16 | if: ${{ github.event.workflow_run.conclusion == 'failure' && github.event.workflow_run.event == 'pull_request' }} 17 | steps: 18 | - name: Post comment to help 19 | uses: adafruit/circuitpython-action-library-ci-failed@v1 20 | -------------------------------------------------------------------------------- /.github/workflows/release_gh.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries 2 | # 3 | # SPDX-License-Identifier: MIT 4 | 5 | name: GitHub Release Actions 6 | 7 | on: 8 | release: 9 | types: [published] 10 | 11 | jobs: 12 | upload-release-assets: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Run GitHub Release CI workflow 16 | uses: adafruit/workflows-circuitpython-libs/release-gh@main 17 | with: 18 | github-token: ${{ secrets.GITHUB_TOKEN }} 19 | upload-url: ${{ github.event.release.upload_url }} 20 | -------------------------------------------------------------------------------- /.github/workflows/release_pypi.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries 2 | # 3 | # SPDX-License-Identifier: MIT 4 | 5 | name: PyPI Release Actions 6 | 7 | on: 8 | release: 9 | types: [published] 10 | 11 | jobs: 12 | upload-release-assets: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Run PyPI Release CI workflow 16 | uses: adafruit/workflows-circuitpython-libs/release-pypi@main 17 | with: 18 | pypi-username: ${{ secrets.pypi_username }} 19 | pypi-password: ${{ secrets.pypi_password }} 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022 Kattni Rembor, written for Adafruit Industries 2 | # 3 | # SPDX-License-Identifier: MIT 4 | 5 | # Do not include files and directories created by your personal work environment, such as the IDE 6 | # you use, except for those already listed here. Pull requests including changes to this file will 7 | # not be accepted. 8 | 9 | # This .gitignore file contains rules for files generated by working with CircuitPython libraries, 10 | # including building Sphinx, testing with pip, and creating a virual environment, as well as the 11 | # MacOS and IDE-specific files generated by using MacOS in general, or the PyCharm or VSCode IDEs. 12 | 13 | # If you find that there are files being generated on your machine that should not be included in 14 | # your git commit, you should create a .gitignore_global file on your computer to include the 15 | # files created by your personal setup. To do so, follow the two steps below. 16 | 17 | # First, create a file called .gitignore_global somewhere convenient for you, and add rules for 18 | # the files you want to exclude from git commits. 19 | 20 | # Second, configure Git to use the exclude file for all Git repositories by running the 21 | # following via commandline, replacing "path/to/your/" with the actual path to your newly created 22 | # .gitignore_global file: 23 | # git config --global core.excludesfile path/to/your/.gitignore_global 24 | 25 | # CircuitPython-specific files 26 | *.mpy 27 | 28 | # Python-specific files 29 | __pycache__ 30 | *.pyc 31 | 32 | # Sphinx build-specific files 33 | _build 34 | 35 | # This file results from running `pip -e install .` in a local repository 36 | *.egg-info 37 | 38 | # Virtual environment-specific files 39 | .env 40 | .venv 41 | 42 | # MacOS-specific files 43 | *.DS_Store 44 | 45 | # IDE-specific files 46 | .idea 47 | .vscode 48 | *~ 49 | 50 | # tox-specific files 51 | .tox 52 | build 53 | 54 | # coverage-specific files 55 | .coverage 56 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2020 Diego Elio Pettenò 2 | # SPDX-FileCopyrightText: 2024 Justin Myers 3 | # 4 | # SPDX-License-Identifier: Unlicense 5 | 6 | repos: 7 | - repo: https://github.com/pre-commit/pre-commit-hooks 8 | rev: v4.5.0 9 | hooks: 10 | - id: check-yaml 11 | - id: end-of-file-fixer 12 | - id: trailing-whitespace 13 | - repo: https://github.com/astral-sh/ruff-pre-commit 14 | rev: v0.3.4 15 | hooks: 16 | - id: ruff-format 17 | - id: ruff 18 | args: ["--fix"] 19 | - repo: https://github.com/fsfe/reuse-tool 20 | rev: v3.0.1 21 | hooks: 22 | - id: reuse 23 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 ladyada for Adafruit Industries 2 | # 3 | # SPDX-License-Identifier: Unlicense 4 | 5 | # Read the Docs configuration file 6 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 7 | 8 | # Required 9 | version: 2 10 | 11 | sphinx: 12 | configuration: docs/conf.py 13 | 14 | build: 15 | os: ubuntu-20.04 16 | tools: 17 | python: "3" 18 | 19 | python: 20 | install: 21 | - requirements: docs/requirements.txt 22 | - requirements: requirements.txt 23 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | # Adafruit Community Code of Conduct 8 | 9 | ## Our Pledge 10 | 11 | In the interest of fostering an open and welcoming environment, we as 12 | contributors and leaders pledge to making participation in our project and 13 | our community a harassment-free experience for everyone, regardless of age, body 14 | size, disability, ethnicity, gender identity and expression, level or type of 15 | experience, education, socio-economic status, nationality, personal appearance, 16 | race, religion, or sexual identity and orientation. 17 | 18 | ## Our Standards 19 | 20 | We are committed to providing a friendly, safe and welcoming environment for 21 | all. 22 | 23 | Examples of behavior that contributes to creating a positive environment 24 | include: 25 | 26 | * Be kind and courteous to others 27 | * Using welcoming and inclusive language 28 | * Being respectful of differing viewpoints and experiences 29 | * Collaborating with other community members 30 | * Gracefully accepting constructive criticism 31 | * Focusing on what is best for the community 32 | * Showing empathy towards other community members 33 | 34 | Examples of unacceptable behavior by participants include: 35 | 36 | * The use of sexualized language or imagery and sexual attention or advances 37 | * The use of inappropriate images, including in a community member's avatar 38 | * The use of inappropriate language, including in a community member's nickname 39 | * Any spamming, flaming, baiting or other attention-stealing behavior 40 | * Excessive or unwelcome helping; answering outside the scope of the question 41 | asked 42 | * Trolling, insulting/derogatory comments, and personal or political attacks 43 | * Promoting or spreading disinformation, lies, or conspiracy theories against 44 | a person, group, organisation, project, or community 45 | * Public or private harassment 46 | * Publishing others' private information, such as a physical or electronic 47 | address, without explicit permission 48 | * Other conduct which could reasonably be considered inappropriate 49 | 50 | The goal of the standards and moderation guidelines outlined here is to build 51 | and maintain a respectful community. We ask that you don’t just aim to be 52 | "technically unimpeachable", but rather try to be your best self. 53 | 54 | We value many things beyond technical expertise, including collaboration and 55 | supporting others within our community. Providing a positive experience for 56 | other community members can have a much more significant impact than simply 57 | providing the correct answer. 58 | 59 | ## Our Responsibilities 60 | 61 | Project leaders are responsible for clarifying the standards of acceptable 62 | behavior and are expected to take appropriate and fair corrective action in 63 | response to any instances of unacceptable behavior. 64 | 65 | Project leaders have the right and responsibility to remove, edit, or 66 | reject messages, comments, commits, code, issues, and other contributions 67 | that are not aligned to this Code of Conduct, or to ban temporarily or 68 | permanently any community member for other behaviors that they deem 69 | inappropriate, threatening, offensive, or harmful. 70 | 71 | ## Moderation 72 | 73 | Instances of behaviors that violate the Adafruit Community Code of Conduct 74 | may be reported by any member of the community. Community members are 75 | encouraged to report these situations, including situations they witness 76 | involving other community members. 77 | 78 | You may report in the following ways: 79 | 80 | In any situation, you may send an email to . 81 | 82 | On the Adafruit Discord, you may send an open message from any channel 83 | to all Community Moderators by tagging @community moderators. You may 84 | also send an open message from any channel, or a direct message to 85 | @kattni#1507, @tannewt#4653, @Dan Halbert#1614, @cater#2442, 86 | @sommersoft#0222, @Mr. Certainly#0472 or @Andon#8175. 87 | 88 | Email and direct message reports will be kept confidential. 89 | 90 | In situations on Discord where the issue is particularly egregious, possibly 91 | illegal, requires immediate action, or violates the Discord terms of service, 92 | you should also report the message directly to Discord. 93 | 94 | These are the steps for upholding our community’s standards of conduct. 95 | 96 | 1. Any member of the community may report any situation that violates the 97 | Adafruit Community Code of Conduct. All reports will be reviewed and 98 | investigated. 99 | 2. If the behavior is an egregious violation, the community member who 100 | committed the violation may be banned immediately, without warning. 101 | 3. Otherwise, moderators will first respond to such behavior with a warning. 102 | 4. Moderators follow a soft "three strikes" policy - the community member may 103 | be given another chance, if they are receptive to the warning and change their 104 | behavior. 105 | 5. If the community member is unreceptive or unreasonable when warned by a 106 | moderator, or the warning goes unheeded, they may be banned for a first or 107 | second offense. Repeated offenses will result in the community member being 108 | banned. 109 | 110 | ## Scope 111 | 112 | This Code of Conduct and the enforcement policies listed above apply to all 113 | Adafruit Community venues. This includes but is not limited to any community 114 | spaces (both public and private), the entire Adafruit Discord server, and 115 | Adafruit GitHub repositories. Examples of Adafruit Community spaces include 116 | but are not limited to meet-ups, audio chats on the Adafruit Discord, or 117 | interaction at a conference. 118 | 119 | This Code of Conduct applies both within project spaces and in public spaces 120 | when an individual is representing the project or its community. As a community 121 | member, you are representing our community, and are expected to behave 122 | accordingly. 123 | 124 | ## Attribution 125 | 126 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 127 | version 1.4, available at 128 | , 129 | and the [Rust Code of Conduct](https://www.rust-lang.org/en-US/conduct.html). 130 | 131 | For other projects adopting the Adafruit Community Code of 132 | Conduct, please contact the maintainers of those projects for enforcement. 133 | If you wish to use this code of conduct for your own project, consider 134 | explicitly mentioning your moderation policy or making a copy with your 135 | own moderation policy so as to avoid confusion. 136 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 ladyada for Adafruit Industries 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /LICENSES/MIT.txt: -------------------------------------------------------------------------------- 1 | MIT License Copyright (c) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice (including the next 11 | paragraph) shall be included in all copies or substantial portions of the 12 | Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS 17 | OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 18 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF 19 | OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /LICENSES/Unlicense.txt: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or distribute 4 | this software, either in source code form or as a compiled binary, for any 5 | purpose, commercial or non-commercial, and by any means. 6 | 7 | In jurisdictions that recognize copyright laws, the author or authors of this 8 | software dedicate any and all copyright interest in the software to the public 9 | domain. We make this dedication for the benefit of the public at large and 10 | to the detriment of our heirs and successors. We intend this dedication to 11 | be an overt act of relinquishment in perpetuity of all present and future 12 | rights to this software under copyright law. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS 17 | BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 18 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH 19 | THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. For more information, 20 | please refer to 21 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Introduction 2 | ============ 3 | 4 | .. image:: https://readthedocs.org/projects/adafruit-circuitpython-requests/badge/?version=latest 5 | :target: https://docs.circuitpython.org/projects/requests/en/latest/ 6 | :alt: Documentation Status 7 | 8 | .. image:: https://raw.githubusercontent.com/adafruit/Adafruit_CircuitPython_Bundle/main/badges/adafruit_discord.svg 9 | :target: https://adafru.it/discord 10 | :alt: Discord 11 | 12 | .. image:: https://github.com/adafruit/Adafruit_CircuitPython_Requests/workflows/Build%20CI/badge.svg 13 | :target: https://github.com/adafruit/Adafruit_CircuitPython_Requests/actions/ 14 | :alt: Build Status 15 | 16 | .. image:: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json 17 | :target: https://github.com/astral-sh/ruff 18 | :alt: Code Style: Ruff 19 | 20 | A requests-like library for HTTP commands. 21 | 22 | 23 | Dependencies 24 | ============= 25 | This driver depends on: 26 | 27 | * `Adafruit CircuitPython `_ 28 | * `Adafruit CircuitPython ConnectionManager `_ 29 | 30 | Please ensure all dependencies are available on the CircuitPython filesystem. 31 | This is easily achieved by downloading 32 | `the Adafruit library and driver bundle `_. 33 | 34 | Installing from PyPI 35 | ===================== 36 | On supported GNU/Linux systems like the Raspberry Pi, you can install the driver locally `from 37 | PyPI `_. To install for current user: 38 | 39 | .. code-block:: shell 40 | 41 | pip3 install adafruit-circuitpython-requests 42 | 43 | To install system-wide (this may be required in some cases): 44 | 45 | .. code-block:: shell 46 | 47 | sudo pip3 install adafruit-circuitpython-requests 48 | 49 | To install in a virtual environment in your current project: 50 | 51 | .. code-block:: shell 52 | 53 | mkdir project-name && cd project-name 54 | python3 -m venv .venv 55 | source .venv/bin/activate 56 | pip3 install adafruit-circuitpython-requests 57 | 58 | Usage Example 59 | ============= 60 | 61 | Usage examples are within the `examples` subfolder of this library. 62 | 63 | Documentation 64 | ============= 65 | 66 | API documentation for this library can be found on `Read the Docs `_. 67 | 68 | For information on building library documentation, please check out `this guide `_. 69 | 70 | Contributing 71 | ============ 72 | 73 | Contributions are welcome! Please read our `Code of Conduct 74 | `_ 75 | before contributing to help this project stay welcoming. 76 | -------------------------------------------------------------------------------- /README.rst.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries 2 | 3 | SPDX-License-Identifier: MIT 4 | -------------------------------------------------------------------------------- /docs/_static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adafruit/Adafruit_CircuitPython_Requests/21255976fe061946cd353e416e254effb45ff12a/docs/_static/favicon.ico -------------------------------------------------------------------------------- /docs/_static/favicon.ico.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2018 Phillip Torrone for Adafruit Industries 2 | 3 | SPDX-License-Identifier: CC-BY-4.0 4 | -------------------------------------------------------------------------------- /docs/api.rst: -------------------------------------------------------------------------------- 1 | 2 | .. If you created a package, create one automodule per module in the package. 3 | 4 | .. If your library file(s) are nested in a directory (e.g. /adafruit_foo/foo.py) 5 | .. use this format as the module name: "adafruit_foo.foo" 6 | 7 | .. automodule:: adafruit_requests 8 | :members: 9 | -------------------------------------------------------------------------------- /docs/api.rst.license: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2020 ladyada for Adafruit Industries 2 | # 3 | # SPDX-License-Identifier: MIT 4 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 ladyada for Adafruit Industries 2 | # 3 | # SPDX-License-Identifier: MIT 4 | 5 | import datetime 6 | import os 7 | import sys 8 | 9 | sys.path.insert(0, os.path.abspath("..")) 10 | 11 | # -- General configuration ------------------------------------------------ 12 | 13 | # Add any Sphinx extension module names here, as strings. They can be 14 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 15 | # ones. 16 | extensions = [ 17 | "sphinx.ext.autodoc", 18 | "sphinxcontrib.jquery", 19 | "sphinx.ext.intersphinx", 20 | "sphinx.ext.napoleon", 21 | "sphinx.ext.todo", 22 | ] 23 | 24 | # TODO: Please Read! 25 | # Uncomment the below if you use native CircuitPython modules such as 26 | # digitalio, micropython and busio. List the modules you use. Without it, the 27 | # autodoc module docs will fail to generate with a warning. 28 | # autodoc_mock_imports = ["digitalio", "busio"] 29 | 30 | 31 | intersphinx_mapping = { 32 | "python": ("https://docs.python.org/3", None), 33 | "CircuitPython": ("https://docs.circuitpython.org/en/latest/", None), 34 | } 35 | 36 | # Add any paths that contain templates here, relative to this directory. 37 | templates_path = ["_templates"] 38 | 39 | source_suffix = ".rst" 40 | 41 | # The master toctree document. 42 | master_doc = "index" 43 | 44 | # General information about the project. 45 | project = "Adafruit Requests Library" 46 | creation_year = "2019" 47 | current_year = str(datetime.datetime.now().year) 48 | year_duration = ( 49 | current_year if current_year == creation_year else creation_year + " - " + current_year 50 | ) 51 | copyright = year_duration + " ladyada" 52 | author = "ladyada" 53 | 54 | # The version info for the project you're documenting, acts as replacement for 55 | # |version| and |release|, also used in various other places throughout the 56 | # built documents. 57 | # 58 | # The short X.Y version. 59 | version = "1.0" 60 | # The full version, including alpha/beta/rc tags. 61 | release = "1.0" 62 | 63 | # The language for content autogenerated by Sphinx. Refer to documentation 64 | # for a list of supported languages. 65 | # 66 | # This is also used if you do content translation via gettext catalogs. 67 | # Usually you set "language" from the command line for these cases. 68 | language = "en" 69 | 70 | # List of patterns, relative to source directory, that match files and 71 | # directories to ignore when looking for source files. 72 | # This patterns also effect to html_static_path and html_extra_path 73 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store", ".env", "CODE_OF_CONDUCT.md"] 74 | 75 | # The reST default role (used for this markup: `text`) to use for all 76 | # documents. 77 | # 78 | default_role = "any" 79 | 80 | # If true, '()' will be appended to :func: etc. cross-reference text. 81 | # 82 | add_function_parentheses = True 83 | 84 | # The name of the Pygments (syntax highlighting) style to use. 85 | pygments_style = "sphinx" 86 | 87 | # If true, `todo` and `todoList` produce output, else they produce nothing. 88 | todo_include_todos = False 89 | 90 | # If this is True, todo emits a warning for each TODO entries. The default is False. 91 | todo_emit_warnings = True 92 | 93 | napoleon_numpy_docstring = False 94 | 95 | # -- Options for HTML output ---------------------------------------------- 96 | 97 | # The theme to use for HTML and HTML Help pages. See the documentation for 98 | # a list of builtin themes. 99 | # 100 | import sphinx_rtd_theme 101 | 102 | html_theme = "sphinx_rtd_theme" 103 | 104 | # Add any paths that contain custom static files (such as style sheets) here, 105 | # relative to this directory. They are copied after the builtin static files, 106 | # so a file named "default.css" will overwrite the builtin "default.css". 107 | html_static_path = ["_static"] 108 | 109 | # The name of an image file (relative to this directory) to use as a favicon of 110 | # the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 111 | # pixels large. 112 | # 113 | html_favicon = "_static/favicon.ico" 114 | 115 | # Output file base name for HTML help builder. 116 | htmlhelp_basename = "AdafruitRequestsLibrarydoc" 117 | 118 | # -- Options for LaTeX output --------------------------------------------- 119 | 120 | latex_elements = { 121 | # The paper size ('letterpaper' or 'a4paper'). 122 | # 123 | # 'papersize': 'letterpaper', 124 | # The font size ('10pt', '11pt' or '12pt'). 125 | # 126 | # 'pointsize': '10pt', 127 | # Additional stuff for the LaTeX preamble. 128 | # 129 | # 'preamble': '', 130 | # Latex figure (float) alignment 131 | # 132 | # 'figure_align': 'htbp', 133 | } 134 | 135 | # Grouping the document tree into LaTeX files. List of tuples 136 | # (source start file, target name, title, 137 | # author, documentclass [howto, manual, or own class]). 138 | latex_documents = [ 139 | ( 140 | master_doc, 141 | "AdafruitRequestsLibrary.tex", 142 | "AdafruitRequests Library Documentation", 143 | author, 144 | "manual", 145 | ), 146 | ] 147 | 148 | # -- Options for manual page output --------------------------------------- 149 | 150 | # One entry per manual page. List of tuples 151 | # (source start file, name, description, authors, manual section). 152 | man_pages = [ 153 | ( 154 | master_doc, 155 | "AdafruitRequestslibrary", 156 | "Adafruit Requests Library Documentation", 157 | [author], 158 | 1, 159 | ) 160 | ] 161 | 162 | # -- Options for Texinfo output ------------------------------------------- 163 | 164 | # Grouping the document tree into Texinfo files. List of tuples 165 | # (source start file, target name, title, author, 166 | # dir menu entry, description, category) 167 | texinfo_documents = [ 168 | ( 169 | master_doc, 170 | "AdafruitRequestsLibrary", 171 | "Adafruit Requests Library Documentation", 172 | author, 173 | "AdafruitRequestsLibrary", 174 | "One line description of project.", 175 | "Miscellaneous", 176 | ), 177 | ] 178 | -------------------------------------------------------------------------------- /docs/examples.rst: -------------------------------------------------------------------------------- 1 | Examples 2 | ======== 3 | 4 | Below are a few examples, for use with common boards. There are more in the examples folder of the library 5 | 6 | On-board WiFi 7 | -------------- 8 | 9 | .. literalinclude:: ../examples/wifi/requests_wifi_simpletest.py 10 | :caption: examples/wifi/requests_wifi_simpletest.py 11 | :linenos: 12 | 13 | ESP32SPI 14 | -------------- 15 | 16 | .. literalinclude:: ../examples/esp32spi/requests_esp32spi_simpletest.py 17 | :caption: examples/esp32spi/requests_esp32spi_simpletest.py 18 | :linenos: 19 | 20 | WIZNET5K 21 | -------------- 22 | 23 | .. literalinclude:: ../examples/wiznet5k/requests_wiznet5k_simpletest.py 24 | :caption: examples/wiznet5k/requests_wiznet5k_simpletest.py 25 | :linenos: 26 | 27 | Fona 28 | -------------- 29 | 30 | .. literalinclude:: ../examples/fona/requests_fona_simpletest.py 31 | :caption: examples/fona/requests_fona_simpletest.py 32 | :linenos: 33 | -------------------------------------------------------------------------------- /docs/examples.rst.license: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2020 ladyada for Adafruit Industries 2 | # 3 | # SPDX-License-Identifier: MIT 4 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../README.rst 2 | 3 | Table of Contents 4 | ================= 5 | 6 | .. toctree:: 7 | :maxdepth: 4 8 | :hidden: 9 | 10 | self 11 | 12 | .. toctree:: 13 | :caption: Examples 14 | 15 | examples 16 | 17 | .. toctree:: 18 | :caption: API Reference 19 | :maxdepth: 3 20 | 21 | api 22 | 23 | .. toctree:: 24 | :caption: Tutorials 25 | 26 | .. toctree:: 27 | :caption: Related Products 28 | 29 | .. toctree:: 30 | :caption: Other Links 31 | 32 | Download from GitHub 33 | Download Library Bundle 34 | CircuitPython Reference Documentation 35 | CircuitPython Support Forum 36 | Discord Chat 37 | Adafruit Learning System 38 | Adafruit Blog 39 | Adafruit Store 40 | 41 | Indices and tables 42 | ================== 43 | 44 | * :ref:`genindex` 45 | * :ref:`modindex` 46 | * :ref:`search` 47 | -------------------------------------------------------------------------------- /docs/index.rst.license: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2020 ladyada for Adafruit Industries 2 | # 3 | # SPDX-License-Identifier: MIT 4 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 Kattni Rembor for Adafruit Industries 2 | # 3 | # SPDX-License-Identifier: Unlicense 4 | 5 | sphinx 6 | sphinxcontrib-jquery 7 | sphinx-rtd-theme 8 | -------------------------------------------------------------------------------- /examples/cpython/requests_cpython_advanced.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 ladyada for Adafruit Industries 2 | # SPDX-License-Identifier: MIT 3 | 4 | import socket as pool 5 | import ssl 6 | 7 | import adafruit_requests 8 | 9 | # Initialize a requests session 10 | requests = adafruit_requests.Session(pool, ssl.create_default_context()) 11 | 12 | JSON_GET_URL = "https://httpbin.org/get" 13 | 14 | # Define a custom header as a dict. 15 | headers = {"user-agent": "blinka/1.0.0"} 16 | 17 | print("Fetching JSON data from %s..." % JSON_GET_URL) 18 | with requests.get(JSON_GET_URL, headers=headers) as response: 19 | print("-" * 60) 20 | json_data = response.json() 21 | headers = json_data["headers"] 22 | print("Response's Custom User-Agent Header: {0}".format(headers["User-Agent"])) 23 | print("-" * 60) 24 | 25 | # Read Response's HTTP status code 26 | print("Response HTTP Status Code: ", response.status_code) 27 | print("-" * 60) 28 | -------------------------------------------------------------------------------- /examples/cpython/requests_cpython_simpletest.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 ladyada for Adafruit Industries 2 | # SPDX-License-Identifier: MIT 3 | 4 | import socket as pool 5 | import ssl 6 | 7 | import adafruit_requests 8 | 9 | # Initialize a requests session 10 | requests = adafruit_requests.Session(pool, ssl.create_default_context()) 11 | 12 | TEXT_URL = "http://wifitest.adafruit.com/testwifi/index.html" 13 | JSON_GET_URL = "https://httpbin.org/get" 14 | JSON_POST_URL = "https://httpbin.org/post" 15 | 16 | print("Fetching text from %s" % TEXT_URL) 17 | with requests.get(TEXT_URL) as response: 18 | print("-" * 40) 19 | print("Text Response: ", response.text) 20 | print("-" * 40) 21 | 22 | print("Fetching JSON data from %s" % JSON_GET_URL) 23 | with requests.get(JSON_GET_URL) as response: 24 | print("-" * 40) 25 | print("JSON Response: ", response.json()) 26 | print("-" * 40) 27 | 28 | data = "31F" 29 | print(f"POSTing data to {JSON_POST_URL}: {data}") 30 | with requests.post(JSON_POST_URL, data=data) as response: 31 | print("-" * 40) 32 | json_resp = response.json() 33 | # Parse out the 'data' key from json_resp dict. 34 | print("Data received from server:", json_resp["data"]) 35 | print("-" * 40) 36 | 37 | json_data = {"Date": "July 25, 2019"} 38 | print(f"POSTing data to {JSON_POST_URL}: {json_data}") 39 | with requests.post(JSON_POST_URL, json=json_data) as response: 40 | print("-" * 40) 41 | json_resp = response.json() 42 | # Parse out the 'json' key from json_resp dict. 43 | print("JSON Data received from server:", json_resp["json"]) 44 | print("-" * 40) 45 | -------------------------------------------------------------------------------- /examples/esp32spi/requests_esp32spi_advanced.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 ladyada for Adafruit Industries 2 | # SPDX-License-Identifier: MIT 3 | 4 | import os 5 | 6 | import adafruit_connection_manager 7 | import board 8 | import busio 9 | from adafruit_esp32spi import adafruit_esp32spi 10 | from digitalio import DigitalInOut 11 | 12 | import adafruit_requests 13 | 14 | # Get WiFi details, ensure these are setup in settings.toml 15 | ssid = os.getenv("CIRCUITPY_WIFI_SSID") 16 | password = os.getenv("CIRCUITPY_WIFI_PASSWORD") 17 | 18 | # If you are using a board with pre-defined ESP32 Pins: 19 | esp32_cs = DigitalInOut(board.ESP_CS) 20 | esp32_ready = DigitalInOut(board.ESP_BUSY) 21 | esp32_reset = DigitalInOut(board.ESP_RESET) 22 | 23 | # If you have an externally connected ESP32: 24 | # esp32_cs = DigitalInOut(board.D9) 25 | # esp32_ready = DigitalInOut(board.D10) 26 | # esp32_reset = DigitalInOut(board.D5) 27 | 28 | # If you have an AirLift Featherwing or ItsyBitsy Airlift: 29 | # esp32_cs = DigitalInOut(board.D13) 30 | # esp32_ready = DigitalInOut(board.D11) 31 | # esp32_reset = DigitalInOut(board.D12) 32 | 33 | spi = busio.SPI(board.SCK, board.MOSI, board.MISO) 34 | radio = adafruit_esp32spi.ESP_SPIcontrol(spi, esp32_cs, esp32_ready, esp32_reset) 35 | 36 | print("Connecting to AP...") 37 | while not radio.is_connected: 38 | try: 39 | radio.connect_AP(ssid, password) 40 | except RuntimeError as e: 41 | print("could not connect to AP, retrying: ", e) 42 | continue 43 | print("Connected to", str(radio.ap_info.ssid, "utf-8"), "\tRSSI:", radio.ap_info.rssi) 44 | 45 | # Initialize a requests session 46 | pool = adafruit_connection_manager.get_radio_socketpool(radio) 47 | ssl_context = adafruit_connection_manager.get_radio_ssl_context(radio) 48 | requests = adafruit_requests.Session(pool, ssl_context) 49 | 50 | JSON_GET_URL = "https://httpbin.org/get" 51 | 52 | # Define a custom header as a dict. 53 | headers = {"user-agent": "blinka/1.0.0"} 54 | 55 | print("Fetching JSON data from %s..." % JSON_GET_URL) 56 | with requests.get(JSON_GET_URL, headers=headers) as response: 57 | print("-" * 60) 58 | 59 | json_data = response.json() 60 | headers = json_data["headers"] 61 | print("Response's Custom User-Agent Header: {0}".format(headers["User-Agent"])) 62 | print("-" * 60) 63 | 64 | # Read Response's HTTP status code 65 | print("Response HTTP Status Code: ", response.status_code) 66 | print("-" * 60) 67 | -------------------------------------------------------------------------------- /examples/esp32spi/requests_esp32spi_simpletest.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 ladyada for Adafruit Industries 2 | # SPDX-License-Identifier: MIT 3 | 4 | import os 5 | 6 | import adafruit_connection_manager 7 | import board 8 | import busio 9 | from adafruit_esp32spi import adafruit_esp32spi 10 | from digitalio import DigitalInOut 11 | 12 | import adafruit_requests 13 | 14 | # Get WiFi details, ensure these are setup in settings.toml 15 | ssid = os.getenv("CIRCUITPY_WIFI_SSID") 16 | password = os.getenv("CIRCUITPY_WIFI_PASSWORD") 17 | 18 | # If you are using a board with pre-defined ESP32 Pins: 19 | esp32_cs = DigitalInOut(board.ESP_CS) 20 | esp32_ready = DigitalInOut(board.ESP_BUSY) 21 | esp32_reset = DigitalInOut(board.ESP_RESET) 22 | 23 | # If you have an externally connected ESP32: 24 | # esp32_cs = DigitalInOut(board.D9) 25 | # esp32_ready = DigitalInOut(board.D10) 26 | # esp32_reset = DigitalInOut(board.D5) 27 | 28 | # If you have an AirLift Featherwing or ItsyBitsy Airlift: 29 | # esp32_cs = DigitalInOut(board.D13) 30 | # esp32_ready = DigitalInOut(board.D11) 31 | # esp32_reset = DigitalInOut(board.D12) 32 | 33 | spi = busio.SPI(board.SCK, board.MOSI, board.MISO) 34 | radio = adafruit_esp32spi.ESP_SPIcontrol(spi, esp32_cs, esp32_ready, esp32_reset) 35 | 36 | print("Connecting to AP...") 37 | while not radio.is_connected: 38 | try: 39 | radio.connect_AP(ssid, password) 40 | except RuntimeError as e: 41 | print("could not connect to AP, retrying: ", e) 42 | continue 43 | print("Connected to", str(radio.ap_info.ssid, "utf-8"), "\tRSSI:", radio.ap_info.rssi) 44 | 45 | # Initialize a requests session 46 | pool = adafruit_connection_manager.get_radio_socketpool(radio) 47 | ssl_context = adafruit_connection_manager.get_radio_ssl_context(radio) 48 | requests = adafruit_requests.Session(pool, ssl_context) 49 | 50 | TEXT_URL = "http://wifitest.adafruit.com/testwifi/index.html" 51 | JSON_GET_URL = "https://httpbin.org/get" 52 | JSON_POST_URL = "https://httpbin.org/post" 53 | 54 | print("Fetching text from %s" % TEXT_URL) 55 | with requests.get(TEXT_URL) as response: 56 | print("-" * 40) 57 | print("Text Response: ", response.text) 58 | print("-" * 40) 59 | 60 | print("Fetching JSON data from %s" % JSON_GET_URL) 61 | with requests.get(JSON_GET_URL) as response: 62 | print("-" * 40) 63 | print("JSON Response: ", response.json()) 64 | print("-" * 40) 65 | 66 | data = "31F" 67 | print(f"POSTing data to {JSON_POST_URL}: {data}") 68 | with requests.post(JSON_POST_URL, data=data) as response: 69 | print("-" * 40) 70 | json_resp = response.json() 71 | # Parse out the 'data' key from json_resp dict. 72 | print("Data received from server:", json_resp["data"]) 73 | print("-" * 40) 74 | 75 | json_data = {"Date": "July 25, 2019"} 76 | print(f"POSTing data to {JSON_POST_URL}: {json_data}") 77 | with requests.post(JSON_POST_URL, json=json_data) as response: 78 | print("-" * 40) 79 | json_resp = response.json() 80 | # Parse out the 'json' key from json_resp dict. 81 | print("JSON Data received from server:", json_resp["json"]) 82 | print("-" * 40) 83 | -------------------------------------------------------------------------------- /examples/fona/requests_fona_advanced.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 ladyada for Adafruit Industries 2 | # SPDX-License-Identifier: MIT 3 | 4 | import os 5 | import time 6 | 7 | import adafruit_connection_manager 8 | import adafruit_fona.adafruit_fona_network as network 9 | import adafruit_fona.adafruit_fona_socket as pool 10 | import board 11 | import busio 12 | import digitalio 13 | from adafruit_fona.adafruit_fona import FONA 14 | from adafruit_fona.fona_3g import FONA3G 15 | 16 | import adafruit_requests 17 | 18 | # Get GPRS details, ensure these are setup in settings.toml 19 | apn = os.getenv("APN") 20 | apn_username = os.getenv("APN_USERNAME") 21 | apn_password = os.getenv("APN_PASSWORD") 22 | 23 | # Create a serial connection for the FONA connection 24 | uart = busio.UART(board.TX, board.RX) 25 | rst = digitalio.DigitalInOut(board.D4) 26 | 27 | # Use this for FONA800 and FONA808 28 | radio = FONA(uart, rst) 29 | 30 | # Use this for FONA3G 31 | # radio = FONA3G(uart, rst) 32 | 33 | # Initialize cellular data network 34 | network = network.CELLULAR(radio, (apn, apn_username, apn_password)) 35 | 36 | while not network.is_attached: 37 | print("Attaching to network...") 38 | time.sleep(0.5) 39 | print("Attached!") 40 | 41 | while not network.is_connected: 42 | print("Connecting to network...") 43 | network.connect() 44 | time.sleep(0.5) 45 | print("Network Connected!") 46 | 47 | # Initialize a requests session 48 | ssl_context = adafruit_connection_manager.create_fake_ssl_context(pool, radio) 49 | requests = adafruit_requests.Session(pool, ssl_context) 50 | 51 | JSON_GET_URL = "http://httpbin.org/get" 52 | 53 | # Define a custom header as a dict. 54 | headers = {"user-agent": "blinka/1.0.0"} 55 | 56 | print("Fetching JSON data from %s..." % JSON_GET_URL) 57 | with requests.get(JSON_GET_URL, headers=headers) as response: 58 | print("-" * 60) 59 | 60 | json_data = response.json() 61 | headers = json_data["headers"] 62 | print("Response's Custom User-Agent Header: {0}".format(headers["User-Agent"])) 63 | print("-" * 60) 64 | 65 | # Read Response's HTTP status code 66 | print("Response HTTP Status Code: ", response.status_code) 67 | print("-" * 60) 68 | -------------------------------------------------------------------------------- /examples/fona/requests_fona_simpletest.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 ladyada for Adafruit Industries 2 | # SPDX-License-Identifier: MIT 3 | 4 | import os 5 | import time 6 | 7 | import adafruit_connection_manager 8 | import adafruit_fona.adafruit_fona_network as network 9 | import adafruit_fona.adafruit_fona_socket as pool 10 | import board 11 | import busio 12 | import digitalio 13 | from adafruit_fona.adafruit_fona import FONA 14 | from adafruit_fona.fona_3g import FONA3G 15 | 16 | import adafruit_requests 17 | 18 | # Get GPRS details, ensure these are setup in settings.toml 19 | apn = os.getenv("APN") 20 | apn_username = os.getenv("APN_USERNAME") 21 | apn_password = os.getenv("APN_PASSWORD") 22 | 23 | # Create a serial connection for the FONA connection 24 | uart = busio.UART(board.TX, board.RX) 25 | rst = digitalio.DigitalInOut(board.D4) 26 | 27 | # Use this for FONA800 and FONA808 28 | radio = FONA(uart, rst) 29 | 30 | # Use this for FONA3G 31 | # radio = FONA3G(uart, rst) 32 | 33 | # Initialize cellular data network 34 | network = network.CELLULAR(radio, (apn, apn_username, apn_password)) 35 | 36 | while not network.is_attached: 37 | print("Attaching to network...") 38 | time.sleep(0.5) 39 | print("Attached!") 40 | 41 | while not network.is_connected: 42 | print("Connecting to network...") 43 | network.connect() 44 | time.sleep(0.5) 45 | print("Network Connected!") 46 | 47 | # Initialize a requests session 48 | ssl_context = adafruit_connection_manager.create_fake_ssl_context(pool, radio) 49 | requests = adafruit_requests.Session(pool, ssl_context) 50 | 51 | TEXT_URL = "http://wifitest.adafruit.com/testwifi/index.html" 52 | JSON_GET_URL = "http://httpbin.org/get" 53 | JSON_POST_URL = "http://httpbin.org/post" 54 | 55 | print("Fetching text from %s" % TEXT_URL) 56 | with requests.get(TEXT_URL) as response: 57 | print("-" * 40) 58 | print("Text Response: ", response.text) 59 | print("-" * 40) 60 | 61 | print("Fetching JSON data from %s" % JSON_GET_URL) 62 | with requests.get(JSON_GET_URL) as response: 63 | print("-" * 40) 64 | print("JSON Response: ", response.json()) 65 | print("-" * 40) 66 | 67 | data = "31F" 68 | print(f"POSTing data to {JSON_POST_URL}: {data}") 69 | with requests.post(JSON_POST_URL, data=data) as response: 70 | print("-" * 40) 71 | json_resp = response.json() 72 | # Parse out the 'data' key from json_resp dict. 73 | print("Data received from server:", json_resp["data"]) 74 | print("-" * 40) 75 | 76 | json_data = {"Date": "July 25, 2019"} 77 | print(f"POSTing data to {JSON_POST_URL}: {json_data}") 78 | with requests.post(JSON_POST_URL, json=json_data) as response: 79 | print("-" * 40) 80 | json_resp = response.json() 81 | # Parse out the 'json' key from json_resp dict. 82 | print("JSON Data received from server:", json_resp["json"]) 83 | print("-" * 40) 84 | -------------------------------------------------------------------------------- /examples/wifi/expanded/requests_wifi_adafruit_discord_active_online.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 DJDevon3 2 | # SPDX-License-Identifier: MIT 3 | # Coded for Circuit Python 8.2.x 4 | """Discord Active Online Shields.IO Example""" 5 | 6 | import os 7 | import time 8 | 9 | import adafruit_connection_manager 10 | import wifi 11 | 12 | import adafruit_requests 13 | 14 | # Public API. No user or token required 15 | # JSON web scrape from SHIELDS.IO 16 | # Adafruit uses Shields.IO to see online users 17 | 18 | # Get WiFi details, ensure these are setup in settings.toml 19 | ssid = os.getenv("CIRCUITPY_WIFI_SSID") 20 | password = os.getenv("CIRCUITPY_WIFI_PASSWORD") 21 | 22 | # API Polling Rate 23 | # 900 = 15 mins, 1800 = 30 mins, 3600 = 1 hour 24 | SLEEP_TIME = 900 25 | 26 | # Initalize Wifi, Socket Pool, Request Session 27 | pool = adafruit_connection_manager.get_radio_socketpool(wifi.radio) 28 | ssl_context = adafruit_connection_manager.get_radio_ssl_context(wifi.radio) 29 | requests = adafruit_requests.Session(pool, ssl_context) 30 | 31 | 32 | def time_calc(input_time): 33 | """Converts seconds to minutes/hours/days""" 34 | if input_time < 60: 35 | return f"{input_time:.0f} seconds" 36 | if input_time < 3600: 37 | return f"{input_time / 60:.0f} minutes" 38 | if input_time < 86400: 39 | return f"{input_time / 60 / 60:.0f} hours" 40 | return f"{input_time / 60 / 60 / 24:.1f} days" 41 | 42 | 43 | # Originally attempted to use SVG. Found JSON exists with same filename. 44 | # https://img.shields.io/discord/327254708534116352.svg 45 | ADA_DISCORD_JSON = "https://img.shields.io/discord/327254708534116352.json" 46 | 47 | while True: 48 | # Connect to Wi-Fi 49 | print("\nConnecting to WiFi...") 50 | while not wifi.radio.ipv4_address: 51 | try: 52 | wifi.radio.connect(ssid, password) 53 | except ConnectionError as e: 54 | print("❌ Connection Error:", e) 55 | print("Retrying in 10 seconds") 56 | print("✅ Wifi!") 57 | try: 58 | print(" | Attempting to GET Adafruit Discord JSON!") 59 | # Set debug to True for full JSON response. 60 | DEBUG_RESPONSE = True 61 | 62 | try: 63 | with requests.get(url=ADA_DISCORD_JSON) as shieldsio_response: 64 | shieldsio_json = shieldsio_response.json() 65 | except ConnectionError as e: 66 | print(f"Connection Error: {e}") 67 | print("Retrying in 10 seconds") 68 | print(" | ✅ Adafruit Discord JSON!") 69 | 70 | if DEBUG_RESPONSE: 71 | print(" | | Full API GET URL: ", ADA_DISCORD_JSON) 72 | print(" | | JSON Dump: ", shieldsio_json) 73 | 74 | ada_users = shieldsio_json["value"] 75 | ONLINE_STRING = " online" 76 | REPLACE_WITH_NOTHING = "" 77 | active_users = ada_users.replace(ONLINE_STRING, REPLACE_WITH_NOTHING) 78 | print(f" | | Active Online Users: {active_users}") 79 | 80 | print("\nFinished!") 81 | print(f"Board Uptime: {time.monotonic()}") 82 | print(f"Next Update: {time_calc(SLEEP_TIME)}") 83 | print("===============================") 84 | 85 | except (ValueError, RuntimeError) as e: 86 | print(f"Failed to get data, retrying\n {e}") 87 | time.sleep(60) 88 | break 89 | time.sleep(SLEEP_TIME) 90 | -------------------------------------------------------------------------------- /examples/wifi/expanded/requests_wifi_api_discord.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 DJDevon3 2 | # SPDX-License-Identifier: MIT 3 | # Coded for Circuit Python 8.2.x 4 | """Discord Web Scrape Example""" 5 | 6 | import os 7 | import time 8 | 9 | import adafruit_connection_manager 10 | import wifi 11 | 12 | import adafruit_requests 13 | 14 | # Active Logged in User Account Required 15 | # WEB SCRAPE authorization key required. Visit URL below. 16 | # Learn how: https://github.com/lorenz234/Discord-Data-Scraping 17 | 18 | # Ensure this is in settings.toml 19 | # DISCORD_AUTHORIZATION = "Approximately 70 Character Hash Here" 20 | 21 | # Get WiFi details, ensure these are setup in settings.toml 22 | ssid = os.getenv("CIRCUITPY_WIFI_SSID") 23 | password = os.getenv("CIRCUITPY_WIFI_PASSWORD") 24 | discord_auth = os.getenv("DISCORD_AUTHORIZATION") 25 | 26 | # API Polling Rate 27 | # 900 = 15 mins, 1800 = 30 mins, 3600 = 1 hour 28 | SLEEP_TIME = 900 29 | 30 | # Initalize Wifi, Socket Pool, Request Session 31 | pool = adafruit_connection_manager.get_radio_socketpool(wifi.radio) 32 | ssl_context = adafruit_connection_manager.get_radio_ssl_context(wifi.radio) 33 | requests = adafruit_requests.Session(pool, ssl_context) 34 | 35 | 36 | def time_calc(input_time): 37 | """Converts seconds to minutes/hours/days""" 38 | if input_time < 60: 39 | return f"{input_time:.0f} seconds" 40 | if input_time < 3600: 41 | return f"{input_time / 60:.0f} minutes" 42 | if input_time < 86400: 43 | return f"{input_time / 60 / 60:.0f} hours" 44 | return f"{input_time / 60 / 60 / 24:.1f} days" 45 | 46 | 47 | DISCORD_HEADER = {"Authorization": "" + discord_auth} 48 | DISCORD_SOURCE = ( 49 | "https://discord.com/api/v10/guilds/" 50 | + "327254708534116352" # Adafruit Discord ID 51 | + "/preview" 52 | ) 53 | 54 | while True: 55 | # Connect to Wi-Fi 56 | print("\nConnecting to WiFi...") 57 | while not wifi.radio.ipv4_address: 58 | try: 59 | wifi.radio.connect(ssid, password) 60 | except ConnectionError as e: 61 | print("❌ Connection Error:", e) 62 | print("Retrying in 10 seconds") 63 | print("✅ Wifi!") 64 | try: 65 | print(" | Attempting to GET Discord JSON!") 66 | # Set debug to True for full JSON response. 67 | # WARNING: may include visible credentials 68 | # MICROCONTROLLER WARNING: might crash by returning too much data 69 | DEBUG_RESPONSE = False 70 | 71 | try: 72 | with requests.get(url=DISCORD_SOURCE, headers=DISCORD_HEADER) as discord_response: 73 | discord_json = discord_response.json() 74 | except ConnectionError as e: 75 | print(f"Connection Error: {e}") 76 | print("Retrying in 10 seconds") 77 | print(" | ✅ Discord JSON!") 78 | 79 | if DEBUG_RESPONSE: 80 | print(f" | | Full API GET URL: {DISCORD_SOURCE}") 81 | print(f" | | JSON Dump: {discord_json}") 82 | 83 | discord_name = discord_json["name"] 84 | print(f" | | Name: {discord_name}") 85 | 86 | discord_description = discord_json["description"] 87 | print(f" | | Description: {discord_description}") 88 | 89 | discord_all_members = discord_json["approximate_member_count"] 90 | print(f" | | Members: {discord_all_members}") 91 | 92 | discord_all_members_online = discord_json["approximate_presence_count"] 93 | print(f" | | Online: {discord_all_members_online}") 94 | 95 | print("\nFinished!") 96 | print(f"Board Uptime: {time.monotonic()}") 97 | print(f"Next Update: {time_calc(SLEEP_TIME)}") 98 | print("===============================") 99 | 100 | except (ValueError, RuntimeError) as e: 101 | print(f"Failed to get data, retrying\n {e}") 102 | time.sleep(60) 103 | break 104 | time.sleep(SLEEP_TIME) 105 | -------------------------------------------------------------------------------- /examples/wifi/expanded/requests_wifi_api_fitbit.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 DJDevon3 2 | # SPDX-License-Identifier: MIT 3 | # Coded for Circuit Python 8.2.x 4 | """Fitbit API Example""" 5 | 6 | import os 7 | import time 8 | 9 | import adafruit_connection_manager 10 | import microcontroller 11 | import wifi 12 | 13 | import adafruit_requests 14 | 15 | # --- Fitbit Developer Account & oAuth App Required: --- 16 | # Required: Google Login (Fitbit owned by Google) & Fitbit Device 17 | # Step 1: Register a personal app here: https://dev.fitbit.com 18 | # Step 2: Use their Tutorial to get the Token and first Refresh Token 19 | # Fitbit's Tutorial Step 4 is as far as you need to go. 20 | # https://dev.fitbit.com/build/reference/web-api/troubleshooting-guide/oauth2-tutorial/ 21 | 22 | # Ensure these are in settings.toml 23 | # Fitbit_ClientID = "YourAppClientID" 24 | # Fitbit_Token = "Long 256 character string (SHA-256)" 25 | # Fitbit_First_Refresh_Token = "64 character string" 26 | # Fitbit_UserID = "UserID authorizing the ClientID" 27 | 28 | # Get WiFi details, ensure these are setup in settings.toml 29 | ssid = os.getenv("CIRCUITPY_WIFI_SSID") 30 | password = os.getenv("CIRCUITPY_WIFI_PASSWORD") 31 | Fitbit_ClientID = os.getenv("FITBIT_CLIENTID") 32 | Fitbit_Token = os.getenv("FITBIT_ACCESS_TOKEN") 33 | Fitbit_First_Refresh_Token = os.getenv("FITBIT_FIRST_REFRESH_TOKEN") # overides nvm first run only 34 | Fitbit_UserID = os.getenv("FITBIT_USERID") 35 | 36 | # Set debug to True for full INTRADAY JSON response. 37 | # WARNING: may include visible credentials 38 | # MICROCONTROLLER WARNING: might crash by returning too much data 39 | DEBUG = False 40 | 41 | # Set debug to True for full DEVICE (Watch) JSON response. 42 | # WARNING: may include visible credentials 43 | # This will not return enough data to crash your device. 44 | DEBUG_DEVICE = False 45 | 46 | # No data from midnight to 00:15 due to lack of 15 values. 47 | # Debug midnight to display something else in this time frame. 48 | MIDNIGHT_DEBUG = False 49 | 50 | # WARNING: Optional: Resets board nvm to factory default. Clean slate. 51 | # Instructions will be printed to console while reset is True. 52 | RESET_NVM = False # Set True once, then back to False 53 | if RESET_NVM: 54 | microcontroller.nvm[0:64] = bytearray(b"\x00" * 64) 55 | # API Polling Rate 56 | # 900 = 15 mins, 1800 = 30 mins, 3600 = 1 hour 57 | SLEEP_TIME = 900 58 | 59 | # Initalize Wifi, Socket Pool, Request Session 60 | pool = adafruit_connection_manager.get_radio_socketpool(wifi.radio) 61 | ssl_context = adafruit_connection_manager.get_radio_ssl_context(wifi.radio) 62 | requests = adafruit_requests.Session(pool, ssl_context) 63 | 64 | 65 | def time_calc(input_time): 66 | """Converts seconds to minutes/hours/days""" 67 | if input_time < 60: 68 | return f"{input_time:.0f} seconds" 69 | if input_time < 3600: 70 | return f"{input_time / 60:.0f} minutes" 71 | if input_time < 86400: 72 | return f"{input_time / 60 / 60:.0f} hours" 73 | return f"{input_time / 60 / 60 / 24:.1f} days" 74 | 75 | 76 | # Authenticates Client ID & SHA-256 Token to POST 77 | FITBIT_OAUTH_HEADER = {"Content-Type": "application/x-www-form-urlencoded"} 78 | FITBIT_OAUTH_TOKEN = "https://api.fitbit.com/oauth2/token" 79 | 80 | # Use to confirm first instance of NVM is the correct refresh token 81 | FIRST_RUN = True 82 | Refresh_Token = Fitbit_First_Refresh_Token 83 | top_nvm = microcontroller.nvm[0:64].decode() 84 | nvm_bytes = microcontroller.nvm[0:64] 85 | top_nvm_3bytes = nvm_bytes[0:3] 86 | if DEBUG: 87 | print(f"Top NVM Length: {len(top_nvm)}") 88 | print(f"Top NVM: {top_nvm}") 89 | print(f"Top NVM bytes: {top_nvm_3bytes}") 90 | if RESET_NVM: 91 | microcontroller.nvm[0:64] = bytearray(b"\x00" * 64) 92 | if top_nvm_3bytes == b"\x00\x00\x00": 93 | print("TOP NVM IS BRAND NEW! WAITING FOR A FIRST TOKEN") 94 | Fitbit_First_Refresh_Token = top_nvm 95 | print(f"Top NVM RESET: {top_nvm}") # No token should appear 96 | Refresh_Token = microcontroller.nvm[0:64].decode() 97 | print(f"Refresh_Token Reset: {Refresh_Token}") # No token should appear 98 | while True: 99 | if not RESET_NVM: 100 | # Connect to Wi-Fi 101 | print("\n📡 Connecting to WiFi...") 102 | while not wifi.radio.ipv4_address: 103 | try: 104 | wifi.radio.connect(ssid, password) 105 | except ConnectionError as e: 106 | print("❌ Connection Error:", e) 107 | print("Retrying in 10 seconds") 108 | print("✅ WiFi!") 109 | 110 | if top_nvm is not Refresh_Token and FIRST_RUN is False: 111 | FIRST_RUN = False 112 | Refresh_Token = microcontroller.nvm[0:64].decode() 113 | print(" | INDEFINITE RUN -------") 114 | if DEBUG: 115 | print("Top NVM is Fitbit First Refresh Token") 116 | # NVM 64 should match Current Refresh Token 117 | print(f"NVM 64: {microcontroller.nvm[0:64].decode()}") 118 | print(f"Current Refresh_Token: {Refresh_Token}") 119 | if top_nvm != Fitbit_First_Refresh_Token and FIRST_RUN is True: 120 | if top_nvm_3bytes == b"\x00\x00\x00": 121 | print(" | TOP NVM IS BRAND NEW! WAITING FOR A FIRST TOKEN") 122 | Refresh_Token = Fitbit_First_Refresh_Token 123 | nvmtoken = b"" + Refresh_Token 124 | microcontroller.nvm[0:64] = nvmtoken 125 | else: 126 | if DEBUG: 127 | print(f"Top NVM: {top_nvm}") 128 | print(f"First Refresh: {Refresh_Token}") 129 | print(f"First Run: {FIRST_RUN}") 130 | Refresh_Token = top_nvm 131 | FIRST_RUN = False 132 | print(" | MANUAL REBOOT TOKEN DIFFERENCE -------") 133 | if DEBUG: 134 | # NVM 64 should not match Current Refresh Token 135 | print("Top NVM is NOT Fitbit First Refresh Token") 136 | print(f"NVM 64: {microcontroller.nvm[0:64].decode()}") 137 | print(f"Current Refresh_Token: {Refresh_Token}") 138 | if top_nvm == Refresh_Token and FIRST_RUN is True: 139 | if DEBUG: 140 | print(f"Top NVM: {top_nvm}") 141 | print(f"First Refresh: {Refresh_Token}") 142 | print(f"First Run: {FIRST_RUN}") 143 | Refresh_Token = Fitbit_First_Refresh_Token 144 | nvmtoken = b"" + Refresh_Token 145 | microcontroller.nvm[0:64] = nvmtoken 146 | FIRST_RUN = False 147 | print(" | FIRST RUN SETTINGS.TOML TOKEN-------") 148 | if DEBUG: 149 | # NVM 64 should match Current Refresh Token 150 | print("Top NVM IS Fitbit First Refresh Token") 151 | print(f"NVM 64: {microcontroller.nvm[0:64].decode()}") 152 | print(f"Current Refresh_Token: {Refresh_Token}") 153 | try: 154 | if DEBUG: 155 | print("\n-----Token Refresh POST Attempt -------") 156 | FITBIT_OAUTH_REFRESH_TOKEN = ( 157 | "&grant_type=refresh_token" 158 | + "&client_id=" 159 | + str(Fitbit_ClientID) 160 | + "&refresh_token=" 161 | + str(Refresh_Token) 162 | ) 163 | 164 | # ------------------------- POST FOR REFRESH TOKEN -------------------- 165 | print(" | Requesting authorization for next token") 166 | if DEBUG: 167 | print( 168 | "FULL REFRESH TOKEN POST:" + f"{FITBIT_OAUTH_TOKEN}{FITBIT_OAUTH_REFRESH_TOKEN}" 169 | ) 170 | print(f"Current Refresh Token: {Refresh_Token}") 171 | # TOKEN REFRESH POST 172 | try: 173 | with requests.post( 174 | url=FITBIT_OAUTH_TOKEN, 175 | data=FITBIT_OAUTH_REFRESH_TOKEN, 176 | headers=FITBIT_OAUTH_HEADER, 177 | ) as fitbit_oauth_refresh_POST: 178 | fitbit_refresh_oauth_json = fitbit_oauth_refresh_POST.json() 179 | except adafruit_requests.OutOfRetries as ex: 180 | print(f"OutOfRetries: {ex}") 181 | break 182 | try: 183 | fitbit_new_token = fitbit_refresh_oauth_json["access_token"] 184 | if DEBUG: 185 | print("Your Private SHA-256 Token: ", fitbit_new_token) 186 | fitbit_access_token = fitbit_new_token # NEW FULL TOKEN 187 | 188 | # Overwrites Initial/Old Refresh Token with Next/New Refresh Token 189 | fitbit_new_refesh_token = fitbit_refresh_oauth_json["refresh_token"] 190 | Refresh_Token = fitbit_new_refesh_token 191 | 192 | fitbit_token_expiration = fitbit_refresh_oauth_json["expires_in"] 193 | fitbit_scope = fitbit_refresh_oauth_json["scope"] 194 | fitbit_token_type = fitbit_refresh_oauth_json["token_type"] 195 | fitbit_user_id = fitbit_refresh_oauth_json["user_id"] 196 | if DEBUG: 197 | print("Next Refresh Token: ", Refresh_Token) 198 | try: 199 | # Stores Next token in NVM 200 | nvmtoken = b"" + Refresh_Token 201 | microcontroller.nvm[0:64] = nvmtoken 202 | if DEBUG: 203 | print(f"nvmtoken: {nvmtoken}") 204 | # It's better to always have next token visible. 205 | # You can manually set this token into settings.toml 206 | print(f" | Next Token: {nvmtoken.decode()}") 207 | print(" | 🔑 Next token written to NVM Successfully!") 208 | except OSError as e: 209 | print("OS Error:", e) 210 | continue 211 | if DEBUG: 212 | print("Token Expires in: ", time_calc(fitbit_token_expiration)) 213 | print("Scope: ", fitbit_scope) 214 | print("Token Type: ", fitbit_token_type) 215 | print("UserID: ", fitbit_user_id) 216 | except KeyError as e: 217 | print("Key Error:", e) 218 | print("Expired token, invalid permission, or (key:value) pair error.") 219 | time.sleep(SLEEP_TIME) 220 | continue 221 | # ----------------------------- GET DATA --------------------------------- 222 | # Now that we have POST response with next refresh token we can GET for data 223 | # 64-bit Refresh tokens will "keep alive" SHA-256 token indefinitely 224 | # Fitbit main SHA-256 token expires in 8 hours unless refreshed! 225 | # ------------------------------------------------------------------------ 226 | DETAIL_LEVEL = "1min" # Supported: 1sec | 1min | 5min | 15min 227 | REQUESTED_DATE = "today" # Date format yyyy-MM-dd or "today" 228 | fitbit_header = { 229 | "Authorization": "Bearer " + fitbit_access_token + "", 230 | "Client-Id": "" + Fitbit_ClientID + "", 231 | } 232 | # Heart Intraday Scope 233 | FITBIT_INTRADAY_SOURCE = ( 234 | "https://api.fitbit.com/1/user/" 235 | + Fitbit_UserID 236 | + "/activities/heart/date/" 237 | + REQUESTED_DATE 238 | + "/1d/" 239 | + DETAIL_LEVEL 240 | + ".json" 241 | ) 242 | # Device Details 243 | FITBIT_DEVICE_SOURCE = ( 244 | "https://api.fitbit.com/1/user/" + Fitbit_UserID + "/devices.json" 245 | ) 246 | 247 | print(" | Attempting to GET Fitbit JSON!") 248 | FBIS = FITBIT_INTRADAY_SOURCE 249 | FBH = fitbit_header 250 | try: 251 | with requests.get(url=FBIS, headers=FBH) as fitbit_get_response: 252 | fitbit_json = fitbit_get_response.json() 253 | except ConnectionError as e: 254 | print("Connection Error:", e) 255 | print("Retrying in 10 seconds") 256 | print(" | ✅ Fitbit Intraday JSON!") 257 | 258 | if DEBUG: 259 | print(f"Full API GET URL: {FBIS}") 260 | print(f"Header: {fitbit_header}") 261 | # This might crash your microcontroller. 262 | # Commented out even in debug. Use only if absolutely necessary. 263 | 264 | # print(f"JSON Full Response: {fitbit_json}") 265 | Intraday_Response = fitbit_json["activities-heart-intraday"]["dataset"] 266 | # print(f"Intraday Full Response: {Intraday_Response}") 267 | try: 268 | # Fitbit's sync to mobile device & server every 15 minutes in chunks. 269 | # Pointless to poll their API faster than 15 minute intervals. 270 | activities_heart_value = fitbit_json["activities-heart-intraday"]["dataset"] 271 | if MIDNIGHT_DEBUG: 272 | RESPONSE_LENGTH = 0 273 | else: 274 | RESPONSE_LENGTH = len(activities_heart_value) 275 | if RESPONSE_LENGTH >= 15: 276 | activities_timestamp = fitbit_json["activities-heart"][0]["dateTime"] 277 | print(f" | | Fitbit Date: {activities_timestamp}") 278 | if MIDNIGHT_DEBUG: 279 | ACTIVITIES_LATEST_HEART_TIME = "00:05:00" 280 | else: 281 | ACTIVITIES_LATEST_HEART_TIME = fitbit_json["activities-heart-intraday"][ 282 | "dataset" 283 | ][RESPONSE_LENGTH - 1]["time"] 284 | print(f" | | Fitbit Time: {ACTIVITIES_LATEST_HEART_TIME[0:-3]}") 285 | print(f" | | Today's Logged Pulses: {RESPONSE_LENGTH}") 286 | 287 | # Each 1min heart rate is a 60 second average 288 | LATEST_15_AVG = " | | Latest 15 Minute Averages: " 289 | LATEST_15_VALUES = ", ".join( 290 | str(activities_heart_value[i]["value"]) 291 | for i in range(RESPONSE_LENGTH - 1, RESPONSE_LENGTH - 16, -1) 292 | ) 293 | print(f"{LATEST_15_AVG}{LATEST_15_VALUES}") 294 | else: 295 | print(" | Waiting for latest sync...") 296 | print(" | ❌ Not enough values for today to display yet.") 297 | except KeyError as keyerror: 298 | print(f"Key Error: {keyerror}") 299 | print( 300 | "Too Many Requests, " 301 | + "Expired token, " 302 | + "invalid permission, " 303 | + "or (key:value) pair error." 304 | ) 305 | time.sleep(60) 306 | continue 307 | # Getting Fitbit Device JSON (separate from intraday) 308 | # Separate call for Watch Battery Percentage. 309 | print(" | Attempting to GET Device JSON!") 310 | FBDS = FITBIT_DEVICE_SOURCE 311 | FBH = fitbit_header 312 | try: 313 | with requests.get(url=FBDS, headers=FBH) as fitbit_get_device_response: 314 | fitbit_device_json = fitbit_get_device_response.json() 315 | except ConnectionError as e: 316 | print("Connection Error:", e) 317 | print("Retrying in 10 seconds") 318 | print(" | ✅ Fitbit Device JSON!") 319 | 320 | if DEBUG_DEVICE: 321 | print(f"Full API GET URL: {FITBIT_DEVICE_SOURCE}") 322 | print(f"Header: {fitbit_header}") 323 | print(f"JSON Full Response: {fitbit_device_json}") 324 | Device_Response = fitbit_device_json[1]["batteryLevel"] 325 | print(f" | | Watch Battery %: {Device_Response}") 326 | 327 | print("\nFinished!") 328 | print(f"Board Uptime: {time_calc(time.monotonic())}") 329 | print(f"Next Update: {time_calc(SLEEP_TIME)}") 330 | print("===============================") 331 | except (ValueError, RuntimeError) as e: 332 | print("Failed to get data, retrying\n", e) 333 | time.sleep(60) 334 | continue 335 | time.sleep(SLEEP_TIME) 336 | else: 337 | print("🚮 NVM Cleared!") 338 | print( 339 | "⚠️ Save your new access token & refresh token from " 340 | "Fitbits Tutorial (Step 4) to settings.toml now." 341 | ) 342 | print( 343 | "⚠️ If the script runs again" 344 | "(due to settings.toml file save) while reset=True that's ok!" 345 | ) 346 | print("⚠️ Then set RESET_NVM back to False.") 347 | break 348 | -------------------------------------------------------------------------------- /examples/wifi/expanded/requests_wifi_api_github.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 DJDevon3 2 | # SPDX-License-Identifier: MIT 3 | # Coded for Circuit Python 9.x 4 | """Github API Example""" 5 | 6 | import os 7 | import time 8 | 9 | import adafruit_connection_manager 10 | import wifi 11 | 12 | import adafruit_requests 13 | 14 | # Github developer token required. 15 | username = os.getenv("GITHUB_USERNAME") 16 | token = os.getenv("GITHUB_TOKEN") 17 | 18 | # Get WiFi details, ensure these are setup in settings.toml 19 | ssid = os.getenv("CIRCUITPY_WIFI_SSID") 20 | password = os.getenv("CIRCUITPY_WIFI_PASSWORD") 21 | 22 | # API Polling Rate 23 | # 900 = 15 mins, 1800 = 30 mins, 3600 = 1 hour 24 | SLEEP_TIME = 900 25 | 26 | # Set debug to True for full JSON response. 27 | # WARNING: may include visible credentials 28 | DEBUG = False 29 | 30 | # Initalize Wifi, Socket Pool, Request Session 31 | pool = adafruit_connection_manager.get_radio_socketpool(wifi.radio) 32 | ssl_context = adafruit_connection_manager.get_radio_ssl_context(wifi.radio) 33 | requests = adafruit_requests.Session(pool, ssl_context) 34 | 35 | GITHUB_HEADER = {"Authorization": " token " + token} 36 | GITHUB_SOURCE = "https://api.github.com/users/" + username 37 | 38 | 39 | def time_calc(input_time): 40 | """Converts seconds to minutes/hours/days""" 41 | if input_time < 60: 42 | return f"{input_time:.0f} seconds" 43 | if input_time < 3600: 44 | return f"{input_time / 60:.0f} minutes" 45 | if input_time < 86400: 46 | return f"{input_time / 60 / 60:.0f} hours" 47 | return f"{input_time / 60 / 60 / 24:.1f} days" 48 | 49 | 50 | while True: 51 | # Connect to Wi-Fi 52 | print("\nConnecting to WiFi...") 53 | while not wifi.radio.ipv4_address: 54 | try: 55 | wifi.radio.connect(ssid, password) 56 | except ConnectionError as e: 57 | print("❌ Connection Error:", e) 58 | print("Retrying in 10 seconds") 59 | print("✅ Wifi!") 60 | 61 | try: 62 | print(" | Attempting to GET Github JSON!") 63 | try: 64 | with requests.get(url=GITHUB_SOURCE, headers=GITHUB_HEADER) as github_response: 65 | github_json = github_response.json() 66 | except ConnectionError as e: 67 | print("Connection Error:", e) 68 | print("Retrying in 10 seconds") 69 | print(" | ✅ Github JSON!") 70 | 71 | github_joined = github_json["created_at"] 72 | print(" | | Join Date: ", github_joined) 73 | 74 | github_id = github_json["id"] 75 | print(" | | UserID: ", github_id) 76 | 77 | github_location = github_json["location"] 78 | print(" | | Location: ", github_location) 79 | 80 | github_name = github_json["name"] 81 | print(" | | Username: ", github_name) 82 | 83 | github_repos = github_json["public_repos"] 84 | print(" | | Respositores: ", github_repos) 85 | 86 | github_followers = github_json["followers"] 87 | print(" | | Followers: ", github_followers) 88 | github_bio = github_json["bio"] 89 | print(" | | Bio: ", github_bio) 90 | 91 | if DEBUG: 92 | print("Full API GET URL: ", GITHUB_SOURCE) 93 | print(github_json) 94 | 95 | print("\nFinished!") 96 | print(f"Board Uptime: {time_calc(time.monotonic())}") 97 | print(f"Next Update: {time_calc(SLEEP_TIME)}") 98 | print("===============================") 99 | 100 | except (ValueError, RuntimeError) as e: 101 | print(f"Failed to get data, retrying\n {e}") 102 | time.sleep(60) 103 | break 104 | time.sleep(SLEEP_TIME) 105 | -------------------------------------------------------------------------------- /examples/wifi/expanded/requests_wifi_api_mastodon.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 DJDevon3 2 | # SPDX-License-Identifier: MIT 3 | # Coded for Circuit Python 8.2.x 4 | """Mastodon API Example""" 5 | 6 | import os 7 | import time 8 | 9 | import adafruit_connection_manager 10 | import wifi 11 | 12 | import adafruit_requests 13 | 14 | # Mastodon V1 API - Public access (no dev creds or app required) 15 | # Visit https://docs.joinmastodon.org/client/public/ for API docs 16 | # For finding your Mastodon numerical UserID 17 | # Example: https://mastodon.YOURSERVER/api/v1/accounts/lookup?acct=YourUserName 18 | 19 | MASTODON_SERVER = "mastodon.social" # Set server instance 20 | MASTODON_USERID = "000000000000000000" # Numerical UserID you want endpoints from 21 | # Test in browser first, this will pull up a JSON webpage 22 | # https://mastodon.YOURSERVER/api/v1/accounts/YOURUSERIDHERE/statuses?limit=1 23 | 24 | # Get WiFi details, ensure these are setup in settings.toml 25 | ssid = os.getenv("CIRCUITPY_WIFI_SSID") 26 | password = os.getenv("CIRCUITPY_WIFI_PASSWORD") 27 | 28 | # API Polling Rate 29 | # 900 = 15 mins, 1800 = 30 mins, 3600 = 1 hour 30 | SLEEP_TIME = 900 31 | 32 | # Initalize Wifi, Socket Pool, Request Session 33 | pool = adafruit_connection_manager.get_radio_socketpool(wifi.radio) 34 | ssl_context = adafruit_connection_manager.get_radio_ssl_context(wifi.radio) 35 | requests = adafruit_requests.Session(pool, ssl_context) 36 | 37 | 38 | def time_calc(input_time): 39 | """Converts seconds to minutes/hours/days""" 40 | if input_time < 60: 41 | return f"{input_time:.0f} seconds" 42 | if input_time < 3600: 43 | return f"{input_time / 60:.0f} minutes" 44 | if input_time < 86400: 45 | return f"{input_time / 60 / 60:.0f} hours" 46 | return f"{input_time / 60 / 60 / 24:.1f} days" 47 | 48 | 49 | # Publicly available data no header required 50 | MAST_SOURCE = ( 51 | "https://" + MASTODON_SERVER + "/api/v1/accounts/" + MASTODON_USERID + "/statuses?limit=1" 52 | ) 53 | 54 | while True: 55 | # Connect to Wi-Fi 56 | print("\nConnecting to WiFi...") 57 | while not wifi.radio.ipv4_address: 58 | try: 59 | wifi.radio.connect(ssid, password) 60 | except ConnectionError as e: 61 | print("❌ Connection Error:", e) 62 | print("Retrying in 10 seconds") 63 | print("✅ Wifi!") 64 | try: 65 | # Print Request to Serial 66 | print(" | Attempting to GET MASTODON JSON!") 67 | 68 | # Set debug to True for full JSON response. 69 | # WARNING: may include visible credentials 70 | # MICROCONTROLLER WARNING: might crash by returning too much data 71 | DEBUG_RESPONSE = False 72 | 73 | try: 74 | with requests.get(url=MAST_SOURCE) as mastodon_response: 75 | mastodon_json = mastodon_response.json() 76 | except ConnectionError as e: 77 | print(f"Connection Error: {e}") 78 | print("Retrying in 10 seconds") 79 | mastodon_json = mastodon_json[0] 80 | print(" | ✅ Mastodon JSON!") 81 | 82 | if DEBUG_RESPONSE: 83 | print(" | | Full API GET URL: ", MAST_SOURCE) 84 | mastodon_userid = mastodon_json["account"]["id"] 85 | print(f" | | User ID: {mastodon_userid}") 86 | print(mastodon_json) 87 | 88 | mastodon_name = mastodon_json["account"]["display_name"] 89 | print(f" | | Name: {mastodon_name}") 90 | mastodon_join_date = mastodon_json["account"]["created_at"] 91 | print(f" | | Member Since: {mastodon_join_date}") 92 | mastodon_follower_count = mastodon_json["account"]["followers_count"] 93 | print(f" | | Followers: {mastodon_follower_count}") 94 | mastodon_following_count = mastodon_json["account"]["following_count"] 95 | print(f" | | Following: {mastodon_following_count}") 96 | mastodon_toot_count = mastodon_json["account"]["statuses_count"] 97 | print(f" | | Toots: {mastodon_toot_count}") 98 | mastodon_last_toot = mastodon_json["account"]["last_status_at"] 99 | print(f" | | Last Toot: {mastodon_last_toot}") 100 | mastodon_bio = mastodon_json["account"]["note"] 101 | print(f" | | Bio: {mastodon_bio[3:-4]}") # removes included html "

&

" 102 | 103 | print("\nFinished!") 104 | print(f"Board Uptime: {time.monotonic()}") 105 | print(f"Next Update: {time_calc(SLEEP_TIME)}") 106 | print("===============================") 107 | 108 | except (ValueError, RuntimeError) as e: 109 | print(f"Failed to get data, retrying\n {e}") 110 | time.sleep(60) 111 | break 112 | time.sleep(SLEEP_TIME) 113 | -------------------------------------------------------------------------------- /examples/wifi/expanded/requests_wifi_api_openskynetwork_private.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 DJDevon3 2 | # SPDX-License-Identifier: MIT 3 | # Coded for Circuit Python 9.x 4 | """OpenSky-Network.org Single Flight Private API Example""" 5 | 6 | import binascii 7 | import os 8 | import time 9 | 10 | import adafruit_connection_manager 11 | import wifi 12 | 13 | import adafruit_requests 14 | 15 | # OpenSky-Network.org Login required for this API 16 | # REST API: https://openskynetwork.github.io/opensky-api/rest.html 17 | # All active flights JSON: https://opensky-network.org/api/states/all # PICK ONE! :) 18 | # JSON order: transponder, callsign, country 19 | # ACTIVE transpondes only, for multiple "c822af&icao24=cb3993&icao24=c63923" 20 | TRANSPONDER = "4b1812" 21 | 22 | # Get WiFi details, ensure these are setup in settings.toml 23 | ssid = os.getenv("CIRCUITPY_WIFI_SSID") 24 | password = os.getenv("CIRCUITPY_WIFI_PASSWORD") 25 | osnusername = os.getenv("OSN_USERNAME") # Website Credentials 26 | osnpassword = os.getenv("OSN_PASSWORD") # Website Credentials 27 | 28 | # API Polling Rate 29 | # 900 = 15 mins, 1800 = 30 mins, 3600 = 1 hour 30 | # OpenSky-Networks IP bans for too many requests, check rate limit. 31 | # https://openskynetwork.github.io/opensky-api/rest.html#limitations 32 | SLEEP_TIME = 1800 33 | 34 | # Set debug to True for full JSON response. 35 | # WARNING: makes credentials visible 36 | DEBUG = False 37 | 38 | # Initalize Wifi, Socket Pool, Request Session 39 | pool = adafruit_connection_manager.get_radio_socketpool(wifi.radio) 40 | ssl_context = adafruit_connection_manager.get_radio_ssl_context(wifi.radio) 41 | requests = adafruit_requests.Session(pool, ssl_context) 42 | 43 | # -- Base64 Conversion -- 44 | OSN_CREDENTIALS = str(osnusername) + ":" + str(osnpassword) 45 | # base64 encode and strip appended \n from bytearray 46 | OSN_CREDENTIALS_B = binascii.b2a_base64(OSN_CREDENTIALS.encode()).strip() 47 | BASE64_STRING = OSN_CREDENTIALS_B.decode() # bytearray 48 | 49 | 50 | if DEBUG: 51 | print("Base64 ByteArray: ", BASE64_STRING) 52 | 53 | # Requests URL - icao24 is their endpoint required for a transponder 54 | # example https://opensky-network.org/api/states/all?icao24=a808c5 55 | # OSN private: requires your website username:password to be base64 encoded 56 | OPENSKY_HEADER = {"Authorization": "Basic " + BASE64_STRING} 57 | OPENSKY_SOURCE = "https://opensky-network.org/api/states/all?" + "icao24=" + TRANSPONDER 58 | 59 | 60 | def time_calc(input_time): 61 | """Converts seconds to minutes/hours/days""" 62 | if input_time < 60: 63 | return f"{input_time:.0f} seconds" 64 | if input_time < 3600: 65 | return f"{input_time / 60:.0f} minutes" 66 | if input_time < 86400: 67 | return f"{input_time / 60 / 60:.0f} hours" 68 | return f"{input_time / 60 / 60 / 24:.1f} days" 69 | 70 | 71 | def _format_datetime(datetime): 72 | return ( 73 | f"{datetime.tm_mon:02}/{datetime.tm_mday:02}/{datetime.tm_year} " 74 | f"{datetime.tm_hour:02}:{datetime.tm_min:02}:{datetime.tm_sec:02}" 75 | ) 76 | 77 | 78 | while True: 79 | # Connect to Wi-Fi 80 | print("\nConnecting to WiFi...") 81 | while not wifi.radio.ipv4_address: 82 | try: 83 | wifi.radio.connect(ssid, password) 84 | except ConnectionError as e: 85 | print("❌ Connection Error:", e) 86 | print("Retrying in 10 seconds") 87 | print("✅ Wifi!") 88 | 89 | try: 90 | print(" | Attempting to GET OpenSky-Network Single Private Flight JSON!") 91 | print(" | Website Credentials Required! Allows more daily calls than Public.") 92 | try: 93 | with requests.get(url=OPENSKY_SOURCE, headers=OPENSKY_HEADER) as opensky_response: 94 | opensky_json = opensky_response.json() 95 | except ConnectionError as e: 96 | print("Connection Error:", e) 97 | print("Retrying in 10 seconds") 98 | 99 | print(" | ✅ OpenSky-Network JSON!") 100 | 101 | if DEBUG: 102 | print("Full API GET URL: ", OPENSKY_SOURCE) 103 | print("Full API GET Header: ", OPENSKY_HEADER) 104 | print(opensky_json) 105 | 106 | # ERROR MESSAGE RESPONSES 107 | if "timestamp" in opensky_json: 108 | osn_timestamp = opensky_json["timestamp"] 109 | print(f"❌ Timestamp: {osn_timestamp}") 110 | 111 | if "message" in opensky_json: 112 | osn_message = opensky_json["message"] 113 | print(f"❌ Message: {osn_message}") 114 | 115 | if "error" in opensky_json: 116 | osn_error = opensky_json["error"] 117 | print(f"❌ Error: {osn_error}") 118 | 119 | if "path" in opensky_json: 120 | osn_path = opensky_json["path"] 121 | print(f"❌ Path: {osn_path}") 122 | 123 | if "status" in opensky_json: 124 | osn_status = opensky_json["status"] 125 | print(f"❌ Status: {osn_status}") 126 | 127 | # Current flight data for single callsign (right now) 128 | osn_single_flight_data = opensky_json["states"] 129 | 130 | if osn_single_flight_data is not None: 131 | if DEBUG: 132 | print(f" | | Single Private Flight Data: {osn_single_flight_data}") 133 | 134 | last_contact = opensky_json["states"][0][4] 135 | # print(f" | | Last Contact Unix Time: {last_contact}") 136 | lc_struct_time = time.localtime(last_contact) 137 | lc_readable_time = f"{_format_datetime(lc_struct_time)}" 138 | print(f" | | Last Contact: {lc_readable_time}") 139 | 140 | flight_transponder = opensky_json["states"][0][0] 141 | print(f" | | Transponder: {flight_transponder}") 142 | 143 | callsign = opensky_json["states"][0][1] 144 | print(f" | | Callsign: {callsign}") 145 | 146 | squawk = opensky_json["states"][0][14] 147 | print(f" | | Squawk: {squawk}") 148 | 149 | country = opensky_json["states"][0][2] 150 | print(f" | | Origin: {country}") 151 | 152 | longitude = opensky_json["states"][0][5] 153 | print(f" | | Longitude: {longitude}") 154 | 155 | latitude = opensky_json["states"][0][6] 156 | print(f" | | Latitude: {latitude}") 157 | 158 | # Return Air Flight data if not on ground 159 | on_ground = opensky_json["states"][0][8] 160 | if on_ground is True: 161 | print(f" | | On Ground: {on_ground}") 162 | else: 163 | altitude = opensky_json["states"][0][7] 164 | print(f" | | Barometric Altitude: {altitude}") 165 | 166 | velocity = opensky_json["states"][0][9] 167 | if velocity != "null": 168 | print(f" | | Velocity: {velocity}") 169 | 170 | vertical_rate = opensky_json["states"][0][11] 171 | if vertical_rate != "null": 172 | print(f" | | Vertical Rate: {vertical_rate}") 173 | 174 | else: 175 | print(" | | ❌ Flight has no active data or you're polling too fast.") 176 | 177 | print("\nFinished!") 178 | print(f"Board Uptime: {time_calc(time.monotonic())}") 179 | print(f"Next Update: {time_calc(SLEEP_TIME)}") 180 | print("===============================") 181 | 182 | except (ValueError, RuntimeError) as e: 183 | print(f"Failed to get data, retrying\n {e}") 184 | time.sleep(60) 185 | break 186 | time.sleep(SLEEP_TIME) 187 | -------------------------------------------------------------------------------- /examples/wifi/expanded/requests_wifi_api_openskynetwork_private_area.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 DJDevon3 2 | # SPDX-License-Identifier: MIT 3 | # Coded for Circuit Python 8.2.x 4 | """OpenSky-Network.org Private Area API Example""" 5 | 6 | import binascii 7 | import os 8 | import time 9 | 10 | import adafruit_connection_manager 11 | import wifi 12 | 13 | import adafruit_requests 14 | 15 | # OpenSky-Network.org Website Login required for this API 16 | # Increased call limit vs Public. 17 | # REST API: https://openskynetwork.github.io/opensky-api/rest.html 18 | # Retrieves all traffic within a geographic area (Orlando example) 19 | LATMIN = "27.22" # east bounding box 20 | LATMAX = "28.8" # west bounding box 21 | LONMIN = "-81.46" # north bounding box 22 | LONMAX = "-80.40" # south bounding box 23 | 24 | # Get WiFi details, ensure these are setup in settings.toml 25 | ssid = os.getenv("CIRCUITPY_WIFI_SSID") 26 | password = os.getenv("CIRCUITPY_WIFI_PASSWORD") 27 | osnusername = os.getenv("OSN_USERNAME") # Website Credentials 28 | osnpassword = os.getenv("OSN_PASSWORD") # Website Credentials 29 | 30 | # API Polling Rate 31 | # 900 = 15 mins, 1800 = 30 mins, 3600 = 1 hour 32 | # OpenSky-Networks IP bans for too many requests, check rate limit. 33 | # https://openskynetwork.github.io/opensky-api/rest.html#limitations 34 | SLEEP_TIME = 1800 35 | 36 | # Set debug to True for full JSON response. 37 | # WARNING: makes credentials visible. based on how many flights 38 | # in your area, full response could crash microcontroller 39 | DEBUG = False 40 | 41 | # Initalize Wifi, Socket Pool, Request Session 42 | pool = adafruit_connection_manager.get_radio_socketpool(wifi.radio) 43 | ssl_context = adafruit_connection_manager.get_radio_ssl_context(wifi.radio) 44 | requests = adafruit_requests.Session(pool, ssl_context) 45 | 46 | # -- Base64 Conversion -- 47 | OSN_CREDENTIALS = str(osnusername) + ":" + str(osnpassword) 48 | # base64 encode and strip appended \n from bytearray 49 | OSN_CREDENTIALS_B = binascii.b2a_base64(OSN_CREDENTIALS.encode()).strip() 50 | BASE64_STRING = OSN_CREDENTIALS_B.decode() # bytearray 51 | 52 | if DEBUG: 53 | print("Base64 ByteArray: ", BASE64_STRING) 54 | 55 | # Area requires OpenSky-Network.org username:password to be base64 encoded 56 | OSN_HEADER = {"Authorization": "Basic " + BASE64_STRING} 57 | 58 | # Example request of all traffic over Florida. 59 | # Geographic areas calls cost less against the limit. 60 | # https://opensky-network.org/api/states/all?lamin=25.21&lomin=-84.36&lamax=30.0&lomax=-78.40 61 | OPENSKY_SOURCE = ( 62 | "https://opensky-network.org/api/states/all?" 63 | + "lamin=" 64 | + LATMIN 65 | + "&lomin=" 66 | + LONMIN 67 | + "&lamax=" 68 | + LATMAX 69 | + "&lomax=" 70 | + LONMAX 71 | ) 72 | 73 | 74 | def time_calc(input_time): 75 | """Converts seconds to minutes/hours/days""" 76 | if input_time < 60: 77 | return f"{input_time:.0f} seconds" 78 | if input_time < 3600: 79 | return f"{input_time / 60:.0f} minutes" 80 | if input_time < 86400: 81 | return f"{input_time / 60 / 60:.0f} hours" 82 | return f"{input_time / 60 / 60 / 24:.1f} days" 83 | 84 | 85 | def _format_datetime(datetime): 86 | return ( 87 | f"{datetime.tm_mon:02}/{datetime.tm_mday:02}/{datetime.tm_year} " 88 | f"{datetime.tm_hour:02}:{datetime.tm_min:02}:{datetime.tm_sec:02}" 89 | ) 90 | 91 | 92 | while True: 93 | # Connect to Wi-Fi 94 | print("\nConnecting to WiFi...") 95 | while not wifi.radio.ipv4_address: 96 | try: 97 | wifi.radio.connect(ssid, password) 98 | except ConnectionError as e: 99 | print("❌ Connection Error:", e) 100 | print("Retrying in 10 seconds") 101 | print("✅ Wifi!") 102 | 103 | try: 104 | print(" | Attempting to GET OpenSky-Network Area Flights JSON!") 105 | try: 106 | with requests.get(url=OPENSKY_SOURCE, headers=OSN_HEADER) as opensky_response: 107 | opensky_json = opensky_response.json() 108 | except ConnectionError as e: 109 | print("Connection Error:", e) 110 | print("Retrying in 10 seconds") 111 | 112 | print(" | ✅ OpenSky-Network JSON!") 113 | 114 | if DEBUG: 115 | print("Full API GET URL: ", OPENSKY_SOURCE) 116 | print(opensky_json) 117 | 118 | # ERROR MESSAGE RESPONSES 119 | if "timestamp" in opensky_json: 120 | osn_timestamp = opensky_json["timestamp"] 121 | print(f"❌ Timestamp: {osn_timestamp}") 122 | 123 | if "message" in opensky_json: 124 | osn_message = opensky_json["message"] 125 | print(f"❌ Message: {osn_message}") 126 | 127 | if "error" in opensky_json: 128 | osn_error = opensky_json["error"] 129 | print(f"❌ Error: {osn_error}") 130 | 131 | if "path" in opensky_json: 132 | osn_path = opensky_json["path"] 133 | print(f"❌ Path: {osn_path}") 134 | 135 | if "status" in opensky_json: 136 | osn_status = opensky_json["status"] 137 | print(f"❌ Status: {osn_status}") 138 | 139 | # Current flight data for single callsign (right now) 140 | osn_all_flights = opensky_json["states"] 141 | 142 | if osn_all_flights is not None: 143 | if DEBUG: 144 | print(f" | | Area Flights Full Response: {osn_all_flights}") 145 | 146 | osn_time = opensky_json["time"] 147 | # print(f" | | Last Contact Unix Time: {osn_time}") 148 | osn_struct_time = time.localtime(osn_time) 149 | osn_readable_time = f"{_format_datetime(osn_struct_time)}" 150 | print(f" | | Timestamp: {osn_readable_time}") 151 | 152 | if osn_all_flights is not None: 153 | # print("Flight Data: ", osn_all_flights) 154 | for flights in osn_all_flights: 155 | osn_t = f" | | Trans:{flights[0]} " 156 | osn_c = f"Sign:{flights[1]}" 157 | osn_o = f"Origin:{flights[2]} " 158 | osn_tm = f"Time:{flights[3]} " 159 | osn_l = f"Last:{flights[4]} " 160 | osn_lo = f"Lon:{flights[5]} " 161 | osn_la = f"Lat:{flights[6]} " 162 | osn_ba = f"BaroAlt:{flights[7]} " 163 | osn_g = f"Ground:{flights[8]} " 164 | osn_v = f"Vel:{flights[9]} " 165 | osn_h = f"Head:{flights[10]} " 166 | osn_vr = f"VertRate:{flights[11]} " 167 | osn_s = f"Sens:{flights[12]} " 168 | osn_ga = f"GeoAlt:{flights[13]} " 169 | osn_sq = f"Squawk:{flights[14]} " 170 | osn_pr = f"Task:{flights[15]} " 171 | osn_ps = f"PosSys:{flights[16]} " 172 | osn_ca = f"Cat:{flights[16]} " 173 | # This is just because pylint complains about long lines 174 | string1 = f"{osn_t}{osn_c}{osn_o}{osn_tm}{osn_l}{osn_lo}" 175 | string2 = f"{osn_la}{osn_ba}{osn_g}{osn_v}{osn_h}{osn_vr}" 176 | string3 = f"{osn_s}{osn_ga}{osn_sq}{osn_pr}{osn_ps}{osn_ca}" 177 | print(f"{string1}{string2}{string3}") 178 | 179 | else: 180 | print(" | | ❌ Area has no active data or you're polling too fast.") 181 | 182 | print("\nFinished!") 183 | print(f"Board Uptime: {time_calc(time.monotonic())}") 184 | print(f"Next Update: {time_calc(SLEEP_TIME)}") 185 | print("===============================") 186 | 187 | except (ValueError, RuntimeError) as e: 188 | print(f"Failed to get data, retrying\n {e}") 189 | time.sleep(60) 190 | break 191 | time.sleep(SLEEP_TIME) 192 | -------------------------------------------------------------------------------- /examples/wifi/expanded/requests_wifi_api_openskynetwork_public.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 DJDevon3 2 | # SPDX-License-Identifier: MIT 3 | # Coded for Circuit Python 9.x 4 | """OpenSky-Network.org Single Flight Public API Example""" 5 | 6 | import os 7 | import time 8 | 9 | import adafruit_connection_manager 10 | import wifi 11 | 12 | import adafruit_requests 13 | 14 | # No login necessary for Public API. Drastically reduced daily limit vs Private 15 | # OpenSky-Networks.org REST API: https://openskynetwork.github.io/opensky-api/rest.html 16 | # All active flights JSON: https://opensky-network.org/api/states/all PICK ONE! 17 | # JSON order: transponder, callsign, country 18 | # ACTIVE transpondes only, for multiple "c822af&icao24=cb3993&icao24=c63923" 19 | TRANSPONDER = "88044d" 20 | 21 | # Get WiFi details, ensure these are setup in settings.toml 22 | ssid = os.getenv("CIRCUITPY_WIFI_SSID") 23 | password = os.getenv("CIRCUITPY_WIFI_PASSWORD") 24 | 25 | # API Polling Rate 26 | # 900 = 15 mins, 1800 = 30 mins, 3600 = 1 hour 27 | # OpenSky-Networks IP bans for too many requests, check rate limit. 28 | # https://openskynetwork.github.io/opensky-api/rest.html#limitations 29 | SLEEP_TIME = 1800 30 | 31 | # Set debug to True for full JSON response. 32 | # WARNING: makes credentials visible 33 | DEBUG = False 34 | 35 | # Initalize Wifi, Socket Pool, Request Session 36 | pool = adafruit_connection_manager.get_radio_socketpool(wifi.radio) 37 | ssl_context = adafruit_connection_manager.get_radio_ssl_context(wifi.radio) 38 | requests = adafruit_requests.Session(pool, ssl_context) 39 | 40 | # Requests URL - icao24 is their endpoint required for a transponder 41 | # example https://opensky-network.org/api/states/all?icao24=a808c5 42 | OPENSKY_SOURCE = "https://opensky-network.org/api/states/all?" + "icao24=" + TRANSPONDER 43 | 44 | 45 | def time_calc(input_time): 46 | """Converts seconds to minutes/hours/days""" 47 | if input_time < 60: 48 | return f"{input_time:.0f} seconds" 49 | if input_time < 3600: 50 | return f"{input_time / 60:.0f} minutes" 51 | if input_time < 86400: 52 | return f"{input_time / 60 / 60:.0f} hours" 53 | return f"{input_time / 60 / 60 / 24:.1f} days" 54 | 55 | 56 | def _format_datetime(datetime): 57 | return ( 58 | f"{datetime.tm_mon:02}/{datetime.tm_mday:02}/{datetime.tm_year} " 59 | f"{datetime.tm_hour:02}:{datetime.tm_min:02}:{datetime.tm_sec:02}" 60 | ) 61 | 62 | 63 | while True: 64 | # Connect to Wi-Fi 65 | print("\nConnecting to WiFi...") 66 | while not wifi.radio.ipv4_address: 67 | try: 68 | wifi.radio.connect(ssid, password) 69 | except ConnectionError as e: 70 | print("❌ Connection Error:", e) 71 | print("Retrying in 10 seconds") 72 | print("✅ Wifi!") 73 | 74 | try: 75 | print(" | Attempting to GET OpenSky-Network Single Public Flight JSON!") 76 | print(" | Website Credentials NOT Required! Less daily calls than Private.") 77 | try: 78 | with requests.get(url=OPENSKY_SOURCE) as opensky_response: 79 | opensky_json = opensky_response.json() 80 | except ConnectionError as e: 81 | print("Connection Error:", e) 82 | print("Retrying in 10 seconds") 83 | 84 | print(" | ✅ OpenSky-Network Public JSON!") 85 | 86 | if DEBUG: 87 | print("Full API GET URL: ", OPENSKY_SOURCE) 88 | print(opensky_json) 89 | 90 | # ERROR MESSAGE RESPONSES 91 | if "timestamp" in opensky_json: 92 | osn_timestamp = opensky_json["timestamp"] 93 | print(f"❌ Timestamp: {osn_timestamp}") 94 | 95 | if "message" in opensky_json: 96 | osn_message = opensky_json["message"] 97 | print(f"❌ Message: {osn_message}") 98 | 99 | if "error" in opensky_json: 100 | osn_error = opensky_json["error"] 101 | print(f"❌ Error: {osn_error}") 102 | 103 | if "path" in opensky_json: 104 | osn_path = opensky_json["path"] 105 | print(f"❌ Path: {osn_path}") 106 | 107 | if "status" in opensky_json: 108 | osn_status = opensky_json["status"] 109 | print(f"❌ Status: {osn_status}") 110 | 111 | # Current flight data for single callsign (right now) 112 | osn_single_flight_data = opensky_json["states"] 113 | 114 | if osn_single_flight_data is not None: 115 | if DEBUG: 116 | print(f" | | Single Flight Public Data: {osn_single_flight_data}") 117 | 118 | last_contact = opensky_json["states"][0][4] 119 | # print(f" | | Last Contact Unix Time: {last_contact}") 120 | lc_struct_time = time.localtime(last_contact) 121 | lc_readable_time = f"{_format_datetime(lc_struct_time)}" 122 | print(f" | | Last Contact: {lc_readable_time}") 123 | 124 | flight_transponder = opensky_json["states"][0][0] 125 | print(f" | | Transponder: {flight_transponder}") 126 | 127 | callsign = opensky_json["states"][0][1] 128 | print(f" | | Callsign: {callsign}") 129 | 130 | squawk = opensky_json["states"][0][14] 131 | print(f" | | Squawk: {squawk}") 132 | 133 | country = opensky_json["states"][0][2] 134 | print(f" | | Origin: {country}") 135 | 136 | longitude = opensky_json["states"][0][5] 137 | print(f" | | Longitude: {longitude}") 138 | 139 | latitude = opensky_json["states"][0][6] 140 | print(f" | | Latitude: {latitude}") 141 | 142 | # Return Air Flight data if not on ground 143 | on_ground = opensky_json["states"][0][8] 144 | if on_ground is True: 145 | print(f" | | On Ground: {on_ground}") 146 | else: 147 | altitude = opensky_json["states"][0][7] 148 | print(f" | | Barometric Altitude: {altitude}") 149 | 150 | velocity = opensky_json["states"][0][9] 151 | if velocity != "null": 152 | print(f" | | Velocity: {velocity}") 153 | 154 | vertical_rate = opensky_json["states"][0][11] 155 | if vertical_rate != "null": 156 | print(f" | | Vertical Rate: {vertical_rate}") 157 | else: 158 | print("This flight has no active data or you're polling too fast.") 159 | print("Public Limits: 10 second max poll & 400 weighted calls daily") 160 | 161 | print("\nFinished!") 162 | print(f"Board Uptime: {time_calc(time.monotonic())}") 163 | print(f"Next Update: {time_calc(SLEEP_TIME)}") 164 | print("===============================") 165 | 166 | except (ValueError, RuntimeError) as e: 167 | print(f"Failed to get data, retrying\n {e}") 168 | time.sleep(60) 169 | break 170 | time.sleep(SLEEP_TIME) 171 | -------------------------------------------------------------------------------- /examples/wifi/expanded/requests_wifi_api_premiereleague.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 DJDevon3 2 | # SPDX-License-Identifier: MIT 3 | # Coded for Circuit Python 8.2.x 4 | """Premiere League Total Players API Example""" 5 | 6 | import os 7 | import time 8 | 9 | import adafruit_connection_manager 10 | import adafruit_json_stream as json_stream 11 | import wifi 12 | 13 | import adafruit_requests 14 | 15 | # Public API. No user or token required 16 | 17 | # Get WiFi details, ensure these are setup in settings.toml 18 | ssid = os.getenv("CIRCUITPY_WIFI_SSID") 19 | password = os.getenv("CIRCUITPY_WIFI_PASSWORD") 20 | 21 | # API Polling Rate 22 | # 900 = 15 mins, 1800 = 30 mins, 3600 = 1 hour 23 | SLEEP_TIME = 900 24 | 25 | # Initalize Wifi, Socket Pool, Request Session 26 | pool = adafruit_connection_manager.get_radio_socketpool(wifi.radio) 27 | ssl_context = adafruit_connection_manager.get_radio_ssl_context(wifi.radio) 28 | requests = adafruit_requests.Session(pool, ssl_context) 29 | 30 | # Publicly available data no header required 31 | PREMIERE_LEAGUE_SOURCE = "https://fantasy.premierleague.com/api/bootstrap-static/" 32 | 33 | 34 | def time_calc(input_time): 35 | """Converts seconds to minutes/hours/days""" 36 | if input_time < 60: 37 | return f"{input_time:.0f} seconds" 38 | if input_time < 3600: 39 | return f"{input_time / 60:.0f} minutes" 40 | if input_time < 86400: 41 | return f"{input_time / 60 / 60:.0f} hours" 42 | return f"{input_time / 60 / 60 / 24:.1f} days" 43 | 44 | 45 | while True: 46 | # Connect to Wi-Fi 47 | print("\nConnecting to WiFi...") 48 | while not wifi.radio.ipv4_address: 49 | try: 50 | wifi.radio.connect(ssid, password) 51 | except ConnectionError as e: 52 | print("❌ Connection Error:", e) 53 | print("Retrying in 10 seconds") 54 | print("✅ Wifi!") 55 | 56 | try: 57 | print(" | Attempting to GET Premiere League JSON!") 58 | 59 | # Set debug to True for full JSON response. 60 | # WARNING: may include visible credentials 61 | # MICROCONTROLLER WARNING: might crash by returning too much data 62 | DEBUG_RESPONSE = False 63 | 64 | try: 65 | with requests.get(url=PREMIERE_LEAGUE_SOURCE) as PREMIERE_LEAGUE_RESPONSE: 66 | pl_json = json_stream.load(PREMIERE_LEAGUE_RESPONSE.iter_content(32)) 67 | except ConnectionError as e: 68 | print(f"Connection Error: {e}") 69 | print("Retrying in 10 seconds") 70 | print(" | ✅ Premiere League JSON!") 71 | 72 | print(f" | Total Premiere League Players: {pl_json['total_players']}") 73 | 74 | print("\nFinished!") 75 | print(f"Board Uptime: {time.monotonic()}") 76 | print(f"Next Update: {time_calc(SLEEP_TIME)}") 77 | print("===============================") 78 | 79 | except (ValueError, RuntimeError) as e: 80 | print(f"Failed to get data, retrying\n {e}") 81 | time.sleep(60) 82 | break 83 | time.sleep(SLEEP_TIME) 84 | -------------------------------------------------------------------------------- /examples/wifi/expanded/requests_wifi_api_queuetimes.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 DJDevon3 2 | # SPDX-License-Identifier: MIT 3 | # Coded for Circuit Python 9.x 4 | """Queue-Times.com API Example""" 5 | 6 | import os 7 | 8 | import adafruit_connection_manager 9 | import wifi 10 | 11 | import adafruit_requests 12 | 13 | # Initalize Wifi, Socket Pool, Request Session 14 | pool = adafruit_connection_manager.get_radio_socketpool(wifi.radio) 15 | ssl_context = adafruit_connection_manager.get_radio_ssl_context(wifi.radio) 16 | requests = adafruit_requests.Session(pool, ssl_context) 17 | 18 | # Time between API refreshes 19 | # 900 = 15 mins, 1800 = 30 mins, 3600 = 1 hour 20 | SLEEP_TIME = 300 21 | 22 | # Get WiFi details, ensure these are setup in settings.toml 23 | ssid = os.getenv("CIRCUITPY_WIFI_SSID") 24 | password = os.getenv("CIRCUITPY_WIFI_PASSWORD") 25 | 26 | # Publicly Open API (no credentials required) 27 | QTIMES_SOURCE = "https://queue-times.com/parks/16/queue_times.json" 28 | 29 | 30 | def time_calc(input_time): 31 | """Converts seconds to minutes/hours/days""" 32 | if input_time < 60: 33 | return f"{input_time:.0f} seconds" 34 | if input_time < 3600: 35 | return f"{input_time / 60:.0f} minutes" 36 | if input_time < 86400: 37 | return f"{input_time / 60 / 60:.0f} hours" 38 | return f"{input_time / 60 / 60 / 24:.1f} days" 39 | 40 | 41 | qtimes_json = {} 42 | 43 | # Connect to Wi-Fi 44 | print("\n===============================") 45 | print("Connecting to WiFi...") 46 | while not wifi.radio.ipv4_address: 47 | try: 48 | wifi.radio.connect(ssid, password) 49 | except ConnectionError as e: 50 | print("❌ Connection Error:", e) 51 | print("Retrying in 10 seconds") 52 | print("✅ WiFi!") 53 | 54 | try: 55 | with requests.get(url=QTIMES_SOURCE) as qtimes_response: 56 | qtimes_json = qtimes_response.json() 57 | 58 | print(" | ✅ Queue-Times JSON\n") 59 | DEBUG_QTIMES = False 60 | if DEBUG_QTIMES: 61 | print("Full API GET URL: ", QTIMES_SOURCE) 62 | print(qtimes_json) 63 | 64 | # Poll Once and end script 65 | for land in qtimes_json["lands"]: 66 | qtimes_lands = str(land["name"]) 67 | print(f" | Land: {qtimes_lands}") 68 | 69 | # Loop through each ride in the land 70 | for ride in land["rides"]: 71 | qtimes_rides = str(ride["name"]) 72 | qtimes_queuetime = str(ride["wait_time"]) 73 | qtimes_isopen = str(ride["is_open"]) 74 | 75 | print(f" | | Ride: {qtimes_rides}") 76 | print(f" | | Queue Time: {qtimes_queuetime} Minutes") 77 | if qtimes_isopen == "False": 78 | print(" | | Status: Closed\n") 79 | elif qtimes_isopen == "True": 80 | print(" | | Status: Open\n") 81 | else: 82 | print(" | | Status: Unknown\n") 83 | 84 | 85 | except ConnectionError as e: 86 | print("Connection Error:", e) 87 | -------------------------------------------------------------------------------- /examples/wifi/expanded/requests_wifi_api_rocketlaunch_live.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 DJDevon3 2 | # SPDX-License-Identifier: MIT 3 | # Coded for Circuit Python 9.0 4 | """RocketLaunch.Live API Example""" 5 | 6 | import os 7 | import time 8 | 9 | import adafruit_connection_manager 10 | import wifi 11 | 12 | import adafruit_requests 13 | 14 | # Time between API refreshes 15 | # 900 = 15 mins, 1800 = 30 mins, 3600 = 1 hour 16 | SLEEP_TIME = 43200 17 | 18 | # Get WiFi details, ensure these are setup in settings.toml 19 | ssid = os.getenv("CIRCUITPY_WIFI_SSID") 20 | password = os.getenv("CIRCUITPY_WIFI_PASSWORD") 21 | 22 | 23 | def time_calc(input_time): 24 | """Converts seconds to minutes/hours/days""" 25 | if input_time < 60: 26 | return f"{input_time:.0f} seconds" 27 | if input_time < 3600: 28 | return f"{input_time / 60:.0f} minutes" 29 | if input_time < 86400: 30 | return f"{input_time / 60 / 60:.0f} hours" 31 | return f"{input_time / 60 / 60 / 24:.1f} days" 32 | 33 | 34 | # Publicly available data no header required 35 | # The number at the end is the amount of launches (max 5 free api) 36 | ROCKETLAUNCH_SOURCE = "https://fdo.rocketlaunch.live/json/launches/next/1" 37 | 38 | # Initalize Wifi, Socket Pool, Request Session 39 | pool = adafruit_connection_manager.get_radio_socketpool(wifi.radio) 40 | ssl_context = adafruit_connection_manager.get_radio_ssl_context(wifi.radio) 41 | requests = adafruit_requests.Session(pool, ssl_context) 42 | 43 | while True: 44 | # Connect to Wi-Fi 45 | print("\n===============================") 46 | print("Connecting to WiFi...") 47 | while not wifi.radio.ipv4_address: 48 | try: 49 | wifi.radio.connect(ssid, password) 50 | except ConnectionError as e: 51 | print("❌ Connection Error:", e) 52 | print("Retrying in 10 seconds") 53 | print("✅ Wifi!") 54 | try: 55 | # Print Request to Serial 56 | print(" | Attempting to GET RocketLaunch.Live JSON!") 57 | time.sleep(2) 58 | debug_rocketlaunch_full_response = False 59 | 60 | try: 61 | with requests.get(url=ROCKETLAUNCH_SOURCE) as rocketlaunch_response: 62 | rocketlaunch_json = rocketlaunch_response.json() 63 | except ConnectionError as e: 64 | print("Connection Error:", e) 65 | print("Retrying in 10 seconds") 66 | print(" | ✅ RocketLaunch.Live JSON!") 67 | 68 | if debug_rocketlaunch_full_response: 69 | print("Full API GET URL: ", ROCKETLAUNCH_SOURCE) 70 | print(rocketlaunch_json) 71 | 72 | # JSON Endpoints 73 | RLFN = str(rocketlaunch_json["result"][0]["name"]) 74 | RLWO = str(rocketlaunch_json["result"][0]["win_open"]) 75 | TZERO = str(rocketlaunch_json["result"][0]["t0"]) 76 | RLWC = str(rocketlaunch_json["result"][0]["win_close"]) 77 | RLP = str(rocketlaunch_json["result"][0]["provider"]["name"]) 78 | RLVN = str(rocketlaunch_json["result"][0]["vehicle"]["name"]) 79 | RLPN = str(rocketlaunch_json["result"][0]["pad"]["name"]) 80 | RLLS = str(rocketlaunch_json["result"][0]["pad"]["location"]["name"]) 81 | RLLD = str(rocketlaunch_json["result"][0]["launch_description"]) 82 | RLM = str(rocketlaunch_json["result"][0]["mission_description"]) 83 | RLDATE = str(rocketlaunch_json["result"][0]["date_str"]) 84 | 85 | # Print to serial & display label if endpoint not "None" 86 | if RLDATE != "None": 87 | print(f" | | Date: {RLDATE}") 88 | if RLFN != "None": 89 | print(f" | | Flight: {RLFN}") 90 | if RLP != "None": 91 | print(f" | | Provider: {RLP}") 92 | if RLVN != "None": 93 | print(f" | | Vehicle: {RLVN}") 94 | 95 | # Launch time can sometimes be Window Open to Close, T-Zero, or weird combination. 96 | # Should obviously be standardized but they're not input that way. 97 | # Have to account for every combination of 3 conditions. 98 | # T-Zero Launch Time Conditionals 99 | if RLWO == "None" and TZERO != "None" and RLWC != "None": 100 | print(f" | | Window: {TZERO} | {RLWC}") 101 | elif RLWO != "None" and TZERO != "None" and RLWC == "None": 102 | print(f" | | Window: {RLWO} | {TZERO}") 103 | elif RLWO != "None" and TZERO == "None" and RLWC != "None": 104 | print(f" | | Window: {RLWO} | {RLWC}") 105 | elif RLWO != "None" and TZERO != "None" and RLWC != "None": 106 | print(f" | | Window: {RLWO} | {TZERO} | {RLWC}") 107 | elif RLWO == "None" and TZERO != "None" and RLWC == "None": 108 | print(f" | | Window: {TZERO}") 109 | elif RLWO != "None" and TZERO == "None" and RLWC == "None": 110 | print(f" | | Window: {RLWO}") 111 | elif RLWO == "None" and TZERO == "None" and RLWC != "None": 112 | print(f" | | Window: {RLWC}") 113 | 114 | if RLLS != "None": 115 | print(f" | | Site: {RLLS}") 116 | if RLPN != "None": 117 | print(f" | | Pad: {RLPN}") 118 | if RLLD != "None": 119 | print(f" | | Description: {RLLD}") 120 | if RLM != "None": 121 | print(f" | | Mission: {RLM}") 122 | 123 | print("\nFinished!") 124 | print(f"Board Uptime: {time_calc(time.monotonic())}") 125 | print(f"Next Update: {time_calc(SLEEP_TIME)}") 126 | print("===============================") 127 | 128 | except (ValueError, RuntimeError) as e: 129 | print("Failed to get data, retrying\n", e) 130 | time.sleep(60) 131 | break 132 | time.sleep(SLEEP_TIME) 133 | -------------------------------------------------------------------------------- /examples/wifi/expanded/requests_wifi_api_steam.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 DJDevon3 2 | # SPDX-License-Identifier: MIT 3 | # Coded for Circuit Python 8.2.x 4 | """Steam API Get Owned Games Example""" 5 | 6 | import os 7 | import time 8 | 9 | import adafruit_connection_manager 10 | import wifi 11 | 12 | import adafruit_requests 13 | 14 | # Steam API Docs: https://steamcommunity.com/dev 15 | # Steam API Key: https://steamcommunity.com/dev/apikey 16 | # Numerical Steam ID: Visit https://store.steampowered.com/account/ 17 | # Your account name will be in big bold letters. 18 | # Your numerical STEAM ID will be below in a very small font. 19 | 20 | # Get WiFi details, ensure these are setup in settings.toml 21 | ssid = os.getenv("CIRCUITPY_WIFI_SSID") 22 | password = os.getenv("CIRCUITPY_WIFI_PASSWORD") 23 | # Requires Steam Developer API key 24 | steam_usernumber = os.getenv("STEAM_ID") 25 | steam_apikey = os.getenv("STEAM_API_KEY") 26 | 27 | # API Polling Rate 28 | # 900 = 15 mins, 1800 = 30 mins, 3600 = 1 hour 29 | SLEEP_TIME = 3600 30 | 31 | # Set debug to True for full JSON response. 32 | # WARNING: Steam's full response will overload most microcontrollers 33 | # SET TO TRUE IF YOU FEEL BRAVE =) 34 | DEBUG = False 35 | 36 | # Initalize Wifi, Socket Pool, Request Session 37 | pool = adafruit_connection_manager.get_radio_socketpool(wifi.radio) 38 | ssl_context = adafruit_connection_manager.get_radio_ssl_context(wifi.radio) 39 | requests = adafruit_requests.Session(pool, ssl_context) 40 | 41 | # Deconstruct URL (pylint hates long lines) 42 | # http://api.steampowered.com/IPlayerService/GetOwnedGames/v0001/ 43 | # ?key=XXXXXXXXXXXXXXXXXXXXX&include_played_free_games=1&steamid=XXXXXXXXXXXXXXXX&format=json 44 | STEAM_SOURCE = ( 45 | "http://api.steampowered.com/IPlayerService/GetOwnedGames/v0001/?" 46 | + "key=" 47 | + steam_apikey 48 | + "&include_played_free_games=1" 49 | + "&steamid=" 50 | + steam_usernumber 51 | + "&format=json" 52 | ) 53 | 54 | 55 | def time_calc(input_time): 56 | """Converts seconds to minutes/hours/days""" 57 | if input_time < 60: 58 | return f"{input_time:.0f} seconds" 59 | if input_time < 3600: 60 | return f"{input_time / 60:.0f} minutes" 61 | if input_time < 86400: 62 | return f"{input_time / 60 / 60:.0f} hours" 63 | return f"{input_time / 60 / 60 / 24:.1f} days" 64 | 65 | 66 | def _format_datetime(datetime): 67 | """F-String formatted struct time conversion""" 68 | return ( 69 | f"{datetime.tm_mon:02}/" 70 | + f"{datetime.tm_mday:02}/" 71 | + f"{datetime.tm_year:02} " 72 | + f"{datetime.tm_hour:02}:" 73 | + f"{datetime.tm_min:02}:" 74 | + f"{datetime.tm_sec:02}" 75 | ) 76 | 77 | 78 | while True: 79 | # Connect to Wi-Fi 80 | print("\nConnecting to WiFi...") 81 | while not wifi.radio.ipv4_address: 82 | try: 83 | wifi.radio.connect(ssid, password) 84 | except ConnectionError as e: 85 | print("❌ Connection Error:", e) 86 | print("Retrying in 10 seconds") 87 | print("✅ Wifi!") 88 | 89 | try: 90 | print(" | Attempting to GET Steam API JSON!") 91 | try: 92 | with requests.get(url=STEAM_SOURCE) as steam_response: 93 | steam_json = steam_response.json() 94 | except ConnectionError as e: 95 | print("Connection Error:", e) 96 | print("Retrying in 10 seconds") 97 | 98 | print(" | ✅ Steam JSON!") 99 | 100 | if DEBUG: 101 | print("Full API GET URL: ", STEAM_SOURCE) 102 | print(steam_json) 103 | 104 | game_count = steam_json["response"]["game_count"] 105 | print(f" | | Total Games: {game_count}") 106 | TOTAL_MINUTES = 0 107 | 108 | for game in steam_json["response"]["games"]: 109 | TOTAL_MINUTES += game["playtime_forever"] 110 | total_hours = TOTAL_MINUTES / 60 111 | total_days = TOTAL_MINUTES / 60 / 24 112 | total_years = TOTAL_MINUTES / 60 / 24 / 365 113 | print(f" | | Total Hours: {total_hours}") 114 | print(f" | | Total Days: {total_days}") 115 | print(f" | | Total Years: {total_years:.2f}") 116 | 117 | print("\nFinished!") 118 | print(f"Board Uptime: {time_calc(time.monotonic())}") 119 | print(f"Next Update: {time_calc(SLEEP_TIME)}") 120 | print("===============================") 121 | 122 | except (ValueError, RuntimeError) as e: 123 | print(f"Failed to get data, retrying\n {e}") 124 | time.sleep(60) 125 | break 126 | time.sleep(SLEEP_TIME) 127 | -------------------------------------------------------------------------------- /examples/wifi/expanded/requests_wifi_api_twitch.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 DJDevon3 2 | # SPDX-License-Identifier: MIT 3 | # Coded for Circuit Python 8.2.x 4 | """Twitch API Example""" 5 | 6 | import os 7 | import time 8 | 9 | import adafruit_connection_manager 10 | import wifi 11 | 12 | import adafruit_requests 13 | 14 | # Twitch Developer Account & oauth App Required: 15 | # Visit https://dev.twitch.tv/console to create an app 16 | # Ensure these are in settings.toml 17 | # TWITCH_CLIENT_ID = "Your Developer APP ID Here" 18 | # TWITCH_CLIENT_SECRET = "APP ID secret here" 19 | # TWITCH_USER_ID = "Your Twitch UserID here" 20 | 21 | # Get WiFi details, ensure these are setup in settings.toml 22 | ssid = os.getenv("CIRCUITPY_WIFI_SSID") 23 | password = os.getenv("CIRCUITPY_WIFI_PASSWORD") 24 | TWITCH_CID = os.getenv("TWITCH_CLIENT_ID") 25 | TWITCH_CS = os.getenv("TWITCH_CLIENT_SECRET") 26 | # For finding your Twitch User ID 27 | # https://www.streamweasels.com/tools/convert-twitch-username-to-user-id/ 28 | TWITCH_UID = os.getenv("TWITCH_USER_ID") 29 | 30 | # API Polling Rate 31 | # 900 = 15 mins, 1800 = 30 mins, 3600 = 1 hour 32 | SLEEP_TIME = 900 33 | 34 | # Set DEBUG to True for full JSON response. 35 | # STREAMER WARNING: Credentials will be viewable 36 | DEBUG = False 37 | 38 | # Initalize Wifi, Socket Pool, Request Session 39 | pool = adafruit_connection_manager.get_radio_socketpool(wifi.radio) 40 | ssl_context = adafruit_connection_manager.get_radio_ssl_context(wifi.radio) 41 | requests = adafruit_requests.Session(pool, ssl_context) 42 | 43 | 44 | def time_calc(input_time): 45 | """Converts seconds to minutes/hours/days""" 46 | if input_time < 60: 47 | return f"{input_time:.0f} seconds" 48 | if input_time < 3600: 49 | return f"{input_time / 60:.0f} minutes" 50 | if input_time < 86400: 51 | return f"{input_time / 60 / 60:.0f} hours" 52 | return f"{input_time / 60 / 60 / 24:.1f} days" 53 | 54 | 55 | def _format_datetime(datetime): 56 | """F-String formatted struct time conversion""" 57 | return ( 58 | f"{datetime.tm_mon:02}/" 59 | + f"{datetime.tm_mday:02}/" 60 | + f"{datetime.tm_year:02} " 61 | + f"{datetime.tm_hour:02}:" 62 | + f"{datetime.tm_min:02}:" 63 | + f"{datetime.tm_sec:02}" 64 | ) 65 | 66 | 67 | # First we use Client ID & Client Secret to create a token with POST 68 | # No user interaction is required for this type of scope (implicit grant flow) 69 | twitch_0auth_header = {"Content-Type": "application/x-www-form-urlencoded"} 70 | TWITCH_0AUTH_TOKEN = "https://id.twitch.tv/oauth2/token" 71 | 72 | while True: 73 | # Connect to Wi-Fi 74 | print("\nConnecting to WiFi...") 75 | while not wifi.radio.ipv4_address: 76 | try: 77 | wifi.radio.connect(ssid, password) 78 | except ConnectionError as e: 79 | print("❌ Connection Error:", e) 80 | print("Retrying in 10 seconds") 81 | print("✅ Wifi!") 82 | 83 | try: 84 | # ------------- POST FOR BEARER TOKEN ----------------- 85 | print(" | Attempting Bearer Token Request!") 86 | if DEBUG: 87 | print(f"Full API GET URL: {TWITCH_0AUTH_TOKEN}") 88 | twitch_0auth_data = ( 89 | "&client_id=" 90 | + TWITCH_CID 91 | + "&client_secret=" 92 | + TWITCH_CS 93 | + "&grant_type=client_credentials" 94 | ) 95 | 96 | # POST REQUEST 97 | try: 98 | with requests.post( 99 | url=TWITCH_0AUTH_TOKEN, 100 | data=twitch_0auth_data, 101 | headers=twitch_0auth_header, 102 | ) as twitch_0auth_response: 103 | twitch_0auth_json = twitch_0auth_response.json() 104 | twitch_access_token = twitch_0auth_json["access_token"] 105 | except ConnectionError as e: 106 | print(f"Connection Error: {e}") 107 | print("Retrying in 10 seconds") 108 | print(" | 🔑 Token Authorized!") 109 | 110 | # STREAMER WARNING: your client secret will be viewable 111 | if DEBUG: 112 | print(f"JSON Dump: {twitch_0auth_json}") 113 | print(f"Header: {twitch_0auth_header}") 114 | print(f"Access Token: {twitch_access_token}") 115 | twitch_token_type = twitch_0auth_json["token_type"] 116 | print(f"Token Type: {twitch_token_type}") 117 | 118 | twitch_token_expiration = twitch_0auth_json["expires_in"] 119 | print(f" | Token Expires in: {time_calc(twitch_token_expiration)}") 120 | 121 | # ----------------------------- GET DATA -------------------- 122 | # Bearer token is refreshed every time script runs :) 123 | # Twitch sets token expiration to about 64 days 124 | # Helix is the name of the current Twitch API 125 | # Now that we have POST bearer token we can do a GET for data 126 | # ----------------------------------------------------------- 127 | twitch_header = { 128 | "Authorization": "Bearer " + twitch_access_token + "", 129 | "Client-Id": "" + TWITCH_CID + "", 130 | } 131 | TWITCH_FOLLOWERS_SOURCE = ( 132 | "https://api.twitch.tv/helix/channels" + "/followers?" + "broadcaster_id=" + TWITCH_UID 133 | ) 134 | print(" | Attempting to GET Twitch JSON!") 135 | try: 136 | with requests.get( 137 | url=TWITCH_FOLLOWERS_SOURCE, headers=twitch_header 138 | ) as twitch_response: 139 | twitch_json = twitch_response.json() 140 | except ConnectionError as e: 141 | print(f"Connection Error: {e}") 142 | print("Retrying in 10 seconds") 143 | 144 | if DEBUG: 145 | print(f" | Full API GET URL: {TWITCH_FOLLOWERS_SOURCE}") 146 | print(f" | Header: {twitch_header}") 147 | print(f" | JSON Full Response: {twitch_json}") 148 | 149 | if "status" in twitch_json: 150 | twitch_error_status = twitch_json["status"] 151 | print(f"❌ Status: {twitch_error_status}") 152 | 153 | if "error" in twitch_json: 154 | twitch_error = twitch_json["error"] 155 | print(f"❌ Error: {twitch_error}") 156 | 157 | if "message" in twitch_json: 158 | twitch_error_msg = twitch_json["message"] 159 | print(f"❌ Message: {twitch_error_msg}") 160 | 161 | if "total" in twitch_json: 162 | print(" | ✅ Twitch JSON!") 163 | twitch_followers = twitch_json["total"] 164 | print(f" | | Followers: {twitch_followers}") 165 | 166 | print("\nFinished!") 167 | print(f"Board Uptime: {time_calc(time.monotonic())}") 168 | print(f"Next Update: {time_calc(SLEEP_TIME)}") 169 | print("===============================") 170 | 171 | except (ValueError, RuntimeError) as e: 172 | print(f"Failed to get data, retrying\n {e}") 173 | time.sleep(60) 174 | break 175 | time.sleep(SLEEP_TIME) 176 | -------------------------------------------------------------------------------- /examples/wifi/expanded/requests_wifi_api_twitter.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adafruit/Adafruit_CircuitPython_Requests/21255976fe061946cd353e416e254effb45ff12a/examples/wifi/expanded/requests_wifi_api_twitter.py -------------------------------------------------------------------------------- /examples/wifi/expanded/requests_wifi_api_youtube.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 DJDevon3 2 | # SPDX-License-Identifier: MIT 3 | # Coded for Circuit Python 8.2.x 4 | """YouTube API Subscriber Count Example""" 5 | 6 | import os 7 | import time 8 | 9 | import adafruit_connection_manager 10 | import wifi 11 | 12 | import adafruit_requests 13 | 14 | # Initalize Wifi, Socket Pool, Request Session 15 | pool = adafruit_connection_manager.get_radio_socketpool(wifi.radio) 16 | ssl_context = adafruit_connection_manager.get_radio_ssl_context(wifi.radio) 17 | requests = adafruit_requests.Session(pool, ssl_context) 18 | 19 | # API Polling Rate 20 | # 900 = 15 mins, 1800 = 30 mins, 3600 = 1 hour 21 | SLEEP_TIME = 900 22 | 23 | # Set debug to True for full JSON response. 24 | # WARNING: Will show credentials 25 | DEBUG = False 26 | 27 | # Ensure these are uncommented and in settings.toml 28 | # YOUTUBE_USERNAME = "Your YouTube Username", 29 | # YOUTUBE_TOKEN = "Your long API developer token", 30 | 31 | # Get WiFi details, ensure these are setup in settings.toml 32 | ssid = os.getenv("CIRCUITPY_WIFI_SSID") 33 | password = os.getenv("CIRCUITPY_WIFI_PASSWORD") 34 | # Requires YouTube/Google API key 35 | # https://console.cloud.google.com/apis/dashboard 36 | YT_USERNAME = os.getenv("YOUTUBE_USERNAME") 37 | YT_TOKEN = os.getenv("YOUTUBE_TOKEN") 38 | 39 | 40 | def time_calc(input_time): 41 | """Converts seconds to minutes/hours/days""" 42 | if input_time < 60: 43 | return f"{input_time:.0f} seconds" 44 | if input_time < 3600: 45 | return f"{input_time / 60:.0f} minutes" 46 | if input_time < 86400: 47 | return f"{input_time / 60 / 60:.0f} hours" 48 | return f"{input_time / 60 / 60 / 24:.1f} days" 49 | 50 | 51 | # https://youtube.googleapis.com/youtube/v3/channels?part=statistics&forUsername=[YOUR_USERNAME]&key=[YOUR_API_KEY] 52 | YOUTUBE_SOURCE = ( 53 | "https://youtube.googleapis.com/youtube/v3/channels?part=statistics&forUsername=" 54 | + str(YT_USERNAME) 55 | + "&key=" 56 | + str(YT_TOKEN) 57 | ) 58 | 59 | while True: 60 | # Connect to Wi-Fi 61 | print("\nConnecting to WiFi...") 62 | while not wifi.radio.ipv4_address: 63 | try: 64 | wifi.radio.connect(ssid, password) 65 | except ConnectionError as e: 66 | print("❌ Connection Error:", e) 67 | print("Retrying in 10 seconds") 68 | print("✅ Wifi!") 69 | try: 70 | print(" | Attempting to GET YouTube JSON...") 71 | try: 72 | with requests.get(url=YOUTUBE_SOURCE) as youtube_response: 73 | youtube_json = youtube_response.json() 74 | except ConnectionError as e: 75 | print("Connection Error:", e) 76 | print("Retrying in 10 seconds") 77 | print(" | ✅ YouTube JSON!") 78 | 79 | if DEBUG: 80 | print(f" | Full API GET URL: {YOUTUBE_SOURCE}") 81 | print(f" | Full API Dump: {youtube_json}") 82 | 83 | # Key:Value RESPONSES 84 | if "pageInfo" in youtube_json: 85 | totalResults = youtube_json["pageInfo"]["totalResults"] 86 | print(f" | | Matching Results: {totalResults}") 87 | 88 | if "items" in youtube_json: 89 | YT_request_kind = youtube_json["items"][0]["kind"] 90 | print(f" | | Request Kind: {YT_request_kind}") 91 | 92 | YT_channel_id = youtube_json["items"][0]["id"] 93 | print(f" | | Channel ID: {YT_channel_id}") 94 | 95 | YT_videoCount = youtube_json["items"][0]["statistics"]["videoCount"] 96 | print(f" | | Videos: {YT_videoCount}") 97 | 98 | YT_viewCount = youtube_json["items"][0]["statistics"]["viewCount"] 99 | print(f" | | Views: {YT_viewCount}") 100 | 101 | YT_subsCount = youtube_json["items"][0]["statistics"]["subscriberCount"] 102 | print(f" | | Subscribers: {YT_subsCount}") 103 | 104 | if "kind" in youtube_json: 105 | YT_response_kind = youtube_json["kind"] 106 | print(f" | | Response Kind: {YT_response_kind}") 107 | 108 | print("\nFinished!") 109 | print(f"Board Uptime: {time_calc(time.monotonic())}") 110 | print(f"Next Update: {time_calc(SLEEP_TIME)}") 111 | print("===============================") 112 | 113 | except (ValueError, RuntimeError) as e: 114 | print(f"Failed to get data, retrying\n {e}") 115 | time.sleep(60) 116 | break 117 | time.sleep(SLEEP_TIME) 118 | -------------------------------------------------------------------------------- /examples/wifi/expanded/requests_wifi_file_upload.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Tim Cocks for Adafruit Industries 2 | # SPDX-License-Identifier: MIT 3 | 4 | import adafruit_connection_manager 5 | import wifi 6 | 7 | import adafruit_requests 8 | 9 | URL = "https://httpbin.org/post" 10 | 11 | pool = adafruit_connection_manager.get_radio_socketpool(wifi.radio) 12 | ssl_context = adafruit_connection_manager.get_radio_ssl_context(wifi.radio) 13 | requests = adafruit_requests.Session(pool, ssl_context) 14 | 15 | with open("requests_wifi_file_upload_image.png", "rb") as file_handle: 16 | files = { 17 | "file": ( 18 | "requests_wifi_file_upload_image.png", 19 | file_handle, 20 | "image/png", 21 | {"CustomHeader": "BlinkaRocks"}, 22 | ), 23 | "othervalue": (None, "HelloWorld"), 24 | } 25 | 26 | with requests.post(URL, files=files) as response: 27 | print(response.content) 28 | -------------------------------------------------------------------------------- /examples/wifi/expanded/requests_wifi_file_upload_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adafruit/Adafruit_CircuitPython_Requests/21255976fe061946cd353e416e254effb45ff12a/examples/wifi/expanded/requests_wifi_file_upload_image.png -------------------------------------------------------------------------------- /examples/wifi/expanded/requests_wifi_file_upload_image.png.license: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Tim Cocks 2 | # SPDX-License-Identifier: CC-BY-4.0 3 | -------------------------------------------------------------------------------- /examples/wifi/expanded/requests_wifi_multiple_cookies.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022 Alec Delaney 2 | # SPDX-License-Identifier: MIT 3 | # Coded for Circuit Python 9.0 4 | """Multiple Cookies Example written for MagTag""" 5 | 6 | import os 7 | 8 | import adafruit_connection_manager 9 | import wifi 10 | 11 | import adafruit_requests 12 | 13 | # Get WiFi details, ensure these are setup in settings.toml 14 | ssid = os.getenv("CIRCUITPY_WIFI_SSID") 15 | password = os.getenv("CIRCUITPY_WIFI_PASSWORD") 16 | 17 | COOKIE_TEST_URL = "https://www.adafruit.com" 18 | 19 | # Initalize Wifi, Socket Pool, Request Session 20 | pool = adafruit_connection_manager.get_radio_socketpool(wifi.radio) 21 | ssl_context = adafruit_connection_manager.get_radio_ssl_context(wifi.radio) 22 | requests = adafruit_requests.Session(pool, ssl_context) 23 | 24 | print(f"\nConnecting to {ssid}...") 25 | try: 26 | # Connect to the Wi-Fi network 27 | wifi.radio.connect(ssid, password) 28 | except OSError as e: 29 | print(f"❌ OSError: {e}") 30 | print("✅ Wifi!") 31 | 32 | # URL GET Request 33 | with requests.get(COOKIE_TEST_URL) as response: 34 | print(f" | Fetching Cookies: {COOKIE_TEST_URL}") 35 | 36 | # Spilt up the cookies by ", " 37 | elements = response.headers["set-cookie"].split(", ") 38 | 39 | # NOTE: Some cookies use ", " when describing dates. This code will iterate through 40 | # the previously split up 'set-cookie' header value and piece back together cookies 41 | # that were accidentally split up for this reason 42 | days_of_week = ("Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat") 43 | elements_iter = iter(elements) 44 | cookie_list = [] 45 | for element in elements_iter: 46 | captured_day = [day for day in days_of_week if element.endswith(day)] 47 | if captured_day: 48 | cookie_list.append(element + ", " + next(elements_iter)) 49 | else: 50 | cookie_list.append(element) 51 | 52 | # Pring the information about the cookies 53 | print(f" | Total Cookies: {len(cookie_list)}") 54 | print("-" * 80) 55 | 56 | for cookie in cookie_list: 57 | print(f" | 🍪 {cookie}") 58 | print("-" * 80) 59 | 60 | print("Finished!") 61 | -------------------------------------------------------------------------------- /examples/wifi/expanded/requests_wifi_rachio_irrigation.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 DJDevon3 2 | # SPDX-License-Identifier: MIT 3 | # Coded for Circuit Python 9.x 4 | """Rachio Irrigation Timer API Example""" 5 | 6 | import os 7 | import time 8 | 9 | import adafruit_connection_manager 10 | import wifi 11 | 12 | import adafruit_requests 13 | 14 | # Rachio API Key required (comes with purchase of a device) 15 | # API is rate limited to 1700 calls per day. 16 | # https://support.rachio.com/en_us/public-api-documentation-S1UydL1Fv 17 | # https://rachio.readme.io/reference/getting-started 18 | RACHIO_KEY = os.getenv("RACHIO_APIKEY") 19 | RACHIO_PERSONID = os.getenv("RACHIO_PERSONID") 20 | 21 | # Get WiFi details, ensure these are setup in settings.toml 22 | ssid = os.getenv("CIRCUITPY_WIFI_SSID") 23 | password = os.getenv("CIRCUITPY_WIFI_PASSWORD") 24 | 25 | # API Polling Rate 26 | # 900 = 15 mins, 1800 = 30 mins, 3600 = 1 hour 27 | SLEEP_TIME = 900 28 | 29 | # Set debug to True for full JSON response. 30 | # WARNING: absolutely shows extremely sensitive personal information & credentials 31 | # Including your real name, latitude, longitude, account id, mac address, etc... 32 | DEBUG = False 33 | 34 | # Initalize Wifi, Socket Pool, Request Session 35 | pool = adafruit_connection_manager.get_radio_socketpool(wifi.radio) 36 | ssl_context = adafruit_connection_manager.get_radio_ssl_context(wifi.radio) 37 | requests = adafruit_requests.Session(pool, ssl_context) 38 | 39 | RACHIO_HEADER = {"Authorization": " Bearer " + RACHIO_KEY} 40 | RACHIO_SOURCE = "https://api.rach.io/1/public/person/info/" 41 | RACHIO_PERSON_SOURCE = "https://api.rach.io/1/public/person/" 42 | 43 | 44 | def obfuscating_asterix(obfuscate_object, direction, characters=2): 45 | """ 46 | Obfuscates a string with asterisks except for a specified number of characters. 47 | param object: str The string to obfuscate with asterisks 48 | param direction: str Option either 'prepend', 'append', or 'all' direction 49 | param characters: int The number of characters to keep unobfuscated (default is 2) 50 | """ 51 | object_len = len(obfuscate_object) 52 | if direction not in {"prepend", "append", "all"}: 53 | raise ValueError("Invalid direction. Use 'prepend', 'append', or 'all'.") 54 | if characters >= object_len and direction != "all": 55 | # If characters greater than or equal to string length, 56 | # return the original string as it can't be obfuscated. 57 | return obfuscate_object 58 | asterix_replace = "*" * object_len 59 | if direction == "append": 60 | asterix_final = obfuscate_object[:characters] + "*" * (object_len - characters) 61 | elif direction == "prepend": 62 | asterix_final = "*" * (object_len - characters) + obfuscate_object[-characters:] 63 | elif direction == "all": 64 | # Replace all characters with asterisks 65 | asterix_final = asterix_replace 66 | 67 | return asterix_final 68 | 69 | 70 | def time_calc(input_time): 71 | """Converts seconds to minutes/hours/days""" 72 | if input_time < 60: 73 | return f"{input_time:.0f} seconds" 74 | if input_time < 3600: 75 | return f"{input_time / 60:.0f} minutes" 76 | if input_time < 86400: 77 | return f"{input_time / 60 / 60:.0f} hours" 78 | return f"{input_time / 60 / 60 / 24:.1f} days" 79 | 80 | 81 | def _format_datetime(datetime): 82 | """F-String formatted struct time conversion""" 83 | return ( 84 | f"{datetime.tm_mon:02}/" 85 | + f"{datetime.tm_mday:02}/" 86 | + f"{datetime.tm_year:02} " 87 | + f"{datetime.tm_hour:02}:" 88 | + f"{datetime.tm_min:02}:" 89 | + f"{datetime.tm_sec:02}" 90 | ) 91 | 92 | 93 | while True: 94 | # Connect to Wi-Fi 95 | print("\nConnecting to WiFi...") 96 | while not wifi.radio.ipv4_address: 97 | try: 98 | wifi.radio.connect(ssid, password) 99 | except ConnectionError as e: 100 | print("❌ Connection Error:", e) 101 | print("Retrying in 10 seconds") 102 | print("✅ Wifi!") 103 | 104 | # RETREIVE PERSONID AND PASTE IT TO SETTINGS.TOML 105 | if RACHIO_PERSONID is None or RACHIO_PERSONID == "": 106 | try: 107 | print(" | Attempting to GET Rachio Authorization") 108 | try: 109 | with requests.get(url=RACHIO_SOURCE, headers=RACHIO_HEADER) as rachio_response: 110 | rachio_json = rachio_response.json() 111 | except ConnectionError as e: 112 | print("Connection Error:", e) 113 | print("Retrying in 10 seconds") 114 | print(" | ✅ Authorized") 115 | 116 | rachio_id = rachio_json["id"] 117 | print("\nADD THIS 🔑 TO YOUR SETTINGS.TOML FILE!") 118 | print(f'RACHIO_PERSONID = "{rachio_id}"') 119 | 120 | if DEBUG: 121 | print("\nFull API GET URL: ", RACHIO_SOURCE) 122 | print(rachio_json) 123 | 124 | except (ValueError, RuntimeError) as e: 125 | print(f"Failed to GET data: {e}") 126 | time.sleep(60) 127 | break 128 | print("\nThis script can only continue when a proper APIKey & PersonID is used.") 129 | print("\nFinished!") 130 | print("===============================") 131 | time.sleep(SLEEP_TIME) 132 | 133 | # Main Script 134 | if RACHIO_PERSONID is not None and RACHIO_PERSONID != "": 135 | try: 136 | print(" | Attempting to GET Rachio JSON") 137 | try: 138 | with requests.get( 139 | url=RACHIO_PERSON_SOURCE + RACHIO_PERSONID, headers=RACHIO_HEADER 140 | ) as rachio_response: 141 | rachio_json = rachio_response.json() 142 | except ConnectionError as e: 143 | print("Connection Error:", e) 144 | print("Retrying in 10 seconds") 145 | print(" | ✅ Rachio JSON") 146 | response_headers = rachio_response.headers 147 | if DEBUG: 148 | print(f"Response Headers: {response_headers}") 149 | call_limit = int(response_headers["x-ratelimit-limit"]) 150 | calls_remaining = int(response_headers["x-ratelimit-remaining"]) 151 | calls_made_today = call_limit - calls_remaining 152 | 153 | print(" | | Headers:") 154 | print(f" | | | Date: {response_headers['date']}") 155 | print(f" | | | Maximum Daily Requests: {call_limit}") 156 | print(f" | | | Today's Requests: {calls_made_today}") 157 | print(f" | | | Remaining Requests: {calls_remaining}") 158 | print(f" | | | Limit Reset: {response_headers['x-ratelimit-reset']}") 159 | print(f" | | | Content Type: {response_headers['content-type']}") 160 | 161 | rachio_id = rachio_json["id"] 162 | rachio_id_ast = obfuscating_asterix(rachio_id, "append", 3) 163 | print(" | | PersonID: ", rachio_id_ast) 164 | 165 | rachio_username = rachio_json["username"] 166 | rachio_username_ast = obfuscating_asterix(rachio_username, "append", 3) 167 | print(" | | Username: ", rachio_username_ast) 168 | 169 | rachio_name = rachio_json["fullName"] 170 | rachio_name_ast = obfuscating_asterix(rachio_name, "append", 3) 171 | print(" | | Full Name: ", rachio_name_ast) 172 | 173 | rachio_deleted = rachio_json["deleted"] 174 | if not rachio_deleted: 175 | print(" | | Account Status: Active") 176 | else: 177 | print(" | | Account Status?: Deleted!") 178 | 179 | rachio_createdate = rachio_json["createDate"] 180 | rachio_timezone_offset = rachio_json["devices"][0]["utcOffset"] 181 | # Rachio Unix time is in milliseconds, convert to seconds 182 | rachio_createdate_seconds = rachio_createdate // 1000 183 | rachio_timezone_offset_seconds = rachio_timezone_offset // 1000 184 | # Apply timezone offset in seconds 185 | local_unix_time = rachio_createdate_seconds + rachio_timezone_offset_seconds 186 | if DEBUG: 187 | print(f" | | Unix Registration Date: {rachio_createdate}") 188 | print(f" | | Unix Timezone Offset: {rachio_timezone_offset}") 189 | current_struct_time = time.localtime(local_unix_time) 190 | final_timestamp = f"{_format_datetime(current_struct_time)}" 191 | print(f" | | Registration Date: {final_timestamp}") 192 | 193 | rachio_devices = rachio_json["devices"][0]["name"] 194 | print(" | | Device: ", rachio_devices) 195 | 196 | rachio_model = rachio_json["devices"][0]["model"] 197 | print(" | | | Model: ", rachio_model) 198 | 199 | rachio_serial = rachio_json["devices"][0]["serialNumber"] 200 | rachio_serial_ast = obfuscating_asterix(rachio_serial, "append") 201 | print(" | | | Serial Number: ", rachio_serial_ast) 202 | 203 | rachio_mac = rachio_json["devices"][0]["macAddress"] 204 | rachio_mac_ast = obfuscating_asterix(rachio_mac, "append") 205 | print(" | | | MAC Address: ", rachio_mac_ast) 206 | 207 | rachio_status = rachio_json["devices"][0]["status"] 208 | print(" | | | Device Status: ", rachio_status) 209 | 210 | rachio_timezone = rachio_json["devices"][0]["timeZone"] 211 | print(" | | | Time Zone: ", rachio_timezone) 212 | 213 | # Latitude & Longtitude are used for smart watering & rain delays 214 | rachio_latitude = str(rachio_json["devices"][0]["latitude"]) 215 | rachio_lat_ast = obfuscating_asterix(rachio_latitude, "all") 216 | print(" | | | Latitude: ", rachio_lat_ast) 217 | 218 | rachio_longitude = str(rachio_json["devices"][0]["longitude"]) 219 | rachio_long_ast = obfuscating_asterix(rachio_longitude, "all") 220 | print(" | | | Longitude: ", rachio_long_ast) 221 | 222 | rachio_rainsensor = rachio_json["devices"][0]["rainSensorTripped"] 223 | print(" | | | Rain Sensor: ", rachio_rainsensor) 224 | 225 | rachio_zone0 = rachio_json["devices"][0]["zones"][0]["name"] 226 | rachio_zone1 = rachio_json["devices"][0]["zones"][1]["name"] 227 | rachio_zone2 = rachio_json["devices"][0]["zones"][2]["name"] 228 | rachio_zone3 = rachio_json["devices"][0]["zones"][3]["name"] 229 | zones = f"{rachio_zone0}, {rachio_zone1}, {rachio_zone2}, {rachio_zone3}" 230 | print(f" | | | Zones: {zones}") 231 | 232 | if DEBUG: 233 | print(f"\nFull API GET URL: {RACHIO_PERSON_SOURCE+rachio_id}") 234 | print(rachio_json) 235 | 236 | print("\nFinished!") 237 | print(f"Board Uptime: {time_calc(time.monotonic())}") 238 | print(f"Next Update: {time_calc(SLEEP_TIME)}") 239 | print("===============================") 240 | 241 | except (ValueError, RuntimeError) as e: 242 | print(f"Failed to get data, retrying\n {e}") 243 | time.sleep(60) 244 | break 245 | 246 | time.sleep(SLEEP_TIME) 247 | -------------------------------------------------------------------------------- /examples/wifi/expanded/requests_wifi_status_codes.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 DJDevon3 2 | # SPDX-License-Identifier: MIT 3 | # Updated for Circuit Python 9.0 4 | # https://help.openai.com/en/articles/6825453-chatgpt-release-notes 5 | # https://chat.openai.com/share/32ef0c5f-ac92-4d36-9d1e-0f91e0c4c574 6 | """WiFi Status Codes Example""" 7 | 8 | import os 9 | import time 10 | 11 | import adafruit_connection_manager 12 | import wifi 13 | 14 | import adafruit_requests 15 | 16 | # Get WiFi details, ensure these are setup in settings.toml 17 | ssid = os.getenv("CIRCUITPY_WIFI_SSID") 18 | password = os.getenv("CIRCUITPY_WIFI_PASSWORD") 19 | 20 | # Initalize Wifi, Socket Pool, Request Session 21 | pool = adafruit_connection_manager.get_radio_socketpool(wifi.radio) 22 | ssl_context = adafruit_connection_manager.get_radio_ssl_context(wifi.radio) 23 | requests = adafruit_requests.Session(pool, ssl_context) 24 | rssi = wifi.radio.ap_info.rssi 25 | 26 | 27 | def print_http_status(expected_code, actual_code, description): 28 | """Returns HTTP status code and description""" 29 | if "100" <= actual_code <= "103": 30 | print(f" | ✅ Status Test Expected: {expected_code} Actual: {actual_code} - {description}") 31 | elif "200" == actual_code: 32 | print(f" | 🆗 Status Test Expected: {expected_code} Actual: {actual_code} - {description}") 33 | elif "201" <= actual_code <= "299": 34 | print(f" | ✅ Status Test Expected: {expected_code} Actual: {actual_code} - {description}") 35 | elif "300" <= actual_code <= "600": 36 | print(f" | ❌ Status Test Expected: {expected_code} Actual: {actual_code} - {description}") 37 | else: 38 | print( 39 | f" | Unknown Response Status Expected: {expected_code} " 40 | + f"Actual: {actual_code} - {description}" 41 | ) 42 | 43 | 44 | # All HTTP Status Codes 45 | http_status_codes = { 46 | "100": "Continue", 47 | "101": "Switching Protocols", 48 | "102": "Processing", 49 | "103": "Early Hints", 50 | "200": "OK", 51 | "201": "Created", 52 | "202": "Accepted", 53 | "203": "Non-Authoritative Information", 54 | "204": "No Content", 55 | "205": "Reset Content", 56 | "206": "Partial Content", 57 | "207": "Multi-Status", 58 | "208": "Already Reported", 59 | "226": "IM Used", 60 | "300": "Multiple Choices", 61 | "301": "Moved Permanently", 62 | "302": "Found", 63 | "303": "See Other", 64 | "304": "Not Modified", 65 | "305": "Use Proxy", 66 | "306": "Unused", 67 | "307": "Temporary Redirect", 68 | "308": "Permanent Redirect", 69 | "400": "Bad Request", 70 | "401": "Unauthorized", 71 | "402": "Payment Required", 72 | "403": "Forbidden", 73 | "404": "Not Found", 74 | "405": "Method Not Allowed", 75 | "406": "Not Acceptable", 76 | "407": "Proxy Authentication Required", 77 | "408": "Request Timeout", 78 | "409": "Conflict", 79 | "410": "Gone", 80 | "411": "Length Required", 81 | "412": "Precondition Failed", 82 | "413": "Payload Too Large", 83 | "414": "URI Too Long", 84 | "415": "Unsupported Media Type", 85 | "416": "Range Not Satisfiable", 86 | "417": "Expectation Failed", 87 | "418": "I'm a teapot", 88 | "421": "Misdirected Request", 89 | "422": "Unprocessable Entity", 90 | "423": "Locked", 91 | "424": "Failed Dependency", 92 | "425": "Too Early", 93 | "426": "Upgrade Required", 94 | "428": "Precondition Required", 95 | "429": "Too Many Requests", 96 | "431": "Request Header Fields Too Large", 97 | "451": "Unavailable For Legal Reasons", 98 | "500": "Internal Server Error", 99 | "501": "Not Implemented", 100 | "502": "Bad Gateway", 101 | "503": "Service Unavailable", 102 | "504": "Gateway Timeout", 103 | "505": "HTTP Version Not Supported", 104 | "506": "Variant Also Negotiates", 105 | "507": "Insufficient Storage", 106 | "508": "Loop Detected", 107 | "510": "Not Extended", 108 | "511": "Network Authentication Required", 109 | } 110 | 111 | STATUS_TEST_URL = "https://httpbin.org/status/" 112 | 113 | print(f"\nConnecting to {ssid}...") 114 | print(f"Signal Strength: {rssi}") 115 | try: 116 | # Connect to the Wi-Fi network 117 | wifi.radio.connect(ssid, password) 118 | except OSError as e: 119 | print(f"❌ OSError: {e}") 120 | print("✅ Wifi!") 121 | 122 | 123 | print(f" | Status Code Test: {STATUS_TEST_URL}") 124 | # Some return errors then confirm the error (that's a good thing) 125 | # Demonstrates not all errors have the same behavior 126 | # Some 300 level responses contain redirects that requests automatically follows 127 | # By default the response object will contain the status code from the final 128 | # response after all redirect, so it can differ from the expected status code. 129 | for current_code in sorted(http_status_codes.keys(), key=int): 130 | header_status_test_url = STATUS_TEST_URL + current_code 131 | with requests.get(header_status_test_url) as response: 132 | response_status_code = str(response.status_code) 133 | SORT_STATUS_DESC = http_status_codes.get(response_status_code, "Unknown Status Code") 134 | print_http_status(current_code, response_status_code, SORT_STATUS_DESC) 135 | 136 | # Rate limit ourselves a little to avoid strain on server 137 | time.sleep(0.5) 138 | print("Finished!") 139 | -------------------------------------------------------------------------------- /examples/wifi/requests_wifi_advanced.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 ladyada for Adafruit Industries 2 | # SPDX-License-Identifier: MIT 3 | # Updated for Circuit Python 9.0 4 | 5 | """WiFi Advanced Example""" 6 | 7 | import os 8 | 9 | import adafruit_connection_manager 10 | import wifi 11 | 12 | import adafruit_requests 13 | 14 | # Get WiFi details, ensure these are setup in settings.toml 15 | ssid = os.getenv("CIRCUITPY_WIFI_SSID") 16 | password = os.getenv("CIRCUITPY_WIFI_PASSWORD") 17 | 18 | # Initalize Wifi, Socket Pool, Request Session 19 | pool = adafruit_connection_manager.get_radio_socketpool(wifi.radio) 20 | ssl_context = adafruit_connection_manager.get_radio_ssl_context(wifi.radio) 21 | requests = adafruit_requests.Session(pool, ssl_context) 22 | rssi = wifi.radio.ap_info.rssi 23 | 24 | # URL for GET request 25 | JSON_GET_URL = "https://httpbin.org/get" 26 | # Define a custom header as a dict. 27 | headers = {"user-agent": "blinka/1.0.0"} 28 | 29 | print(f"\nConnecting to {ssid}...") 30 | print(f"Signal Strength: {rssi}") 31 | try: 32 | # Connect to the Wi-Fi network 33 | wifi.radio.connect(ssid, password) 34 | except OSError as e: 35 | print(f"❌ OSError: {e}") 36 | print("✅ Wifi!") 37 | 38 | # Define a custom header as a dict. 39 | headers = {"user-agent": "blinka/1.0.0"} 40 | print(f" | Fetching URL {JSON_GET_URL}") 41 | 42 | # Use with statement for retreiving GET request data 43 | with requests.get(JSON_GET_URL, headers=headers) as response: 44 | json_data = response.json() 45 | headers = json_data["headers"] 46 | content_type = response.headers.get("content-type", "") 47 | date = response.headers.get("date", "") 48 | if response.status_code == 200: 49 | print(f" | 🆗 Status Code: {response.status_code}") 50 | else: 51 | print(f" | ❌ Status Code: {response.status_code}") 52 | print(f" | | Custom User-Agent Header: {headers['User-Agent']}") 53 | print(f" | | Content-Type: {content_type}") 54 | print(f" | | Response Timestamp: {date}") 55 | -------------------------------------------------------------------------------- /examples/wifi/requests_wifi_simpletest.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 ladyada for Adafruit Industries 2 | # SPDX-License-Identifier: MIT 3 | # Updated for Circuit Python 9.0 4 | """WiFi Simpletest""" 5 | 6 | import os 7 | 8 | import adafruit_connection_manager 9 | import wifi 10 | 11 | import adafruit_requests 12 | 13 | # Get WiFi details, ensure these are setup in settings.toml 14 | ssid = os.getenv("CIRCUITPY_WIFI_SSID") 15 | password = os.getenv("CIRCUITPY_WIFI_PASSWORD") 16 | 17 | TEXT_URL = "http://wifitest.adafruit.com/testwifi/index.html" 18 | JSON_GET_URL = "https://httpbin.org/get" 19 | JSON_POST_URL = "https://httpbin.org/post" 20 | 21 | # Initalize Wifi, Socket Pool, Request Session 22 | pool = adafruit_connection_manager.get_radio_socketpool(wifi.radio) 23 | ssl_context = adafruit_connection_manager.get_radio_ssl_context(wifi.radio) 24 | requests = adafruit_requests.Session(pool, ssl_context) 25 | rssi = wifi.radio.ap_info.rssi 26 | 27 | print(f"\nConnecting to {ssid}...") 28 | print(f"Signal Strength: {rssi}") 29 | try: 30 | # Connect to the Wi-Fi network 31 | wifi.radio.connect(ssid, password) 32 | except OSError as e: 33 | print(f"❌ OSError: {e}") 34 | print("✅ Wifi!") 35 | 36 | print(f" | GET Text Test: {TEXT_URL}") 37 | with requests.get(TEXT_URL) as response: 38 | print(f" | ✅ GET Response: {response.text}") 39 | print("-" * 80) 40 | 41 | print(f" | GET Full Response Test: {JSON_GET_URL}") 42 | with requests.get(JSON_GET_URL) as response: 43 | print(f" | ✅ Unparsed Full JSON Response: {response.json()}") 44 | print("-" * 80) 45 | 46 | DATA = "This is an example of a JSON value" 47 | print(f" | ✅ JSON 'value' POST Test: {JSON_POST_URL} {DATA}") 48 | with requests.post(JSON_POST_URL, data=DATA) as response: 49 | json_resp = response.json() 50 | # Parse out the 'data' key from json_resp dict. 51 | print(f" | ✅ JSON 'value' Response: {json_resp['data']}") 52 | print("-" * 80) 53 | 54 | json_data = {"Date": "January 1, 1970"} 55 | print(f" | ✅ JSON 'key':'value' POST Test: {JSON_POST_URL} {json_data}") 56 | with requests.post(JSON_POST_URL, json=json_data) as response: 57 | json_resp = response.json() 58 | # Parse out the 'json' key from json_resp dict. 59 | print(f" | ✅ JSON 'key':'value' Response: {json_resp['json']}") 60 | print("-" * 80) 61 | 62 | print("Finished!") 63 | -------------------------------------------------------------------------------- /examples/wiznet5k/requests_wiznet5k_advanced.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 ladyada for Adafruit Industries 2 | # SPDX-License-Identifier: MIT 3 | 4 | import adafruit_connection_manager 5 | import board 6 | import busio 7 | from adafruit_wiznet5k.adafruit_wiznet5k import WIZNET5K 8 | from digitalio import DigitalInOut 9 | 10 | import adafruit_requests 11 | 12 | cs = DigitalInOut(board.D10) 13 | spi_bus = busio.SPI(board.SCK, MOSI=board.MOSI, MISO=board.MISO) 14 | 15 | # Initialize ethernet interface with DHCP 16 | radio = WIZNET5K(spi_bus, cs) 17 | 18 | # Initialize a requests session 19 | pool = adafruit_connection_manager.get_radio_socketpool(radio) 20 | ssl_context = adafruit_connection_manager.get_radio_ssl_context(radio) 21 | requests = adafruit_requests.Session(pool, ssl_context) 22 | 23 | JSON_GET_URL = "http://httpbin.org/get" 24 | 25 | # Define a custom header as a dict. 26 | headers = {"user-agent": "blinka/1.0.0"} 27 | 28 | print("Fetching JSON data from %s..." % JSON_GET_URL) 29 | with requests.get(JSON_GET_URL, headers=headers) as response: 30 | print("-" * 60) 31 | 32 | json_data = response.json() 33 | headers = json_data["headers"] 34 | print("Response's Custom User-Agent Header: {0}".format(headers["User-Agent"])) 35 | print("-" * 60) 36 | 37 | # Read Response's HTTP status code 38 | print("Response HTTP Status Code: ", response.status_code) 39 | print("-" * 60) 40 | -------------------------------------------------------------------------------- /examples/wiznet5k/requests_wiznet5k_simpletest.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 ladyada for Adafruit Industries 2 | # SPDX-License-Identifier: MIT 3 | 4 | import adafruit_connection_manager 5 | import board 6 | import busio 7 | from adafruit_wiznet5k.adafruit_wiznet5k import WIZNET5K 8 | from digitalio import DigitalInOut 9 | 10 | import adafruit_requests 11 | 12 | cs = DigitalInOut(board.D10) 13 | spi_bus = busio.SPI(board.SCK, MOSI=board.MOSI, MISO=board.MISO) 14 | 15 | # Initialize ethernet interface with DHCP 16 | radio = WIZNET5K(spi_bus, cs) 17 | 18 | # Initialize a requests session 19 | pool = adafruit_connection_manager.get_radio_socketpool(radio) 20 | ssl_context = adafruit_connection_manager.get_radio_ssl_context(radio) 21 | requests = adafruit_requests.Session(pool, ssl_context) 22 | 23 | TEXT_URL = "http://wifitest.adafruit.com/testwifi/index.html" 24 | JSON_GET_URL = "http://httpbin.org/get" 25 | JSON_POST_URL = "http://httpbin.org/post" 26 | 27 | print("Fetching text from %s" % TEXT_URL) 28 | with requests.get(TEXT_URL) as response: 29 | print("-" * 40) 30 | print("Text Response: ", response.text) 31 | print("-" * 40) 32 | 33 | print("Fetching JSON data from %s" % JSON_GET_URL) 34 | with requests.get(JSON_GET_URL) as response: 35 | print("-" * 40) 36 | print("JSON Response: ", response.json()) 37 | print("-" * 40) 38 | 39 | data = "31F" 40 | print(f"POSTing data to {JSON_POST_URL}: {data}") 41 | with requests.post(JSON_POST_URL, data=data) as response: 42 | print("-" * 40) 43 | json_resp = response.json() 44 | # Parse out the 'data' key from json_resp dict. 45 | print("Data received from server:", json_resp["data"]) 46 | print("-" * 40) 47 | 48 | json_data = {"Date": "July 25, 2019"} 49 | print(f"POSTing data to {JSON_POST_URL}: {json_data}") 50 | with requests.post(JSON_POST_URL, json=json_data) as response: 51 | print("-" * 40) 52 | json_resp = response.json() 53 | # Parse out the 'json' key from json_resp dict. 54 | print("JSON Data received from server:", json_resp["json"]) 55 | print("-" * 40) 56 | -------------------------------------------------------------------------------- /optional_requirements.txt: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022 Alec Delaney, for Adafruit Industries 2 | # 3 | # SPDX-License-Identifier: Unlicense 4 | 5 | requests 6 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022 Alec Delaney for Adafruit Industries 2 | # 3 | # SPDX-License-Identifier: MIT 4 | 5 | [build-system] 6 | requires = [ 7 | "setuptools", 8 | "wheel", 9 | "setuptools-scm", 10 | ] 11 | 12 | [project] 13 | name = "adafruit-circuitpython-requests" 14 | description = "A requests-like library for web interfacing" 15 | version = "0.0.0+auto.0" 16 | readme = "README.rst" 17 | authors = [ 18 | {name = "Adafruit Industries", email = "circuitpython@adafruit.com"} 19 | ] 20 | urls = {Homepage = "https://github.com/adafruit/Adafruit_CircuitPython_Requests"} 21 | keywords = [ 22 | "adafruit", 23 | "blinka", 24 | "circuitpython", 25 | "micropython", 26 | "requests", 27 | "requests,", 28 | "networking", 29 | ] 30 | license = {text = "MIT"} 31 | classifiers = [ 32 | "Intended Audience :: Developers", 33 | "Topic :: Software Development :: Libraries", 34 | "Topic :: Software Development :: Embedded Systems", 35 | "Topic :: System :: Hardware", 36 | "License :: OSI Approved :: MIT License", 37 | "Programming Language :: Python :: 3", 38 | ] 39 | dynamic = ["dependencies", "optional-dependencies"] 40 | 41 | [tool.ruff] 42 | target-version = "py38" 43 | 44 | [tool.ruff.lint] 45 | select = ["I", "PL", "UP"] 46 | ignore = ["PLR2004", "UP030"] 47 | 48 | [tool.ruff.format] 49 | line-ending = "lf" 50 | 51 | [tool.setuptools] 52 | py-modules = ["adafruit_requests"] 53 | 54 | [tool.setuptools.dynamic] 55 | dependencies = {file = ["requirements.txt"]} 56 | optional-dependencies = {optional = {file = ["optional_requirements.txt"]}} 57 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022 Alec Delaney, for Adafruit Industries 2 | # 3 | # SPDX-License-Identifier: Unlicense 4 | 5 | Adafruit-Blinka 6 | Adafruit-Circuitpython-ConnectionManager 7 | -------------------------------------------------------------------------------- /ruff.toml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Tim Cocks for Adafruit Industries 2 | # 3 | # SPDX-License-Identifier: MIT 4 | 5 | target-version = "py38" 6 | line-length = 100 7 | 8 | [lint] 9 | select = ["I", "PL", "UP"] 10 | 11 | extend-select = [ 12 | "D419", # empty-docstring 13 | "E501", # line-too-long 14 | "W291", # trailing-whitespace 15 | "PLC0414", # useless-import-alias 16 | "PLC2401", # non-ascii-name 17 | "PLC2801", # unnecessary-dunder-call 18 | "PLC3002", # unnecessary-direct-lambda-call 19 | "E999", # syntax-error 20 | "PLE0101", # return-in-init 21 | "F706", # return-outside-function 22 | "F704", # yield-outside-function 23 | "PLE0116", # continue-in-finally 24 | "PLE0117", # nonlocal-without-binding 25 | "PLE0241", # duplicate-bases 26 | "PLE0302", # unexpected-special-method-signature 27 | "PLE0604", # invalid-all-object 28 | "PLE0605", # invalid-all-format 29 | "PLE0643", # potential-index-error 30 | "PLE0704", # misplaced-bare-raise 31 | "PLE1141", # dict-iter-missing-items 32 | "PLE1142", # await-outside-async 33 | "PLE1205", # logging-too-many-args 34 | "PLE1206", # logging-too-few-args 35 | "PLE1307", # bad-string-format-type 36 | "PLE1310", # bad-str-strip-call 37 | "PLE1507", # invalid-envvar-value 38 | "PLE2502", # bidirectional-unicode 39 | "PLE2510", # invalid-character-backspace 40 | "PLE2512", # invalid-character-sub 41 | "PLE2513", # invalid-character-esc 42 | "PLE2514", # invalid-character-nul 43 | "PLE2515", # invalid-character-zero-width-space 44 | "PLR0124", # comparison-with-itself 45 | "PLR0202", # no-classmethod-decorator 46 | "PLR0203", # no-staticmethod-decorator 47 | "UP004", # useless-object-inheritance 48 | "PLR0206", # property-with-parameters 49 | "PLR0904", # too-many-public-methods 50 | "PLR0911", # too-many-return-statements 51 | "PLR0912", # too-many-branches 52 | "PLR0913", # too-many-arguments 53 | "PLR0914", # too-many-locals 54 | "PLR0915", # too-many-statements 55 | "PLR0916", # too-many-boolean-expressions 56 | "PLR1702", # too-many-nested-blocks 57 | "PLR1704", # redefined-argument-from-local 58 | "PLR1711", # useless-return 59 | "C416", # unnecessary-comprehension 60 | "PLR1733", # unnecessary-dict-index-lookup 61 | "PLR1736", # unnecessary-list-index-lookup 62 | 63 | # ruff reports this rule is unstable 64 | #"PLR6301", # no-self-use 65 | 66 | "PLW0108", # unnecessary-lambda 67 | "PLW0120", # useless-else-on-loop 68 | "PLW0127", # self-assigning-variable 69 | "PLW0129", # assert-on-string-literal 70 | "B033", # duplicate-value 71 | "PLW0131", # named-expr-without-context 72 | "PLW0245", # super-without-brackets 73 | "PLW0406", # import-self 74 | "PLW0602", # global-variable-not-assigned 75 | "PLW0603", # global-statement 76 | "PLW0604", # global-at-module-level 77 | 78 | # fails on the try: import typing used by libraries 79 | #"F401", # unused-import 80 | 81 | "F841", # unused-variable 82 | "E722", # bare-except 83 | "PLW0711", # binary-op-exception 84 | "PLW1501", # bad-open-mode 85 | "PLW1508", # invalid-envvar-default 86 | "PLW1509", # subprocess-popen-preexec-fn 87 | "PLW2101", # useless-with-lock 88 | "PLW3301", # nested-min-max 89 | ] 90 | 91 | ignore = [ 92 | "PLR2004", # magic-value-comparison 93 | "UP030", # format literals 94 | "PLW1514", # unspecified-encoding 95 | 96 | ] 97 | 98 | [format] 99 | line-ending = "lf" 100 | -------------------------------------------------------------------------------- /tests/chunk_test.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 ladyada for Adafruit Industries 2 | # 3 | # SPDX-License-Identifier: Unlicense 4 | 5 | """Chunk Tests""" 6 | 7 | from unittest import mock 8 | 9 | import mocket 10 | 11 | import adafruit_requests 12 | 13 | IP = "1.2.3.4" 14 | HOST = "wifitest.adafruit.com" 15 | PATH = "/testwifi/index.html" 16 | TEXT = b"This is a test of Adafruit WiFi!\r\nIf you can read this, its working :)" 17 | HEADERS = b"HTTP/1.0 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n" 18 | HEADERS_EXTRA_SPACE = b"HTTP/1.0 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n" 19 | 20 | 21 | def _chunk(response, split, extra=b""): 22 | i = 0 23 | chunked = b"" 24 | while i < len(response): 25 | remaining = len(response) - i 26 | chunk_size = split 27 | if remaining < chunk_size: 28 | chunk_size = remaining 29 | new_i = i + chunk_size 30 | chunked += ( 31 | hex(chunk_size)[2:].encode("ascii") + extra + b"\r\n" + response[i:new_i] + b"\r\n" 32 | ) 33 | i = new_i 34 | # The final chunk is zero length. 35 | chunked += b"0\r\n\r\n" 36 | return chunked 37 | 38 | 39 | def do_test_get_text( 40 | extra=b"", 41 | ): 42 | pool = mocket.MocketPool() 43 | pool.getaddrinfo.return_value = ((None, None, None, None, (IP, 80)),) 44 | chunk = _chunk(TEXT, 33, extra) 45 | print(chunk) 46 | sock = mocket.Mocket(HEADERS + chunk) 47 | pool.socket.return_value = sock 48 | 49 | requests_session = adafruit_requests.Session(pool) 50 | response = requests_session.get("http://" + HOST + PATH) 51 | 52 | sock.connect.assert_called_once_with((IP, 80)) 53 | 54 | sock.send.assert_has_calls( 55 | [ 56 | mock.call(b"GET"), 57 | mock.call(b" /"), 58 | mock.call(b"testwifi/index.html"), 59 | mock.call(b" HTTP/1.1\r\n"), 60 | ] 61 | ) 62 | sock.send.assert_has_calls( 63 | [ 64 | mock.call(b"Host"), 65 | mock.call(b": "), 66 | mock.call(b"wifitest.adafruit.com"), 67 | ] 68 | ) 69 | assert response.text == str(TEXT, "utf-8") 70 | 71 | 72 | def test_get_text(): 73 | do_test_get_text() 74 | 75 | 76 | def test_get_text_extra(): 77 | do_test_get_text(b";blahblah; blah") 78 | 79 | 80 | def do_test_close_flush( 81 | extra=b"", 82 | ): 83 | """Test that a chunked response can be closed even when the 84 | request contents were not accessed.""" 85 | pool = mocket.MocketPool() 86 | pool.getaddrinfo.return_value = ((None, None, None, None, (IP, 80)),) 87 | chunk = _chunk(TEXT, 33, extra) 88 | print(chunk) 89 | sock = mocket.Mocket(HEADERS + chunk) 90 | pool.socket.return_value = sock 91 | 92 | requests_session = adafruit_requests.Session(pool) 93 | response = requests_session.get("http://" + HOST + PATH) 94 | 95 | sock.connect.assert_called_once_with((IP, 80)) 96 | 97 | sock.send.assert_has_calls( 98 | [ 99 | mock.call(b"GET"), 100 | mock.call(b" /"), 101 | mock.call(b"testwifi/index.html"), 102 | mock.call(b" HTTP/1.1\r\n"), 103 | ] 104 | ) 105 | sock.send.assert_has_calls( 106 | [ 107 | mock.call(b"Host"), 108 | mock.call(b": "), 109 | mock.call(b"wifitest.adafruit.com"), 110 | ] 111 | ) 112 | 113 | response.close() 114 | 115 | 116 | def test_close_flush(): 117 | do_test_close_flush() 118 | 119 | 120 | def test_close_flush_extra(): 121 | do_test_close_flush(b";blahblah; blah") 122 | 123 | 124 | def do_test_get_text_extra_space( 125 | extra=b"", 126 | ): 127 | pool = mocket.MocketPool() 128 | pool.getaddrinfo.return_value = ((None, None, None, None, (IP, 80)),) 129 | chunk = _chunk(TEXT, 33, extra) 130 | print(chunk) 131 | sock = mocket.Mocket(HEADERS_EXTRA_SPACE + chunk) 132 | pool.socket.return_value = sock 133 | 134 | requests_session = adafruit_requests.Session(pool) 135 | response = requests_session.get("http://" + HOST + PATH) 136 | 137 | sock.connect.assert_called_once_with((IP, 80)) 138 | 139 | sock.send.assert_has_calls( 140 | [ 141 | mock.call(b"GET"), 142 | mock.call(b" /"), 143 | mock.call(b"testwifi/index.html"), 144 | mock.call(b" HTTP/1.1\r\n"), 145 | ] 146 | ) 147 | sock.send.assert_has_calls( 148 | [ 149 | mock.call(b"Host"), 150 | mock.call(b": "), 151 | mock.call(b"wifitest.adafruit.com"), 152 | ] 153 | ) 154 | assert response.text == str(TEXT, "utf-8") 155 | -------------------------------------------------------------------------------- /tests/chunked_redirect_test.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 ladyada for Adafruit Industries 2 | # 3 | # SPDX-License-Identifier: Unlicense 4 | 5 | """Redirection Tests""" 6 | 7 | from unittest import mock 8 | 9 | import mocket 10 | from chunk_test import _chunk 11 | 12 | import adafruit_requests 13 | 14 | IP = "1.2.3.4" 15 | HOST = "docs.google.com" 16 | PATH_BASE = ( 17 | "/spreadsheets/d/e/2PACX-1vR1WjUKz35-ek6SiR5droDfvPp51MTds4wUs57vEZNh2uDfihSTPhTaiiRo" 18 | "vLbNe1mkeRgurppRJ_Zy/" 19 | ) 20 | PATH = PATH_BASE + "pub?output=tsv" 21 | 22 | FILE_REDIRECT = ( 23 | b"e@2PACX-1vR1WjUKz35-ek6SiR5droDfvPp51MTds4wUs57vEZNh2uDfihSTPhTai" 24 | b"iRovLbNe1mkeRgurppRJ_Zy?output=tsv" 25 | ) 26 | RELATIVE_RELATIVE_REDIRECT = ( 27 | b"370cmver1f290kjsnpar5ku2h9g/3llvt5u8njbvat22m9l19db1h4/1656191325000/109226138307867586192/*/" 28 | + FILE_REDIRECT 29 | ) 30 | RELATIVE_ABSOLUTE_REDIRECT = b"/pub/70cmver1f290kjsnpar5ku2h9g/" + RELATIVE_RELATIVE_REDIRECT 31 | ABSOLUTE_ABSOLUTE_REDIRECT = ( 32 | b"https://doc-14-2g-sheets.googleusercontent.com" + RELATIVE_ABSOLUTE_REDIRECT 33 | ) 34 | 35 | # response headers returned from the initial request 36 | HEADERS_REDIRECT = ( 37 | b"HTTP/1.1 307 Temporary Redirect\r\n" 38 | b"Content-Type: text/html; charset=UTF-8\r\n" 39 | b"Cache-Control: no-cache, no-store, max-age=0, must-revalidate\r\n" 40 | b"Pragma: no-cache\r\n" 41 | b"Expires: Mon, 01 Jan 1990 00:00:00 GMT\r\n" 42 | b"Date: Sat, 25 Jun 2022 21:08:48 GMT\r\n" 43 | b"Location: NEW_LOCATION_HERE\r\n" 44 | b'P3P: CP="This is not a P3P policy! See g.co/p3phelp for more info."\r\n' 45 | b"X-Content-Type-Options: nosniff\r\n" 46 | b"X-XSS-Protection: 1; mode=block\r\n" 47 | b"Server: GSE\r\n" 48 | b"Set-Cookie: NID=511=EcnO010Porg0NIrxM8tSG6MhfQiVtWrQS42CjhKEpzwIvzBj2PFYH0-H_N--EAXaPBkR2j" 49 | b"FjAWEAxIJNqhvKb0vswOWp9hqcCrO51S8kO5I4C3" 50 | b"Is2ctWe1b_ysRU-6hjnJyLHzqjXotAWzEmr_qA3bpqWDwlRaQIiC6SvxM8L0M; expires=Sun, 25-Dec-2022 " 51 | b"21:08:48 GMT; path=/; " 52 | b"domain=.google.com; HttpOnly\r\n" 53 | b'Alt-Svc: h3=":443"; ma=2592000,h3-29=":443"; ma=2592000,h3-Q050=":443"; ' 54 | b'ma=2592000,h3-Q046=":443";' 55 | b' ma=2592000,h3-Q043=":443"; ma=2592000,quic=":443"; ma=2592000; v="46,43"\r\n' 56 | b"Accept-Ranges: none\r\n" 57 | b"Vary: Accept-Encoding\r\n" 58 | b"Transfer-Encoding: chunked\r\n\r\n" 59 | ) 60 | 61 | # response body returned from the initial request (needs to be chunked.) 62 | BODY_REDIRECT = ( 63 | b"\n\nTemporary Redirect\n\n" 64 | b'\n' 65 | b"

Temporary Redirect

\nThe document has moved " 66 | b'here.\n\n\n' 71 | ) 72 | 73 | # response headers from the request to the redirected location 74 | HEADERS = ( 75 | b"HTTP/1.1 200 OK\r\n" 76 | b"Content-Type: text/tab-separated-values\r\n" 77 | b"X-Frame-Options: ALLOW-FROM https://docs.google.com\r\n" 78 | b"X-Robots-Tag: noindex, nofollow, nosnippet\r\n" 79 | b"Expires: Sat, 25 Jun 2022 21:08:49 GMT\r\n" 80 | b"Date: Sat, 25 Jun 2022 21:08:49 GMT\r\n" 81 | b"Cache-Control: private, max-age=300\r\n" 82 | b'Content-Disposition: attachment; filename="WeeklyPlanner-Sheet1.tsv"; ' 83 | b"filename*=UTF-8''Weekly%20Planner%20-%20Sheet1.tsv\r\n" 84 | b"Access-Control-Allow-Origin: *\r\n" 85 | b"Access-Control-Expose-Headers: Cache-Control,Content-Disposition,Content-Encoding," 86 | b"Content-Length,Content-Type,Date,Expires,Server,Transfer-Encoding\r\n" 87 | b"Content-Security-Policy: frame-ancestors 'self' https://docs.google.com\r\n" 88 | b"Content-Security-Policy: base-uri 'self';object-src 'self';report-uri https://" 89 | b"doc-14-2g-sheets.googleusercontent.com/spreadsheets/cspreport;" 90 | b"script-src 'report-sample' 'nonce-6V57medLoq3hw2BWeyGu_A' 'unsafe-inline' 'strict-dynamic'" 91 | b" https: http: 'unsafe-eval';worker-src 'self' blob:\r\n" 92 | b"X-Content-Type-Options: nosniff\r\n" 93 | b"X-XSS-Protection: 1; mode=block\r\n" 94 | b"Server: GSE\r\n" 95 | b'Alt-Svc: h3=":443"; ma=2592000,h3-29=":443"; ma=2592000,h3-Q050=":443"; ' 96 | b'ma=2592000,h3-Q046=":443"; ma=2592000,h3-Q043=":443";' 97 | b' ma=2592000,quic=":443"; ma=2592000; v="46,43"\r\n' 98 | b"Accept-Ranges: none\r\n" 99 | b"Vary: Accept-Encoding\r\n" 100 | b"Transfer-Encoding: chunked\r\n\r\n" 101 | ) 102 | 103 | # response body from the request to the redirected location (needs to be chunked.) 104 | BODY = ( 105 | b"Sunday\tMonday\tTuesday\tWednesday\tThursday\tFriday\tSaturday\r\n" 106 | b"Rest\tSpin class\tRowing\tWerewolf Twitter\tWeights\tLaundry\tPoke bowl\r\n" 107 | b"\t\tZoom call\tShow & Tell\t\tMow lawn\tSynth Riders\r\n" 108 | b"\t\tTacos\tAsk an Engineer\t\tTrash pickup\t\r\n" 109 | b"\t\t\t\t\tLeg day\t\r\n" 110 | b"\t\t\t\t\tPizza night\t" 111 | ) 112 | 113 | 114 | class MocketRecvInto(mocket.Mocket): 115 | """A special Mocket to cap the number of bytes returned from recv_into()""" 116 | 117 | def __init__(self, response): 118 | super().__init__(response) 119 | self.recv_into = mock.Mock(side_effect=self._recv_into) 120 | 121 | def _recv_into(self, buf, nbytes=0): 122 | assert isinstance(nbytes, int) and nbytes >= 0 123 | read = nbytes if nbytes > 0 else len(buf) 124 | remaining = len(self._response) - self._position 125 | read = min(read, remaining, 205) 126 | end = self._position + read 127 | buf[:read] = self._response[self._position : end] 128 | self._position = end 129 | return read 130 | 131 | 132 | def test_chunked_absolute_absolute_redirect(): 133 | pool = mocket.MocketPool() 134 | pool.getaddrinfo.return_value = ((None, None, None, None, (IP, 443)),) 135 | chunk = _chunk(BODY_REDIRECT, len(BODY_REDIRECT)) 136 | chunk2 = _chunk(BODY, len(BODY)) 137 | 138 | redirect = HEADERS_REDIRECT.replace(b"NEW_LOCATION_HERE", ABSOLUTE_ABSOLUTE_REDIRECT) 139 | sock1 = MocketRecvInto(redirect + chunk) 140 | sock2 = mocket.Mocket(HEADERS + chunk2) 141 | pool.socket.side_effect = (sock1, sock2) 142 | 143 | requests_session = adafruit_requests.Session(pool, mocket.SSLContext()) 144 | response = requests_session.get("https://" + HOST + PATH) 145 | 146 | sock1.connect.assert_called_once_with((HOST, 443)) 147 | sock2.connect.assert_called_once_with(("doc-14-2g-sheets.googleusercontent.com", 443)) 148 | sock2.send.assert_has_calls([mock.call(RELATIVE_ABSOLUTE_REDIRECT[1:])]) 149 | 150 | assert response.text == str(BODY, "utf-8") 151 | 152 | 153 | def test_chunked_relative_absolute_redirect(): 154 | pool = mocket.MocketPool() 155 | pool.getaddrinfo.return_value = ((None, None, None, None, (IP, 443)),) 156 | chunk = _chunk(BODY_REDIRECT, len(BODY_REDIRECT)) 157 | chunk2 = _chunk(BODY, len(BODY)) 158 | 159 | redirect = HEADERS_REDIRECT.replace(b"NEW_LOCATION_HERE", RELATIVE_ABSOLUTE_REDIRECT) 160 | sock1 = MocketRecvInto(redirect + chunk) 161 | sock2 = mocket.Mocket(HEADERS + chunk2) 162 | pool.socket.side_effect = (sock1, sock2) 163 | 164 | requests_session = adafruit_requests.Session(pool, mocket.SSLContext()) 165 | response = requests_session.get("https://" + HOST + PATH) 166 | 167 | sock1.connect.assert_called_once_with((HOST, 443)) 168 | sock2.connect.assert_called_once_with(("docs.google.com", 443)) 169 | sock2.send.assert_has_calls([mock.call(RELATIVE_ABSOLUTE_REDIRECT[1:])]) 170 | 171 | assert response.text == str(BODY, "utf-8") 172 | 173 | 174 | def test_chunked_relative_relative_redirect(): 175 | pool = mocket.MocketPool() 176 | pool.getaddrinfo.return_value = ((None, None, None, None, (IP, 443)),) 177 | chunk = _chunk(BODY_REDIRECT, len(BODY_REDIRECT)) 178 | chunk2 = _chunk(BODY, len(BODY)) 179 | 180 | redirect = HEADERS_REDIRECT.replace(b"NEW_LOCATION_HERE", RELATIVE_RELATIVE_REDIRECT) 181 | sock1 = MocketRecvInto(redirect + chunk) 182 | sock2 = mocket.Mocket(HEADERS + chunk2) 183 | pool.socket.side_effect = (sock1, sock2) 184 | 185 | requests_session = adafruit_requests.Session(pool, mocket.SSLContext()) 186 | response = requests_session.get("https://" + HOST + PATH) 187 | 188 | sock1.connect.assert_called_once_with((HOST, 443)) 189 | sock2.connect.assert_called_once_with(("docs.google.com", 443)) 190 | sock2.send.assert_has_calls( 191 | [mock.call(bytes(PATH_BASE[1:], "utf-8") + RELATIVE_RELATIVE_REDIRECT)] 192 | ) 193 | 194 | assert response.text == str(BODY, "utf-8") 195 | 196 | 197 | def test_chunked_relative_relative_redirect_backstep(): 198 | pool = mocket.MocketPool() 199 | pool.getaddrinfo.return_value = ((None, None, None, None, (IP, 443)),) 200 | chunk = _chunk(BODY_REDIRECT, len(BODY_REDIRECT)) 201 | chunk2 = _chunk(BODY, len(BODY)) 202 | 203 | remove_paths = 2 204 | backstep = b"../" * remove_paths 205 | path_base_parts = PATH_BASE.split("/") 206 | # PATH_BASE starts with "/" so skip it and also remove from the count 207 | path_base = "/".join(path_base_parts[1 : len(path_base_parts) - remove_paths - 1]) + "/" 208 | 209 | redirect = HEADERS_REDIRECT.replace(b"NEW_LOCATION_HERE", backstep + RELATIVE_RELATIVE_REDIRECT) 210 | sock1 = MocketRecvInto(redirect + chunk) 211 | sock2 = mocket.Mocket(HEADERS + chunk2) 212 | pool.socket.side_effect = (sock1, sock2) 213 | 214 | requests_session = adafruit_requests.Session(pool, mocket.SSLContext()) 215 | response = requests_session.get("https://" + HOST + PATH) 216 | 217 | sock1.connect.assert_called_once_with((HOST, 443)) 218 | sock2.connect.assert_called_once_with(("docs.google.com", 443)) 219 | sock2.send.assert_has_calls([mock.call(bytes(path_base, "utf-8") + RELATIVE_RELATIVE_REDIRECT)]) 220 | 221 | assert response.text == str(BODY, "utf-8") 222 | 223 | 224 | def test_chunked_allow_redirects_false(): 225 | pool = mocket.MocketPool() 226 | pool.getaddrinfo.return_value = ((None, None, None, None, (IP, 443)),) 227 | chunk = _chunk(BODY_REDIRECT, len(BODY_REDIRECT)) 228 | chunk2 = _chunk(BODY, len(BODY)) 229 | 230 | redirect = HEADERS_REDIRECT.replace(b"NEW_LOCATION_HERE", ABSOLUTE_ABSOLUTE_REDIRECT) 231 | sock1 = MocketRecvInto(redirect + chunk) 232 | sock2 = mocket.Mocket(HEADERS + chunk2) 233 | pool.socket.side_effect = (sock1, sock2) 234 | 235 | requests_session = adafruit_requests.Session(pool, mocket.SSLContext()) 236 | response = requests_session.get("https://" + HOST + PATH, allow_redirects=False) 237 | 238 | sock1.connect.assert_called_once_with((HOST, 443)) 239 | sock2.connect.assert_not_called() 240 | 241 | assert response.text == str(BODY_REDIRECT, "utf-8") 242 | -------------------------------------------------------------------------------- /tests/concurrent_test.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 ladyada for Adafruit Industries 2 | # 3 | # SPDX-License-Identifier: Unlicense 4 | 5 | """Concurrent Tests""" 6 | 7 | import errno 8 | from unittest import mock 9 | 10 | import mocket 11 | 12 | 13 | def test_second_connect_fails_memoryerror(pool, requests_ssl): 14 | sock = mocket.Mocket() 15 | sock2 = mocket.Mocket() 16 | sock3 = mocket.Mocket() 17 | pool.socket.call_count = 0 # Reset call count 18 | pool.socket.side_effect = [sock, sock2, sock3] 19 | sock2.connect.side_effect = MemoryError() 20 | 21 | response = requests_ssl.get("https://" + mocket.MOCK_ENDPOINT_1) 22 | 23 | sock.send.assert_has_calls( 24 | [ 25 | mock.call(b"testwifi/index.html"), 26 | ] 27 | ) 28 | 29 | sock.send.assert_has_calls( 30 | [ 31 | mock.call(b"Host"), 32 | mock.call(b": "), 33 | mock.call(b"wifitest.adafruit.com"), 34 | mock.call(b"\r\n"), 35 | ] 36 | ) 37 | assert response.text == str(mocket.MOCK_RESPONSE_TEXT, "utf-8") 38 | 39 | requests_ssl.get("https://" + mocket.MOCK_ENDPOINT_2) 40 | 41 | sock.connect.assert_called_once_with((mocket.MOCK_HOST_1, 443)) 42 | sock2.connect.assert_called_once_with((mocket.MOCK_HOST_2, 443)) 43 | sock3.connect.assert_called_once_with((mocket.MOCK_HOST_2, 443)) 44 | # Make sure that the socket is closed after send fails. 45 | sock.close.assert_called_once() 46 | sock2.close.assert_called_once() 47 | assert sock3.close.call_count == 0 48 | assert pool.socket.call_count == 3 49 | 50 | 51 | def test_second_connect_fails_oserror(pool, requests_ssl): 52 | sock = mocket.Mocket() 53 | sock2 = mocket.Mocket() 54 | sock3 = mocket.Mocket() 55 | pool.socket.call_count = 0 # Reset call count 56 | pool.socket.side_effect = [sock, sock2, sock3] 57 | sock2.connect.side_effect = OSError(errno.ENOMEM) 58 | 59 | response = requests_ssl.get("https://" + mocket.MOCK_ENDPOINT_1) 60 | 61 | sock.send.assert_has_calls( 62 | [ 63 | mock.call(b"testwifi/index.html"), 64 | ] 65 | ) 66 | 67 | sock.send.assert_has_calls( 68 | [ 69 | mock.call(b"Host"), 70 | mock.call(b": "), 71 | mock.call(b"wifitest.adafruit.com"), 72 | mock.call(b"\r\n"), 73 | ] 74 | ) 75 | assert response.text == str(mocket.MOCK_RESPONSE_TEXT, "utf-8") 76 | 77 | requests_ssl.get("https://" + mocket.MOCK_ENDPOINT_2) 78 | 79 | sock.connect.assert_called_once_with((mocket.MOCK_HOST_1, 443)) 80 | sock2.connect.assert_called_once_with((mocket.MOCK_HOST_2, 443)) 81 | sock3.connect.assert_called_once_with((mocket.MOCK_HOST_2, 443)) 82 | # Make sure that the socket is closed after send fails. 83 | sock.close.assert_called_once() 84 | sock2.close.assert_called_once() 85 | assert sock3.close.call_count == 0 86 | assert pool.socket.call_count == 3 87 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023 Justin Myers for Adafruit Industries 2 | # 3 | # SPDX-License-Identifier: Unlicense 4 | 5 | """PyTest Setup""" 6 | 7 | import adafruit_connection_manager 8 | import mocket 9 | import pytest 10 | 11 | import adafruit_requests 12 | 13 | 14 | @pytest.fixture(autouse=True) 15 | def reset_connection_manager(monkeypatch): 16 | """Reset the ConnectionManager, since it's a singlton and will hold data""" 17 | monkeypatch.setattr( 18 | "adafruit_requests.get_connection_manager", 19 | adafruit_connection_manager.ConnectionManager, 20 | ) 21 | 22 | 23 | @pytest.fixture 24 | def sock(): 25 | return mocket.Mocket(mocket.MOCK_RESPONSE) 26 | 27 | 28 | @pytest.fixture 29 | def pool(sock): 30 | pool = mocket.MocketPool() 31 | pool.getaddrinfo.return_value = ((None, None, None, None, (mocket.MOCK_POOL_IP, 80)),) 32 | pool.socket.return_value = sock 33 | return pool 34 | 35 | 36 | @pytest.fixture 37 | def requests(pool): 38 | return adafruit_requests.Session(pool) 39 | 40 | 41 | @pytest.fixture 42 | def requests_ssl(pool): 43 | ssl_context = mocket.SSLContext() 44 | return adafruit_requests.Session(pool, ssl_context) 45 | -------------------------------------------------------------------------------- /tests/files/green_red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adafruit/Adafruit_CircuitPython_Requests/21255976fe061946cd353e416e254effb45ff12a/tests/files/green_red.png -------------------------------------------------------------------------------- /tests/files/green_red.png.license: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Justin Myers 2 | # SPDX-License-Identifier: Unlicense 3 | -------------------------------------------------------------------------------- /tests/files/red_green.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adafruit/Adafruit_CircuitPython_Requests/21255976fe061946cd353e416e254effb45ff12a/tests/files/red_green.png -------------------------------------------------------------------------------- /tests/files/red_green.png.license: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Justin Myers 2 | # SPDX-License-Identifier: Unlicense 3 | -------------------------------------------------------------------------------- /tests/files_test.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Justin Myers 2 | # 3 | # SPDX-License-Identifier: Unlicense 4 | 5 | """Post Files Tests""" 6 | # pylint: disable=line-too-long 7 | 8 | import re 9 | from unittest import mock 10 | 11 | import mocket 12 | import pytest 13 | import requests as python_requests 14 | 15 | 16 | @pytest.fixture 17 | def log_stream(): 18 | return [] 19 | 20 | 21 | @pytest.fixture 22 | def post_url(): 23 | return "https://httpbin.org/post" 24 | 25 | 26 | @pytest.fixture 27 | def request_logging(log_stream): 28 | """Reset the ConnectionManager, since it's a singlton and will hold data""" 29 | import http.client # pylint: disable=import-outside-toplevel 30 | 31 | def httpclient_log(*args): 32 | log_stream.append(args) 33 | 34 | http.client.print = httpclient_log 35 | http.client.HTTPConnection.debuglevel = 1 36 | 37 | 38 | def get_actual_request_data(log_stream): 39 | boundary_pattern = r"(?<=boundary=)(.\w*)" 40 | content_length_pattern = r"(?<=Content-Length: )(.\d*)" 41 | 42 | boundary = "" 43 | actual_request_post = "" 44 | content_length = "" 45 | for log in log_stream: 46 | for log_arg in log: 47 | boundary_search = re.findall(boundary_pattern, log_arg) 48 | content_length_search = re.findall(content_length_pattern, log_arg) 49 | if boundary_search: 50 | boundary = boundary_search[0] 51 | if content_length_search: 52 | content_length = content_length_search[0] 53 | if "Content-Disposition" in log_arg or "\\x" in log_arg: 54 | # this will look like: 55 | # b\'{content}\' 56 | # and escaped characters look like: 57 | # \\r 58 | post_data = log_arg[2:-1] 59 | post_bytes = post_data.encode("utf-8") 60 | post_unescaped = post_bytes.decode("unicode_escape") 61 | actual_request_post = post_unescaped.encode("latin1") 62 | 63 | return boundary, content_length, actual_request_post 64 | 65 | 66 | def test_post_file_as_data( # pylint: disable=unused-argument 67 | requests, sock, log_stream, post_url, request_logging 68 | ): 69 | with open("tests/files/red_green.png", "rb") as file_1: 70 | python_requests.post(post_url, data=file_1, timeout=30) 71 | __, content_length, actual_request_post = get_actual_request_data(log_stream) 72 | 73 | requests.post("http://" + mocket.MOCK_HOST_1 + "/post", data=file_1) 74 | 75 | sock.connect.assert_called_once_with((mocket.MOCK_POOL_IP, 80)) 76 | sock.send.assert_has_calls( 77 | [ 78 | mock.call(b"Content-Length"), 79 | mock.call(b": "), 80 | mock.call(content_length.encode()), 81 | mock.call(b"\r\n"), 82 | ] 83 | ) 84 | sent = b"".join(sock.sent_data) 85 | assert sent.endswith(actual_request_post) 86 | 87 | 88 | def test_post_files_text( # pylint: disable=unused-argument 89 | sock, requests, log_stream, post_url, request_logging 90 | ): 91 | file_data = { 92 | "key_4": (None, "Value 5"), 93 | } 94 | 95 | python_requests.post(post_url, files=file_data, timeout=30) 96 | boundary, content_length, actual_request_post = get_actual_request_data(log_stream) 97 | 98 | requests._build_boundary_string = mock.Mock(return_value=boundary) 99 | requests.post("http://" + mocket.MOCK_HOST_1 + "/post", files=file_data) 100 | 101 | sock.connect.assert_called_once_with((mocket.MOCK_POOL_IP, 80)) 102 | sock.send.assert_has_calls( 103 | [ 104 | mock.call(b"Content-Type"), 105 | mock.call(b": "), 106 | mock.call(f"multipart/form-data; boundary={boundary}".encode()), 107 | mock.call(b"\r\n"), 108 | ] 109 | ) 110 | sock.send.assert_has_calls( 111 | [ 112 | mock.call(b"Content-Length"), 113 | mock.call(b": "), 114 | mock.call(content_length.encode()), 115 | mock.call(b"\r\n"), 116 | ] 117 | ) 118 | 119 | sent = b"".join(sock.sent_data) 120 | assert sent.endswith(actual_request_post) 121 | 122 | 123 | def test_post_files_file( # pylint: disable=unused-argument 124 | sock, requests, log_stream, post_url, request_logging 125 | ): 126 | with open("tests/files/red_green.png", "rb") as file_1: 127 | file_data = { 128 | "file_1": ( 129 | "red_green.png", 130 | file_1, 131 | "image/png", 132 | { 133 | "Key_1": "Value 1", 134 | "Key_2": "Value 2", 135 | "Key_3": "Value 3", 136 | }, 137 | ), 138 | } 139 | 140 | python_requests.post(post_url, files=file_data, timeout=30) 141 | boundary, content_length, actual_request_post = get_actual_request_data(log_stream) 142 | 143 | requests._build_boundary_string = mock.Mock(return_value=boundary) 144 | requests.post("http://" + mocket.MOCK_HOST_1 + "/post", files=file_data) 145 | 146 | sock.connect.assert_called_once_with((mocket.MOCK_POOL_IP, 80)) 147 | sock.send.assert_has_calls( 148 | [ 149 | mock.call(b"Content-Type"), 150 | mock.call(b": "), 151 | mock.call(f"multipart/form-data; boundary={boundary}".encode()), 152 | mock.call(b"\r\n"), 153 | ] 154 | ) 155 | sock.send.assert_has_calls( 156 | [ 157 | mock.call(b"Content-Length"), 158 | mock.call(b": "), 159 | mock.call(content_length.encode()), 160 | mock.call(b"\r\n"), 161 | ] 162 | ) 163 | sent = b"".join(sock.sent_data) 164 | assert sent.endswith(actual_request_post) 165 | 166 | 167 | def test_post_files_complex( # pylint: disable=unused-argument 168 | sock, requests, log_stream, post_url, request_logging 169 | ): 170 | with open("tests/files/red_green.png", "rb") as file_1, open( 171 | "tests/files/green_red.png", "rb" 172 | ) as file_2: 173 | file_data = { 174 | "file_1": ( 175 | "red_green.png", 176 | file_1, 177 | "image/png", 178 | { 179 | "Key_1": "Value 1", 180 | "Key_2": "Value 2", 181 | "Key_3": "Value 3", 182 | }, 183 | ), 184 | "key_4": (None, "Value 5"), 185 | "file_2": ( 186 | "green_red.png", 187 | file_2, 188 | "image/png", 189 | ), 190 | "key_6": (None, "Value 6"), 191 | } 192 | 193 | python_requests.post(post_url, files=file_data, timeout=30) 194 | boundary, content_length, actual_request_post = get_actual_request_data(log_stream) 195 | 196 | requests._build_boundary_string = mock.Mock(return_value=boundary) 197 | requests.post("http://" + mocket.MOCK_HOST_1 + "/post", files=file_data) 198 | 199 | sock.connect.assert_called_once_with((mocket.MOCK_POOL_IP, 80)) 200 | sock.send.assert_has_calls( 201 | [ 202 | mock.call(b"Content-Type"), 203 | mock.call(b": "), 204 | mock.call(f"multipart/form-data; boundary={boundary}".encode()), 205 | mock.call(b"\r\n"), 206 | ] 207 | ) 208 | sock.send.assert_has_calls( 209 | [ 210 | mock.call(b"Content-Length"), 211 | mock.call(b": "), 212 | mock.call(content_length.encode()), 213 | mock.call(b"\r\n"), 214 | ] 215 | ) 216 | sent = b"".join(sock.sent_data) 217 | assert sent.endswith(actual_request_post) 218 | 219 | 220 | def test_post_files_not_binary(requests): 221 | with open("tests/files/red_green.png") as file_1: 222 | file_data = { 223 | "file_1": ( 224 | "red_green.png", 225 | file_1, 226 | "image/png", 227 | ), 228 | } 229 | 230 | with pytest.raises(ValueError) as context: 231 | requests.post("http://" + mocket.MOCK_HOST_1 + "/post", files=file_data) 232 | assert "Files must be opened in binary mode" in str(context) 233 | -------------------------------------------------------------------------------- /tests/header_test.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 ladyada for Adafruit Industries 2 | # 3 | # SPDX-License-Identifier: Unlicense 4 | 5 | """Header Tests""" 6 | 7 | import mocket 8 | import pytest 9 | 10 | 11 | def test_check_headers_not_dict(requests): 12 | with pytest.raises(TypeError) as context: 13 | requests._check_headers("") 14 | assert "Headers must be in dict format" in str(context) 15 | 16 | 17 | def test_check_headers_not_valid(requests): 18 | with pytest.raises(TypeError) as context: 19 | requests._check_headers({"Good1": "a", "Good2": b"b", "Good3": None, "Bad1": True}) 20 | assert "Header part (True) from Bad1 must be of type str or bytes" in str(context) 21 | 22 | 23 | def test_check_headers_valid(requests): 24 | requests._check_headers({"Good1": "a", "Good2": b"b", "Good3": None}) 25 | assert True 26 | 27 | 28 | def test_host(sock, requests): 29 | headers = {} 30 | requests.get("http://" + mocket.MOCK_HOST_1 + "/get", headers=headers) 31 | 32 | sock.connect.assert_called_once_with((mocket.MOCK_POOL_IP, 80)) 33 | sent = b"".join(sock.sent_data) 34 | assert b"Host: wifitest.adafruit.com\r\n" in sent 35 | 36 | 37 | def test_host_replace(sock, requests): 38 | headers = {"host": mocket.MOCK_POOL_IP} 39 | requests.get("http://" + mocket.MOCK_HOST_1 + "/get", headers=headers) 40 | 41 | sock.connect.assert_called_once_with((mocket.MOCK_POOL_IP, 80)) 42 | sent = b"".join(sock.sent_data) 43 | assert b"host: 10.10.10.10\r\n" in sent 44 | assert b"Host: wifitest.adafruit.com\r\n" not in sent 45 | assert sent.lower().count(b"host:") == 1 46 | 47 | 48 | def test_user_agent(sock, requests): 49 | headers = {} 50 | requests.get("http://" + mocket.MOCK_HOST_1 + "/get", headers=headers) 51 | 52 | sock.connect.assert_called_once_with((mocket.MOCK_POOL_IP, 80)) 53 | sent = b"".join(sock.sent_data) 54 | assert b"User-Agent: Adafruit CircuitPython\r\n" in sent 55 | 56 | 57 | def test_user_agent_replace(sock, requests): 58 | headers = {"user-agent": "blinka/1.0.0"} 59 | requests.get("http://" + mocket.MOCK_HOST_1 + "/get", headers=headers) 60 | 61 | sock.connect.assert_called_once_with((mocket.MOCK_POOL_IP, 80)) 62 | sent = b"".join(sock.sent_data) 63 | assert b"user-agent: blinka/1.0.0\r\n" in sent 64 | assert b"User-Agent: Adafruit CircuitPython\r\n" not in sent 65 | assert sent.lower().count(b"user-agent:") == 1 66 | 67 | 68 | def test_content_type(sock, requests): 69 | headers = {} 70 | data = {"test": True} 71 | requests.post("http://" + mocket.MOCK_HOST_1 + "/get", data=data, headers=headers) 72 | 73 | sock.connect.assert_called_once_with((mocket.MOCK_POOL_IP, 80)) 74 | sent = b"".join(sock.sent_data) 75 | assert b"Content-Type: application/x-www-form-urlencoded\r\n" in sent 76 | 77 | 78 | def test_content_type_replace(sock, requests): 79 | headers = {"content-type": "application/test"} 80 | data = {"test": True} 81 | requests.post("http://" + mocket.MOCK_HOST_1 + "/get", data=data, headers=headers) 82 | 83 | sock.connect.assert_called_once_with((mocket.MOCK_POOL_IP, 80)) 84 | sent = b"".join(sock.sent_data) 85 | assert b"content-type: application/test\r\n" in sent 86 | assert b"Content-Type: application/x-www-form-urlencoded\r\n" not in sent 87 | assert sent.lower().count(b"content-type:") == 1 88 | -------------------------------------------------------------------------------- /tests/local_test_server.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2025 Tim Cocks 2 | # 3 | # SPDX-License-Identifier: MIT 4 | import json 5 | from http.server import SimpleHTTPRequestHandler 6 | 7 | 8 | class LocalTestServerHandler(SimpleHTTPRequestHandler): 9 | def do_GET(self): 10 | if self.path == "/get": 11 | resp_body = json.dumps({"url": "http://localhost:5000/get"}).encode("utf-8") 12 | self.send_response(200) 13 | self.send_header("Content-type", "application/json") 14 | self.send_header("Content-Length", str(len(resp_body))) 15 | self.end_headers() 16 | self.wfile.write(resp_body) 17 | if self.path.startswith("/status"): 18 | try: 19 | requested_status = int(self.path.split("/")[2]) 20 | except ValueError: 21 | resp_body = json.dumps({"error": "requested status code must be int"}).encode( 22 | "utf-8" 23 | ) 24 | self.send_response(400) 25 | self.send_header("Content-type", "application/json") 26 | self.send_header("Content-Length", str(len(resp_body))) 27 | self.end_headers() 28 | self.wfile.write(resp_body) 29 | return 30 | 31 | if requested_status != 204: 32 | self.send_response(requested_status) 33 | self.send_header("Content-type", "text/html") 34 | self.send_header("Content-Length", "0") 35 | else: 36 | self.send_response(requested_status) 37 | self.send_header("Content-type", "text/html") 38 | self.end_headers() 39 | -------------------------------------------------------------------------------- /tests/method_test.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 ladyada for Adafruit Industries 2 | # 3 | # SPDX-License-Identifier: Unlicense 4 | 5 | """Post Tests""" 6 | 7 | from unittest import mock 8 | 9 | import mocket 10 | import pytest 11 | 12 | 13 | @pytest.mark.parametrize( 14 | "call", 15 | ( 16 | "DELETE", 17 | "GET", 18 | "HEAD", 19 | "OPTIONS", 20 | "PATCH", 21 | "POST", 22 | "PUT", 23 | ), 24 | ) 25 | def test_methods(call, sock, requests): 26 | method = getattr(requests, call.lower()) 27 | method("http://" + mocket.MOCK_HOST_1 + "/" + call.lower()) 28 | sock.connect.assert_called_once_with((mocket.MOCK_POOL_IP, 80)) 29 | 30 | sock.send.assert_has_calls( 31 | [ 32 | mock.call(bytes(call, "utf-8")), 33 | mock.call(b" /"), 34 | mock.call(bytes(call.lower(), "utf-8")), 35 | mock.call(b" HTTP/1.1\r\n"), 36 | ] 37 | ) 38 | sock.send.assert_has_calls( 39 | [ 40 | mock.call(b"Host"), 41 | mock.call(b": "), 42 | mock.call(b"wifitest.adafruit.com"), 43 | ] 44 | ) 45 | 46 | 47 | def test_post_string(sock, requests): 48 | data = "31F" 49 | requests.post("http://" + mocket.MOCK_HOST_1 + "/post", data=data) 50 | sock.connect.assert_called_once_with((mocket.MOCK_POOL_IP, 80)) 51 | sock.send.assert_called_with(b"31F") 52 | 53 | 54 | def test_post_form(sock, requests): 55 | data = { 56 | "Date": "July 25, 2019", 57 | "Time": "12:00", 58 | } 59 | requests.post("http://" + mocket.MOCK_HOST_1 + "/post", data=data) 60 | sock.connect.assert_called_once_with((mocket.MOCK_POOL_IP, 80)) 61 | sock.send.assert_has_calls( 62 | [ 63 | mock.call(b"Content-Type"), 64 | mock.call(b": "), 65 | mock.call(b"application/x-www-form-urlencoded"), 66 | mock.call(b"\r\n"), 67 | ] 68 | ) 69 | sock.send.assert_called_with(b"Date=July 25, 2019&Time=12:00") 70 | 71 | 72 | def test_post_json(sock, requests): 73 | json_data = { 74 | "Date": "July 25, 2019", 75 | "Time": "12:00", 76 | } 77 | requests.post("http://" + mocket.MOCK_HOST_1 + "/post", json=json_data) 78 | sock.connect.assert_called_once_with((mocket.MOCK_POOL_IP, 80)) 79 | sock.send.assert_has_calls( 80 | [ 81 | mock.call(b"Content-Type"), 82 | mock.call(b": "), 83 | mock.call(b"application/json"), 84 | mock.call(b"\r\n"), 85 | ] 86 | ) 87 | sock.send.assert_called_with(b'{"Date": "July 25, 2019", "Time": "12:00"}') 88 | -------------------------------------------------------------------------------- /tests/mocket.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 ladyada for Adafruit Industries 2 | # SPDX-FileCopyrightText: 2024 Justin Myers for Adafruit Industries 3 | # 4 | # SPDX-License-Identifier: Unlicense 5 | 6 | """Mock Socket""" 7 | 8 | from unittest import mock 9 | 10 | MOCK_POOL_IP = "10.10.10.10" 11 | MOCK_HOST_1 = "wifitest.adafruit.com" 12 | MOCK_HOST_2 = "wifitest2.adafruit.com" 13 | MOCK_PATH_1 = "/testwifi/index.html" 14 | MOCK_ENDPOINT_1 = MOCK_HOST_1 + MOCK_PATH_1 15 | MOCK_ENDPOINT_2 = MOCK_HOST_2 + MOCK_PATH_1 16 | MOCK_RESPONSE_TEXT = b"This is a test of Adafruit WiFi!\r\nIf you can read this, its working :)" 17 | MOCK_RESPONSE = b"HTTP/1.0 200 OK\r\nContent-Length: 70\r\n\r\n" + MOCK_RESPONSE_TEXT 18 | 19 | 20 | class MocketPool: 21 | """Mock SocketPool""" 22 | 23 | SOCK_STREAM = 0 24 | 25 | def __init__(self, radio=None): 26 | self.getaddrinfo = mock.Mock() 27 | self.getaddrinfo.return_value = ((None, None, None, None, (MOCK_POOL_IP, 80)),) 28 | self.socket = mock.Mock() 29 | 30 | 31 | class Mocket: 32 | """Mock Socket""" 33 | 34 | def __init__(self, response=MOCK_RESPONSE): 35 | self.settimeout = mock.Mock() 36 | self.close = mock.Mock() 37 | self.connect = mock.Mock() 38 | self.send = mock.Mock(side_effect=self._send) 39 | self.readline = mock.Mock(side_effect=self._readline) 40 | self.recv = mock.Mock(side_effect=self._recv) 41 | self.recv_into = mock.Mock(side_effect=self._recv_into) 42 | # Test helpers 43 | self._response = response 44 | self._position = 0 45 | self.fail_next_send = False 46 | self.sent_data = [] 47 | 48 | def _send(self, data): 49 | if self.fail_next_send: 50 | self.fail_next_send = False 51 | return 0 52 | self.sent_data.append(data) 53 | return len(data) 54 | 55 | def _readline(self): 56 | i = self._response.find(b"\r\n", self._position) 57 | response = self._response[self._position : i + 2] 58 | self._position = i + 2 59 | return response 60 | 61 | def _recv(self, count): 62 | end = self._position + count 63 | response = self._response[self._position : end] 64 | self._position = end 65 | return response 66 | 67 | def _recv_into(self, buf, nbytes=0): 68 | assert isinstance(nbytes, int) and nbytes >= 0 69 | read = nbytes if nbytes > 0 else len(buf) 70 | remaining = len(self._response) - self._position 71 | read = min(read, remaining) 72 | end = self._position + read 73 | buf[:read] = self._response[self._position : end] 74 | self._position = end 75 | return read 76 | 77 | 78 | class SSLContext: 79 | """Mock SSL Context""" 80 | 81 | def __init__(self): 82 | self.wrap_socket = mock.Mock(side_effect=self._wrap_socket) 83 | 84 | def _wrap_socket(self, sock, server_hostname=None): 85 | return sock 86 | 87 | 88 | class MockRadio: 89 | class Radio: 90 | pass 91 | 92 | class ESP_SPIcontrol: 93 | TLS_MODE = 2 94 | 95 | class WIZNET5K: 96 | pass 97 | 98 | class Unsupported: 99 | pass 100 | -------------------------------------------------------------------------------- /tests/parse_test.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 ladyada for Adafruit Industries 2 | # 3 | # SPDX-License-Identifier: Unlicense 4 | 5 | """Parse Tests""" 6 | 7 | import json 8 | 9 | import mocket 10 | 11 | import adafruit_requests 12 | 13 | RESPONSE = {"Date": "July 25, 2019"} 14 | ENCODED = json.dumps(RESPONSE).encode("utf-8") 15 | # Padding here tests the case where a header line is exactly 32 bytes buffered by 16 | # aligning the Content-Type header after it. 17 | HEADERS = ( 18 | "HTTP/1.0 200 OK\r\npadding: 000\r\n" 19 | f"Content-Type: application/json\r\nContent-Length: {len(ENCODED)}\r\n\r\n" 20 | ).encode() 21 | 22 | 23 | def test_json(pool): 24 | sock = mocket.Mocket(HEADERS + ENCODED) 25 | pool.socket.return_value = sock 26 | 27 | requests_session = adafruit_requests.Session(pool) 28 | response = requests_session.get("http://" + mocket.MOCK_HOST_1 + "/get") 29 | sock.connect.assert_called_once_with((mocket.MOCK_POOL_IP, 80)) 30 | assert response.json() == RESPONSE 31 | -------------------------------------------------------------------------------- /tests/protocol_test.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 ladyada for Adafruit Industries 2 | # 3 | # SPDX-License-Identifier: Unlicense 4 | 5 | """Protocol Tests""" 6 | 7 | from unittest import mock 8 | 9 | import mocket 10 | import pytest 11 | 12 | 13 | def test_get_https_no_ssl(requests): 14 | with pytest.raises(ValueError): 15 | requests.get("https://" + mocket.MOCK_ENDPOINT_1) 16 | 17 | 18 | def test_get_https_text(sock, requests_ssl): 19 | response = requests_ssl.get("https://" + mocket.MOCK_ENDPOINT_1) 20 | 21 | sock.connect.assert_called_once_with((mocket.MOCK_HOST_1, 443)) 22 | 23 | sock.send.assert_has_calls( 24 | [ 25 | mock.call(b"GET"), 26 | mock.call(b" /"), 27 | mock.call(b"testwifi/index.html"), 28 | mock.call(b" HTTP/1.1\r\n"), 29 | ] 30 | ) 31 | sock.send.assert_has_calls( 32 | [ 33 | mock.call(b"Host"), 34 | mock.call(b": "), 35 | mock.call(b"wifitest.adafruit.com"), 36 | ] 37 | ) 38 | assert response.text == str(mocket.MOCK_RESPONSE_TEXT, "utf-8") 39 | 40 | # Close isn't needed but can be called to release the socket early. 41 | response.close() 42 | 43 | 44 | def test_get_http_text(sock, requests): 45 | response = requests.get("http://" + mocket.MOCK_ENDPOINT_1) 46 | 47 | sock.connect.assert_called_once_with((mocket.MOCK_POOL_IP, 80)) 48 | 49 | sock.send.assert_has_calls( 50 | [ 51 | mock.call(b"GET"), 52 | mock.call(b" /"), 53 | mock.call(b"testwifi/index.html"), 54 | mock.call(b" HTTP/1.1\r\n"), 55 | ] 56 | ) 57 | sock.send.assert_has_calls( 58 | [ 59 | mock.call(b"Host"), 60 | mock.call(b": "), 61 | mock.call(b"wifitest.adafruit.com"), 62 | ] 63 | ) 64 | assert response.text == str(mocket.MOCK_RESPONSE_TEXT, "utf-8") 65 | 66 | 67 | def test_get_close(sock, requests): 68 | """Test that a response can be closed without the contents being accessed.""" 69 | response = requests.get("http://" + mocket.MOCK_ENDPOINT_1) 70 | 71 | sock.connect.assert_called_once_with((mocket.MOCK_POOL_IP, 80)) 72 | 73 | sock.send.assert_has_calls( 74 | [ 75 | mock.call(b"GET"), 76 | mock.call(b" /"), 77 | mock.call(b"testwifi/index.html"), 78 | mock.call(b" HTTP/1.1\r\n"), 79 | ] 80 | ) 81 | sock.send.assert_has_calls( 82 | [ 83 | mock.call(b"Host"), 84 | mock.call(b": "), 85 | mock.call(b"wifitest.adafruit.com"), 86 | ] 87 | ) 88 | response.close() 89 | -------------------------------------------------------------------------------- /tests/real_call_test.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Justin Myers 2 | # 3 | # SPDX-License-Identifier: Unlicense 4 | 5 | """Real call Tests""" 6 | 7 | import socket 8 | import socketserver 9 | import ssl 10 | import threading 11 | import time 12 | 13 | import adafruit_connection_manager 14 | import pytest 15 | from local_test_server import LocalTestServerHandler 16 | 17 | import adafruit_requests 18 | 19 | 20 | def test_gets(): 21 | path_index = 0 22 | status_code_index = 1 23 | text_result_index = 2 24 | json_keys_index = 3 25 | cases = [ 26 | ("get", 200, None, {"url": "http://localhost:5000/get"}), 27 | ("status/200", 200, "", None), 28 | ("status/204", 204, "", None), 29 | ] 30 | 31 | with socketserver.TCPServer(("127.0.0.1", 5000), LocalTestServerHandler) as server: 32 | server_thread = threading.Thread(target=server.serve_forever) 33 | server_thread.daemon = True 34 | server_thread.start() 35 | 36 | time.sleep(2) # Give the server some time to start 37 | 38 | for case in cases: 39 | requests = adafruit_requests.Session(socket, ssl.create_default_context()) 40 | with requests.get(f"http://127.0.0.1:5000/{case[path_index]}") as response: 41 | assert response.status_code == case[status_code_index] 42 | if case[text_result_index] is not None: 43 | assert response.text == case[text_result_index] 44 | if case[json_keys_index] is not None: 45 | for key, value in case[json_keys_index].items(): 46 | assert response.json()[key] == value 47 | 48 | adafruit_connection_manager.connection_manager_close_all(release_references=True) 49 | 50 | server.shutdown() 51 | server.server_close() 52 | time.sleep(2) 53 | -------------------------------------------------------------------------------- /tests/reuse_test.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 ladyada for Adafruit Industries 2 | # 3 | # SPDX-License-Identifier: Unlicense 4 | 5 | """Reuse Tests""" 6 | 7 | from unittest import mock 8 | 9 | import mocket 10 | import pytest 11 | 12 | 13 | def test_get_twice(pool, requests_ssl): 14 | sock = mocket.Mocket(mocket.MOCK_RESPONSE + mocket.MOCK_RESPONSE) 15 | pool.socket.return_value = sock 16 | 17 | response = requests_ssl.get("https://" + mocket.MOCK_ENDPOINT_1) 18 | 19 | sock.send.assert_has_calls( 20 | [ 21 | mock.call(b"GET"), 22 | mock.call(b" /"), 23 | mock.call(b"testwifi/index.html"), 24 | mock.call(b" HTTP/1.1\r\n"), 25 | ] 26 | ) 27 | sock.send.assert_has_calls( 28 | [ 29 | mock.call(b"Host"), 30 | mock.call(b": "), 31 | mock.call(b"wifitest.adafruit.com"), 32 | ] 33 | ) 34 | assert response.text == str(mocket.MOCK_RESPONSE_TEXT, "utf-8") 35 | 36 | response = requests_ssl.get("https://" + mocket.MOCK_ENDPOINT_1 + "2") 37 | 38 | sock.send.assert_has_calls( 39 | [ 40 | mock.call(b"GET"), 41 | mock.call(b" /"), 42 | mock.call(b"testwifi/index.html2"), 43 | mock.call(b" HTTP/1.1\r\n"), 44 | ] 45 | ) 46 | sock.send.assert_has_calls( 47 | [ 48 | mock.call(b"Host"), 49 | mock.call(b": "), 50 | mock.call(b"wifitest.adafruit.com"), 51 | ] 52 | ) 53 | 54 | assert response.text == str(mocket.MOCK_RESPONSE_TEXT, "utf-8") 55 | sock.connect.assert_called_once_with((mocket.MOCK_HOST_1, 443)) 56 | pool.socket.assert_called_once() 57 | 58 | 59 | def test_get_twice_after_second(pool, requests_ssl): 60 | sock = mocket.Mocket( 61 | b"H" 62 | b"TTP/1.0 200 OK\r\nContent-Length: " 63 | b"70\r\n\r\nHTTP/1.0 2" 64 | b"H" 65 | b"TTP/1.0 200 OK\r\nContent-Length: " 66 | b"70\r\n\r\nHTTP/1.0 2" 67 | ) 68 | pool.socket.return_value = sock 69 | 70 | response = requests_ssl.get("https://" + mocket.MOCK_ENDPOINT_1) 71 | 72 | sock.send.assert_has_calls( 73 | [ 74 | mock.call(b"GET"), 75 | mock.call(b" /"), 76 | mock.call(b"testwifi/index.html"), 77 | mock.call(b" HTTP/1.1\r\n"), 78 | ] 79 | ) 80 | sock.send.assert_has_calls( 81 | [ 82 | mock.call(b"Host"), 83 | mock.call(b": "), 84 | mock.call(b"wifitest.adafruit.com"), 85 | ] 86 | ) 87 | 88 | requests_ssl.get("https://" + mocket.MOCK_ENDPOINT_1 + "2") 89 | 90 | sock.send.assert_has_calls( 91 | [ 92 | mock.call(b"GET"), 93 | mock.call(b" /"), 94 | mock.call(b"testwifi/index.html2"), 95 | mock.call(b" HTTP/1.1\r\n"), 96 | ] 97 | ) 98 | sock.send.assert_has_calls( 99 | [ 100 | mock.call(b"Host"), 101 | mock.call(b": "), 102 | mock.call(b"wifitest.adafruit.com"), 103 | ] 104 | ) 105 | sock.connect.assert_called_once_with((mocket.MOCK_HOST_1, 443)) 106 | pool.socket.assert_called_once() 107 | 108 | with pytest.raises(RuntimeError) as context: 109 | result = response.text # noqa: F841 Local variable not used 110 | assert "Newer Response closed this one. Use Responses immediately." in str(context) 111 | 112 | 113 | def test_connect_out_of_memory(pool, requests_ssl): 114 | sock = mocket.Mocket() 115 | sock2 = mocket.Mocket() 116 | sock3 = mocket.Mocket() 117 | pool.socket.side_effect = [sock, sock2, sock3] 118 | sock2.connect.side_effect = MemoryError() 119 | 120 | response = requests_ssl.get("https://" + mocket.MOCK_ENDPOINT_1) 121 | 122 | sock.send.assert_has_calls( 123 | [ 124 | mock.call(b"GET"), 125 | mock.call(b" /"), 126 | mock.call(b"testwifi/index.html"), 127 | mock.call(b" HTTP/1.1\r\n"), 128 | ] 129 | ) 130 | sock.send.assert_has_calls( 131 | [ 132 | mock.call(b"Host"), 133 | mock.call(b": "), 134 | mock.call(b"wifitest.adafruit.com"), 135 | ] 136 | ) 137 | assert response.text == str(mocket.MOCK_RESPONSE_TEXT, "utf-8") 138 | 139 | response = requests_ssl.get("https://" + mocket.MOCK_ENDPOINT_2) 140 | sock3.send.assert_has_calls( 141 | [ 142 | mock.call(b"GET"), 143 | mock.call(b" /"), 144 | mock.call(b"testwifi/index.html"), 145 | mock.call(b" HTTP/1.1\r\n"), 146 | ] 147 | ) 148 | sock3.send.assert_has_calls( 149 | [ 150 | mock.call(b"Host"), 151 | mock.call(b": "), 152 | mock.call(b"wifitest2.adafruit.com"), 153 | ] 154 | ) 155 | 156 | assert response.text == str(mocket.MOCK_RESPONSE_TEXT, "utf-8") 157 | sock.close.assert_called_once() 158 | sock.connect.assert_called_once_with((mocket.MOCK_HOST_1, 443)) 159 | sock3.connect.assert_called_once_with((mocket.MOCK_HOST_2, 443)) 160 | 161 | 162 | def test_second_send_fails(pool, requests_ssl): 163 | sock = mocket.Mocket() 164 | sock2 = mocket.Mocket() 165 | pool.socket.side_effect = [sock, sock2] 166 | 167 | response = requests_ssl.get("https://" + mocket.MOCK_ENDPOINT_1) 168 | 169 | sock.send.assert_has_calls( 170 | [ 171 | mock.call(b"testwifi/index.html"), 172 | ] 173 | ) 174 | 175 | sock.send.assert_has_calls( 176 | [ 177 | mock.call(b"Host"), 178 | mock.call(b": "), 179 | mock.call(b"wifitest.adafruit.com"), 180 | mock.call(b"\r\n"), 181 | ] 182 | ) 183 | assert response.text == str(mocket.MOCK_RESPONSE_TEXT, "utf-8") 184 | 185 | sock.fail_next_send = True 186 | requests_ssl.get("https://" + mocket.MOCK_ENDPOINT_1 + "2") 187 | 188 | sock.connect.assert_called_once_with((mocket.MOCK_HOST_1, 443)) 189 | sock2.connect.assert_called_once_with((mocket.MOCK_HOST_1, 443)) 190 | # Make sure that the socket is closed after send fails. 191 | sock.close.assert_called_once() 192 | assert sock2.close.call_count == 0 193 | assert pool.socket.call_count == 2 194 | 195 | 196 | def test_second_send_lies_recv_fails(pool, requests_ssl): 197 | sock = mocket.Mocket() 198 | sock2 = mocket.Mocket() 199 | pool.socket.side_effect = [sock, sock2] 200 | 201 | response = requests_ssl.get("https://" + mocket.MOCK_ENDPOINT_1) 202 | 203 | sock.send.assert_has_calls( 204 | [ 205 | mock.call(b"testwifi/index.html"), 206 | ] 207 | ) 208 | 209 | sock.send.assert_has_calls( 210 | [ 211 | mock.call(b"Host"), 212 | mock.call(b": "), 213 | mock.call(b"wifitest.adafruit.com"), 214 | mock.call(b"\r\n"), 215 | ] 216 | ) 217 | assert response.text == str(mocket.MOCK_RESPONSE_TEXT, "utf-8") 218 | 219 | requests_ssl.get("https://" + mocket.MOCK_ENDPOINT_1 + "2") 220 | 221 | sock.connect.assert_called_once_with((mocket.MOCK_HOST_1, 443)) 222 | sock2.connect.assert_called_once_with((mocket.MOCK_HOST_1, 443)) 223 | # Make sure that the socket is closed after send fails. 224 | sock.close.assert_called_once() 225 | assert sock2.close.call_count == 0 226 | assert pool.socket.call_count == 2 227 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022 Kevin Conley 2 | # SPDX-FileCopyrightText: 2024 Justin Myers for Adafruit Industries 3 | # 4 | # SPDX-License-Identifier: MIT 5 | 6 | [tox] 7 | envlist = py311 8 | 9 | [testenv] 10 | description = run tests 11 | deps = 12 | pytest==7.4.3 13 | requests 14 | commands = pytest 15 | 16 | [testenv:coverage] 17 | description = run coverage 18 | deps = 19 | pytest==7.4.3 20 | pytest-cov==4.1.0 21 | requests 22 | package = editable 23 | commands = 24 | coverage run --source=. --omit=tests/* --branch {posargs} -m pytest 25 | coverage report 26 | coverage html 27 | 28 | [testenv:lint] 29 | description = run linters 30 | deps = 31 | pre-commit==3.6.0 32 | skip_install = true 33 | commands = pre-commit run {posargs} 34 | 35 | [testenv:docs] 36 | description = build docs 37 | deps = 38 | -r requirements.txt 39 | -r docs/requirements.txt 40 | skip_install = true 41 | commands = sphinx-build -E -W -b html docs/. _build/html 42 | --------------------------------------------------------------------------------